Compare commits

..

1 Commits

Author SHA1 Message Date
Teng Ma f750e9d8ab
[Doc] Update CODEOWNERS with new document owners 2026-04-17 10:07:50 +08:00
103 changed files with 569 additions and 7631 deletions

1
.github/CODEOWNERS vendored
View File

@ -8,6 +8,7 @@
.github @stmatengss @ykwd @Ann-1024 @luketong777
/docs @ShangmingCai @stmatengss @ykwd
/docs/source/design/transfer-engine @alogfans @doujiang24 @chestnut-Q @amd-arozanov @ascend-direct-dev @whn09
/mooncake-ep @UNIDY2002 @ympcMark @yuechen-sys
/mooncake-integration/transfer_engine @ShangmingCai @alogfans
/mooncake-integration/store @ykwd @stmatengss

View File

@ -134,12 +134,10 @@ jobs:
MASTER_PID=$!
sleep 3
cd mooncake-store/go
export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/build/mooncake-common:$GITHUB_WORKSPACE/build/mooncake-store/src:$GITHUB_WORKSPACE/build/mooncake-transfer-engine/src:$GITHUB_WORKSPACE/build/mooncake-transfer-engine/src/common/base:$GITHUB_WORKSPACE/build/mooncake-common/etcd
export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/build/mooncake-asio:$GITHUB_WORKSPACE/build/mooncake-store/src:$GITHUB_WORKSPACE/build/mooncake-transfer-engine/src:$GITHUB_WORKSPACE/build/mooncake-transfer-engine/src/common/base:$GITHUB_WORKSPACE/build/mooncake-common/etcd
export CGO_ENABLED=1
export CGO_CFLAGS="-I$GITHUB_WORKSPACE/mooncake-store/include -I$GITHUB_WORKSPACE/mooncake-transfer-engine/include"
export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/build/mooncake-store/src -L$GITHUB_WORKSPACE/build/mooncake-store/src/cachelib_memory_allocator -L$GITHUB_WORKSPACE/build/mooncake-transfer-engine/src -L$GITHUB_WORKSPACE/build/mooncake-transfer-engine/src/common/base -L$GITHUB_WORKSPACE/build/mooncake-common -L$GITHUB_WORKSPACE/build/mooncake-common/etcd -lmooncake_store -lcachelib_memory_allocator -ltransfer_engine -lbase -lasio -letcd_wrapper -lstdc++ -lnuma -lglog -lgflags -libverbs -ljsoncpp -lzstd -lcurl -luring -lasan -lm -lgcov"
# Link cudart if CUDA is available (needed for D2H staging in mooncake_store)
if [ -d /usr/local/cuda/lib64 ]; then export CGO_LDFLAGS="$CGO_LDFLAGS -L/usr/local/cuda/lib64 -lcudart"; fi
export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/build/mooncake-store/src -L$GITHUB_WORKSPACE/build/mooncake-store/src/cachelib_memory_allocator -L$GITHUB_WORKSPACE/build/mooncake-transfer-engine/src -L$GITHUB_WORKSPACE/build/mooncake-transfer-engine/src/common/base -L$GITHUB_WORKSPACE/build/mooncake-asio -L$GITHUB_WORKSPACE/build/mooncake-common/etcd -lmooncake_store -lcachelib_memory_allocator -ltransfer_engine -lbase -lasio -letcd_wrapper -lstdc++ -lnuma -lglog -lgflags -libverbs -ljsoncpp -lzstd -lcurl -luring -lasan -lm -lgcov"
ASAN_OPTIONS=detect_leaks=0:verify_asan_link_order=0 MC_METADATA_SERVER=http://127.0.0.1:8080/metadata go test -v ./tests/...
kill $MASTER_PID 2>/dev/null || true
shell: bash
@ -746,18 +744,6 @@ jobs:
uses: ./.github/workflows/ci_cu13.yml
secrets: inherit
ascend-test:
needs: [build, check-paths]
if: needs.check-paths.outputs.should-run-downstream == 'true'
uses: ./.github/workflows/ci_ascend.yml
secrets: inherit
integration-test:
needs: [build, check-paths]
if: needs.check-paths.outputs.should-run-downstream == 'true'
uses: ./.github/workflows/integration-test.yml
secrets: inherit
ci-gate:
name: CI Gate
if: always()
@ -770,8 +756,6 @@ jobs:
- build-docker
- test-wheel-ubuntu
- build-wheel-cu13
- ascend-test
- integration-test
runs-on: ubuntu-latest
steps:
- name: Check required job results

View File

@ -64,6 +64,7 @@ jobs:
base="${base#${base%%[![:space:]]*}}"
base="${base%${base##*[![:space:]]}}"
[ -n "$base" ] || return 1
[ "$base" = "https://github.com" ] && base="https://github.com/"
[ "$base" != "https://github.com/" ] && base="${base%/}/"
printf '%s\n' "$base"
}
@ -107,53 +108,22 @@ jobs:
- name: Configure CMake
shell: bash
env:
ASCEND_GITHUB_MIRROR_URLS: ${{ vars.ASCEND_GITHUB_MIRROR_URLS }}
run: |
source /usr/local/Ascend/cann-9.0.0/set_env.sh
pwd
submodule_updated=false
if git submodule update --init --recursive; then
submodule_updated=true
elif [ -n "${ASCEND_GITHUB_MIRROR_URLS:-}" ]; then
normalize_base() {
local base="$1"
base="${base#${base%%[![:space:]]*}}"
base="${base%${base##*[![:space:]]}}"
[ -n "$base" ] || return 1
[ "$base" != "https://github.com/" ] && base="${base%/}/"
printf '%s\n' "$base"
}
while IFS= read -r raw; do
base="$(normalize_base "$raw" || true)"
[ -n "$base" ] || continue
[ "$base" = "https://github.com/" ] && continue
echo "Retrying submodule update with ${base}"
if git -c url."${base}https://github.com/".insteadOf=https://github.com/ \
submodule update --init --recursive; then
submodule_updated=true
break
fi
done < <(printf '%s\n' "$ASCEND_GITHUB_MIRROR_URLS" | tr ',;' '\n')
fi
if [ "$submodule_updated" != true ]; then
if [ ! -d "extern/pybind11" ] || [ -z "$(ls -A 'extern/pybind11' 2>/dev/null)" ]; then
echo "git submodule update failed (mirrors also exhausted), trying to cp pybind11..."
if [ -d "../pybind11" ]; then
cp -r ../pybind11 extern/
if ! git submodule update --init --recursive; then
if [ ! -d "extern/pybind11" ] || [ -z "$(ls -A 'extern/pybind11' 2>/dev/null)" ]; then
echo "git submodule update failed, try to cp pybind11..."
if [ -d "../pybind11" ]; then
cp -r ../pybind11 extern/
else
echo "Error: ../pybind11 does not exist. Cannot copy pybind11."
exit 1
fi
else
echo "Error: ../pybind11 does not exist. Cannot copy pybind11."
exit 1
echo "Detected that extern/pybind11 already exists, continuing execution...."
fi
else
echo "Detected that extern/pybind11 already exists, continuing execution...."
fi
fi
bash scripts/ascend/dependencies_ascend_installation.sh
echo "Configuring CMake..."
rm -rf build

3
.gitignore vendored
View File

@ -5,7 +5,6 @@ build_ofed4
old
local_test
go.sum
!mooncake-common/etcd/go.sum
*.so
bin
mod
@ -205,4 +204,4 @@ CLAUDE.md
_codeql_detected_source_root
# CodeBuddy Memory
.codebuddy/
.codebuddy/

View File

@ -23,16 +23,6 @@ repos:
- id: check-added-large-files
args: ['--maxkb=1024']
- repo: local
hooks:
- id: mooncake-code-format
name: Run Mooncake code format script
entry: ./scripts/code_format.sh
language: system
pass_filenames: false
always_run: true
require_serial: true
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:

View File

@ -45,21 +45,12 @@ option(STORE_USE_REDIS "build mooncake store with redis" OFF)
if (STORE_USE_REDIS)
add_compile_definitions(STORE_USE_REDIS)
endif()
option(STORE_USE_K8S_LEASE "build mooncake store with K8s Lease leader election" OFF)
if (STORE_USE_K8S_LEASE)
if (STORE_USE_ETCD)
message(FATAL_ERROR "STORE_USE_K8S_LEASE and STORE_USE_ETCD cannot be enabled together because both build Go c-shared HA backends.")
endif()
if (USE_ETCD AND NOT USE_ETCD_LEGACY)
message(FATAL_ERROR "STORE_USE_K8S_LEASE cannot be enabled with non-legacy USE_ETCD because both build Go c-shared libraries in the same process.")
endif()
add_compile_definitions(STORE_USE_K8S_LEASE)
endif()
option(STORE_USE_JEMALLOC "Use jemalloc in mooncake store master" OFF)
# Define ASIO macros before building targets that include ASIO headers.
# Define ASIO macros before adding mooncake-asio subdirectory
add_compile_definitions(ASIO_SEPARATE_COMPILATION ASIO_DYN_LINK)
add_subdirectory(mooncake-asio)
add_subdirectory(mooncake-common)
include_directories(mooncake-common/etcd)

View File

@ -41,7 +41,6 @@ Mooncake uses [pre-commit](https://pre-commit.com/) to enforce consistent format
| Type | Tool | Purpose |
|------|------|---------|
| Generic | trailing-whitespace / end-of-file-fixer | Basic hygiene |
| Project | `./scripts/code_format.sh` | Enforce Mooncake C/C++ formatting script before commit |
| Python | ruff / ruff-format | Lint + format (includes import sorting) |
| Spelling | codespell | Catch common typos (ignores domain-specific words) |
| C/C++ | clang-format | Apply style from the repository's `.clang-format` |
@ -54,8 +53,6 @@ pip install -r requirements-dev.txt
pre-commit install
```
After installation, every commit will run `./scripts/code_format.sh` automatically. If it rewrites files, re-stage the changes and commit again.
#### Usage
Run on all files (first run will install hook environments):
```bash

View File

@ -23,7 +23,7 @@ NC="\033[0m" # No Color
# Configuration
REPO_ROOT=`pwd`
GITHUB_PROXY=${GITHUB_PROXY:-"https://github.com"}
GOVER=1.25.9
GOVER=1.23.8
# Function to print section headers
print_section() {

View File

@ -88,11 +88,10 @@ store.setup(
| `MOONCAKE_OFFLOAD_FILE_STORAGE_PATH` | `/data/file_storage` | Absolute path to the SSD storage directory |
| `MOONCAKE_OFFLOAD_STORAGE_BACKEND_DESCRIPTOR` | `bucket_storage_backend` | Storage backend type (see below) |
| `MOONCAKE_OFFLOAD_LOCAL_BUFFER_SIZE_BYTES` | `1342177280` (1.25 GB) | Client-side staging buffer size |
| `MOONCAKE_OFFLOAD_SCANMETA_ITERATOR_KEYS_LIMIT` | `20000` | Max keys processed per iteration when scanning existing SSD metadata on startup |
| `MOONCAKE_OFFLOAD_TOTAL_SIZE_LIMIT_BYTES` | `2199023255552` (2 TB) | Maximum disk usage |
| `MOONCAKE_OFFLOAD_TOTAL_KEYS_LIMIT` | `10000000` | Maximum number of objects on disk |
| `MOONCAKE_OFFLOAD_HEARTBEAT_INTERVAL_SECONDS` | `10` | Interval for offload heartbeat to master (seconds) |
| `MOONCAKE_OFFLOAD_USE_URING` | `false` | Enable io_uring for async file I/O |
| `MOONCAKE_USE_URING` | `false` | Enable io_uring for async file I/O |
### Bucket backend settings
@ -102,8 +101,8 @@ Applies when `MOONCAKE_OFFLOAD_STORAGE_BACKEND_DESCRIPTOR=bucket_storage_backend
|---|---|---|
| `MOONCAKE_OFFLOAD_BUCKET_SIZE_LIMIT_BYTES` | `268435456` (256 MB) | Max size per bucket |
| `MOONCAKE_OFFLOAD_BUCKET_KEYS_LIMIT` | `500` | Max keys per bucket |
| `MOONCAKE_OFFLOAD_BUCKET_MAX_TOTAL_SIZE` | `0` | Eviction threshold in bytes. When set to `0`, the backend uses **90% of the physical disk capacity** as the quota — it does not mean unlimited. Set an explicit value to control disk usage precisely. |
| `MOONCAKE_OFFLOAD_BUCKET_EVICTION_POLICY` | `none` | Eviction policy: `none` / `fifo` / `lru` |
| `MOONCAKE_BUCKET_MAX_TOTAL_SIZE` | `0` | Eviction threshold in bytes. When set to `0`, the backend uses **90% of the physical disk capacity** as the quota — it does not mean unlimited. Set an explicit value to control disk usage precisely. |
| `MOONCAKE_BUCKET_EVICTION_POLICY` | `none` | Eviction policy: `none` / `fifo` / `lru` |
---
@ -128,11 +127,6 @@ Best for: general-purpose use, large-scale deployments.
Stores each object in an individual file. Simple and easy to inspect, but generates many small files at scale.
| Environment Variable | Default | Description |
|---|---|---|
| `MOONCAKE_OFFLOAD_FSDIR` | `file_per_key_dir` | Subdirectory name under `MOONCAKE_OFFLOAD_FILE_STORAGE_PATH` where objects are stored |
| `MOONCAKE_OFFLOAD_ENABLE_EVICTION` | `true` | Enable disk eviction when the total size exceeds the quota |
Best for: debugging or small-scale deployments.
### `offset_allocator_storage_backend`
@ -149,7 +143,7 @@ Best for: high-concurrency scenarios with many small objects where restart durab
## Eviction (Bucket Backend Only)
When `MOONCAKE_OFFLOAD_BUCKET_MAX_TOTAL_SIZE` is set, the backend automatically evicts buckets before writing new ones if total disk usage would exceed the limit.
When `MOONCAKE_BUCKET_MAX_TOTAL_SIZE` is set, the backend automatically evicts buckets before writing new ones if total disk usage would exceed the limit.
| Policy | Behavior |
|--------|----------|
@ -184,8 +178,8 @@ mooncake_master \
```bash
export MOONCAKE_OFFLOAD_FILE_STORAGE_PATH=/nvme/mooncake_offload
export MOONCAKE_OFFLOAD_STORAGE_BACKEND_DESCRIPTOR=bucket_storage_backend
export MOONCAKE_OFFLOAD_BUCKET_MAX_TOTAL_SIZE=$((200 * 1024 * 1024 * 1024)) # 200 GB
export MOONCAKE_OFFLOAD_BUCKET_EVICTION_POLICY=lru
export MOONCAKE_BUCKET_MAX_TOTAL_SIZE=$((200 * 1024 * 1024 * 1024)) # 200 GB
export MOONCAKE_BUCKET_EVICTION_POLICY=lru
mooncake_client \
--master_server_address="192.168.1.10:50051" \
@ -256,7 +250,7 @@ mooncake_client \
### "Failed to register buffer with UringFile" warning in logs
This warning appears when `MOONCAKE_OFFLOAD_USE_URING=true` and the io_uring fixed-buffer registration fails. The most common cause is that `MOONCAKE_OFFLOAD_LOCAL_BUFFER_SIZE_BYTES` exceeds the process's locked-memory limit (`RLIMIT_MEMLOCK`). io_uring requires the registered buffer to be pinned in physical memory, which counts against this limit.
This warning appears when `MOONCAKE_USE_URING=true` and the io_uring fixed-buffer registration fails. The most common cause is that `MOONCAKE_OFFLOAD_LOCAL_BUFFER_SIZE_BYTES` exceeds the process's locked-memory limit (`RLIMIT_MEMLOCK`). io_uring requires the registered buffer to be pinned in physical memory, which counts against this limit.
Check the current limit:

View File

@ -154,7 +154,7 @@ A single pre-allocated file (`kv_cache.data`) is shared by all objects. Space wi
## Eviction (BucketStorageBackend)
When `MOONCAKE_OFFLOAD_BUCKET_MAX_TOTAL_SIZE` is set, the backend evicts existing buckets to make room before writing a new one. Eviction is disabled by default (`BucketEvictionPolicy::NONE`).
When `MOONCAKE_BUCKET_MAX_TOTAL_SIZE` is set, the backend evicts existing buckets to make room before writing a new one. Eviction is disabled by default (`BucketEvictionPolicy::NONE`).
### Policies
@ -195,7 +195,7 @@ This ordering guarantees:
## io_uring File I/O
When `MOONCAKE_OFFLOAD_USE_URING=true`, the storage backends replace POSIX `pread`/`pwrite` calls with an io_uring-based implementation (`UringFile`). The design prioritizes eliminating inter-thread lock contention, which was the dominant latency source in the previous global-ring approach.
When `MOONCAKE_USE_URING=true`, the storage backends replace POSIX `pread`/`pwrite` calls with an io_uring-based implementation (`UringFile`). The design prioritizes eliminating inter-thread lock contention, which was the dominant latency source in the previous global-ring approach.
### Thread-local rings (`SharedUringRing`)

View File

@ -81,7 +81,7 @@ make -j$(nproc)
# Copy built modules to wheel directory
cp mooncake-integration/engine.cpython-*.so ../mooncake-wheel/mooncake/
cp mooncake-integration/store.cpython-*.so ../mooncake-wheel/mooncake/
cp mooncake-common/libasio.so ../mooncake-wheel/mooncake/
cp mooncake-asio/libasio.so ../mooncake-wheel/mooncake/
# Install with pip
pip install -e ../mooncake-wheel --no-build-isolation
@ -224,30 +224,6 @@ Earlier CPU-to-CPU tuning results (before EFA striping optimization, when `MC_SL
</details>
#### p6-b300.48xlarge (B300, 16 EFA × 400 Gbps)
Tested on two p6-b300.48xlarge instances (Intel Xeon Platinum 8559C, 8× B300, 16 EFA devices) in the same AWS placement group.
**GPU-to-GPU** (build with `-DUSE_CUDA=ON`, `--gpu_id=-1` for all 8 GPUs, `--buffer_size=2147483648`):
| Configuration | Write | Read |
|---------------|-------|------|
| block=1MB, threads=16, batch=128 | 701 GB/s | **697 GB/s** |
| **block=1MB, threads=32, batch=64** | **752 GB/s** | 713 GB/s |
| block=1MB, threads=32, batch=32 | 751 GB/s | - |
| block=1MB, threads=64, batch=32 | 728 GB/s | - |
> **Peak: 752 GB/s write**, reaching ~94% of the 800 GB/s theoretical line rate (16×400 Gbps). GPUDirect RDMA bypasses DRAM entirely (HBM3e → PCIe switch → NIC), so performance is not bottlenecked by CPU memory bandwidth.
**CPU-to-CPU** (build with `-DUSE_CUDA=OFF`):
| Configuration | Write | Read |
|---------------|-------|------|
| **block=1MB, threads=32, batch=128, buf=4GB** | **230 GB/s** | 180 GB/s |
| block=16MB, threads=32, batch=8, buf=8GB (striping off) | 233 GB/s | - |
> CPU-to-CPU is bounded by DRAM bandwidth (~250 GB/s/socket on Xeon 8559C). Per-NIC sampling shows NUMA-0 NICs at 90 Gbps and NUMA-1 NICs at 53 Gbps, confirming DRAM controller saturation rather than NIC limit.
#### p5en.48xlarge (H200, 16 EFA × 200 Gbps)
Tested on two p5en.48xlarge instances (Intel Xeon 8488C, 8× H200 141GB, 16 EFA devices) in the same AWS placement group.
@ -277,10 +253,8 @@ Tested on two p5en.48xlarge instances (Intel Xeon 8488C, 8× H200 141GB, 16 EFA
| Transport | Throughput | Notes |
|-----------|-----------|-------|
| **EFA GPU-to-GPU (B300)** | **752 GB/s** | p6-b300.48xlarge, 16×400G, block=1MB, ~94% line rate |
| **EFA GPU-to-GPU (H200)** | **347 GB/s** | p5en.48xlarge, 16×200G, block=1MB |
| **EFA GPU-to-GPU (B200)** | **313 GB/s** | p6-b200.48xlarge, 8×400G, block=1MB |
| **EFA CPU-to-CPU (B300)** | **230 GB/s** | p6-b300.48xlarge, 16×400G, block=1MB, DRAM-limited |
| **EFA CPU-to-CPU (B200)** | **222 GB/s** | p6-b200.48xlarge, 8×400G, block=1MB, DRAM-limited |
| **EFA CPU-to-CPU (H200)** | **192 GB/s** | p5en.48xlarge, block=1MB, NUMA-split, DRAM-limited |
| EFA (default params) | 69.47 GB/s | Default block=64KB |
@ -299,27 +273,6 @@ Tested on two p5en.48xlarge instances (Intel Xeon 8488C, 8× H200 141GB, 16 EFA
- Allocate buffers on both NUMA nodes for balanced NIC utilization (the bench tool does this by default for CPU mode)
- On 16-NIC instances (p5en), writes are NUMA-sensitive: 8 local-NUMA NICs reach 90 Gbps each, while 8 cross-NUMA NICs only reach ~20 Gbps without NUMA-split
### Eager endpoint warmup (first-request latency)
libfabric `FI_EP_RDM` endpoints resolve peer addresses lazily: `fi_av_insert()` and the metadata handshake fire on the first send to each `(local_ctx, peer_nic)` pair. On 16-NIC instances that gives `16 × N_peer_NICs` serial handshakes inside the first `submitTransfer`, which shows up as a single-digit-second first-batch stall (measured ~4 s on p6-B300 for a 100 × 0.5 MB batch; the first batch runs at <0.1 GB/s while the CQ drains, steady-state afterwards is unaffected).
Mooncake exposes an explicit eager-warmup API to eliminate the stall:
- C++: `EfaTransport::warmupSegment(const std::string& segment_name)`
- C: `int warmupEfaSegment(transfer_engine_t engine, const char *segment_name)`
- Rust: `TransferEngine::warmup_efa_segment(name: &str)`
Call it once per peer segment, right after `openSegment` (or after any metadata change that adds a new peer). Every `(local_ctx, peer_nic)` endpoint is connected concurrently via `std::async`; the critical path becomes `max(handshake RTT)` instead of `sum(handshake RTT)`. The call is idempotent — safe to re-run.
Measured on p6-B300 (16 local NICs × 16 peer NICs, dual-NUMA initiator, 100 × 0.5 MB batch):
| | first-batch latency | steady-state |
|---|---:|---:|
| No warmup | 4,043 ms | 141 GB/s |
| `warmup_efa_segment` (256 endpoints connected in 4.1 s) | **13.5 ms** (~300×) | 230 GB/s |
The warmup call itself takes roughly the same wall time as the stall it replaces — the win is that it's a one-time setup cost decoupled from the critical path of the first real transfer, not paid inside your latency budget.
## Usage with vLLM
### Prefill Instance

View File

@ -99,7 +99,7 @@ make -j$(nproc)
# Copy built modules to wheel directory
cp mooncake-integration/engine.cpython-*.so ../mooncake-wheel/mooncake/
cp mooncake-integration/store.cpython-*.so ../mooncake-wheel/mooncake/
cp mooncake-common/libasio.so ../mooncake-wheel/mooncake/
cp mooncake-asio/libasio.so ../mooncake-wheel/mooncake/
# Install with pip
pip install -e ../mooncake-wheel --no-build-isolation

View File

@ -141,7 +141,7 @@ mooncake_master \
# SSD Offload Buffer: 20GB
MOONCAKE_OFFLOAD_FILE_STORAGE_PATH="/mnt/data/file_storage" \
MOONCAKE_OFFLOAD_LOCAL_BUFFER_SIZE_BYTES=21474836480 \
MOONCAKE_OFFLOAD_USE_URING=1 \
MOONCAKE_USE_URING=1 \
mooncake_client \
--host=127.0.0.1 \
--global_segment_size=80GB \

View File

@ -0,0 +1,50 @@
# Build asio as a shared library to avoid ODR violations
# when multiple shared libraries use asio
# Try to find ASIO using find_package first
find_package(asio QUIET)
if(asio_FOUND)
message(STATUS "Found ASIO via find_package")
set(ASIO_INCLUDE_DIR ${asio_INCLUDE_DIR})
else()
# Fallback to find_path if find_package fails
find_path(ASIO_INCLUDE_DIR
NAMES asio.hpp
PATHS
/usr/local/include
/usr/include
${CMAKE_INSTALL_PREFIX}/include
DOC "Path to ASIO headers"
)
if(NOT ASIO_INCLUDE_DIR)
message(FATAL_ERROR "ASIO not found. Please install ASIO or set ASIO_INCLUDE_DIR manually.")
endif()
message(STATUS "Found ASIO at: ${ASIO_INCLUDE_DIR}")
endif()
add_library(asio_shared SHARED asio_impl.cpp)
target_compile_definitions(asio_shared
PUBLIC
ASIO_SEPARATE_COMPILATION
ASIO_DYN_LINK
)
target_include_directories(asio_shared
PUBLIC
${ASIO_INCLUDE_DIR}
)
set_target_properties(asio_shared PROPERTIES
POSITION_INDEPENDENT_CODE ON
INSTALL_RPATH "$ORIGIN"
BUILD_WITH_INSTALL_RPATH TRUE
OUTPUT_NAME "asio"
)
target_link_libraries(asio_shared PUBLIC pthread)
install(TARGETS asio_shared DESTINATION lib)

View File

@ -2,10 +2,6 @@ if ((USE_ETCD AND NOT USE_ETCD_LEGACY) OR STORE_USE_ETCD)
add_subdirectory(etcd)
endif()
if (STORE_USE_K8S_LEASE)
add_subdirectory(k8s-lease)
endif()
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
add_subdirectory(src)

View File

@ -3,7 +3,7 @@ add_custom_command(
COMMAND bash -c "go mod tidy" && bash -c "go build -buildmode=c-shared -o ${CMAKE_CURRENT_BINARY_DIR}/libetcd_wrapper.so etcd_wrapper.go" && cp ${CMAKE_CURRENT_BINARY_DIR}/libetcd_wrapper.h ${CMAKE_CURRENT_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Building Go shared library"
DEPENDS etcd_wrapper.go go.mod go.sum build.sh
DEPENDS etcd_wrapper.go
)
set(ETCD_WRAPPER_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/libetcd_wrapper.h)
@ -17,4 +17,4 @@ add_custom_target(
install(
FILES ${ETCD_WRAPPER_LIB}
DESTINATION lib
)
)

View File

@ -1,8 +1,6 @@
module github.com/kvcache-ai/Mooncake/mooncake-common/etcd
go 1.25.0
toolchain go1.25.9
go 1.24.0
require (
go.etcd.io/etcd/api/v3 v3.5.21

View File

@ -1,108 +0,0 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,20 +0,0 @@
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/libk8s_lease_wrapper.so
COMMAND bash -c "go mod tidy" && bash -c "go build -buildmode=c-shared -o ${CMAKE_CURRENT_BINARY_DIR}/libk8s_lease_wrapper.so k8s_lease_wrapper.go" && cp ${CMAKE_CURRENT_BINARY_DIR}/libk8s_lease_wrapper.h ${CMAKE_CURRENT_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Building K8s Lease Go shared library"
DEPENDS k8s_lease_wrapper.go
)
set(K8S_LEASE_WRAPPER_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/libk8s_lease_wrapper.h)
set(K8S_LEASE_WRAPPER_LIB ${CMAKE_CURRENT_BINARY_DIR}/libk8s_lease_wrapper.so)
add_custom_target(
build_k8s_lease_wrapper
DEPENDS ${K8S_LEASE_WRAPPER_LIB}
)
install(
FILES ${K8S_LEASE_WRAPPER_LIB}
DESTINATION lib
)

View File

@ -1,61 +0,0 @@
// envtest-server starts a real kube-apiserver + etcd via envtest, writes the
// KUBECONFIG path to stdout, and blocks until SIGTERM or SIGINT. This lets
// C++ tests launch it as a subprocess and talk to a real K8s API without a
// full cluster.
package main
import (
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
func main() {
env := &envtest.Environment{}
cfg, err := env.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "envtest start failed: %v\n", err)
os.Exit(1)
}
// Write a KUBECONFIG file that points at the envtest kube-apiserver.
kubeconfigPath := filepath.Join(os.TempDir(), fmt.Sprintf("envtest-kubeconfig-%d", os.Getpid()))
kubeconfig := clientcmdapi.NewConfig()
kubeconfig.Clusters["envtest"] = &clientcmdapi.Cluster{
Server: cfg.Host,
CertificateAuthorityData: cfg.CAData,
}
kubeconfig.AuthInfos["envtest"] = &clientcmdapi.AuthInfo{
ClientCertificateData: cfg.CertData,
ClientKeyData: cfg.KeyData,
}
kubeconfig.Contexts["envtest"] = &clientcmdapi.Context{
Cluster: "envtest",
AuthInfo: "envtest",
}
kubeconfig.CurrentContext = "envtest"
if err := clientcmd.WriteToFile(*kubeconfig, kubeconfigPath); err != nil {
fmt.Fprintf(os.Stderr, "failed to write kubeconfig: %v\n", err)
env.Stop()
os.Exit(1)
}
// Print the kubeconfig path — the parent process reads this from stdout.
fmt.Println(kubeconfigPath)
// Block until SIGTERM or SIGINT.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh
os.Remove(kubeconfigPath)
env.Stop()
}

View File

@ -1,60 +0,0 @@
module github.com/kvcache-ai/Mooncake/mooncake-common/k8s-lease
go 1.24.0
require (
k8s.io/api v0.34.3
k8s.io/apimachinery v0.34.3
k8s.io/client-go v0.34.3
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/controller-runtime v0.22.5
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.34.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

View File

@ -1,571 +0,0 @@
//go:build integration
package main
import (
"context"
"fmt"
"os"
"sync"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
var (
testEnv *envtest.Environment
testConfig *rest.Config
)
type electionStateNoRelease struct {
cancel context.CancelFunc
elected chan struct{}
lost chan struct{}
}
func TestMain(m *testing.M) {
testEnv = &envtest.Environment{}
var err error
testConfig, err = testEnv.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to start envtest: %v\n", err)
os.Exit(1)
}
// Set up global client for the wrapper
client, err := kubernetes.NewForConfig(testConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create clientset: %v\n", err)
testEnv.Stop()
os.Exit(1)
}
clientMutex.Lock()
globalClient = client
clientMutex.Unlock()
code := m.Run()
testEnv.Stop()
os.Exit(code)
}
func runElectionWithoutRelease(namespace, leaseName, identity string,
leaseDurationSec, renewDeadlineSec, retryPeriodSec int) (*electionStateNoRelease, error) {
if err := ensureClientInitialized(); err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
state := &electionStateNoRelease{
cancel: cancel,
elected: make(chan struct{}),
lost: make(chan struct{}),
}
lock := &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: leaseName,
Namespace: namespace,
},
Client: globalClient.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: identity,
},
}
le, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
Lock: lock,
LeaseDuration: time.Duration(leaseDurationSec) * time.Second,
RenewDeadline: time.Duration(renewDeadlineSec) * time.Second,
RetryPeriod: time.Duration(retryPeriodSec) * time.Second,
ReleaseOnCancel: false,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
close(state.elected)
<-ctx.Done()
},
OnStoppedLeading: func() {
close(state.lost)
},
},
})
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create leader elector: %w", err)
}
go le.Run(ctx)
return state, nil
}
// TestSingleLeaderElection verifies a single candidate becomes leader.
func TestSingleLeaderElection(t *testing.T) {
ns := "default"
lease := "single-election-test"
identity := "node-1:8080"
err := runElection(ns, lease, identity, 5, 4, 1)
if err != nil {
t.Fatalf("runElection failed: %v", err)
}
// Wait for elected
key := electionKey(ns, lease)
electionMutex.Lock()
state := elections[key]
electionMutex.Unlock()
select {
case <-state.elected:
// success
case <-time.After(15 * time.Second):
t.Fatal("timed out waiting for election")
}
// Verify holder via getHolder
holder, transitions, err := getHolder(ns, lease)
if err != nil {
t.Fatalf("getHolder failed: %v", err)
}
if holder != identity {
t.Errorf("expected holder %q, got %q", identity, holder)
}
// First election — transitions should be 0 or 1
if transitions < 0 {
t.Errorf("expected non-negative transitions, got %d", transitions)
}
// Cancel the election
electionMutex.Lock()
state = elections[key]
electionMutex.Unlock()
state.cancel()
select {
case <-state.lost:
// success
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for election loss after cancel")
}
}
// TestLeaderEpoch verifies leaseTransitions increments across elections.
func TestLeaderEpoch(t *testing.T) {
ns := "default"
lease := "epoch-test"
// First election
err := runElection(ns, lease, "node-epoch-1:8080", 5, 4, 1)
if err != nil {
t.Fatalf("first runElection failed: %v", err)
}
key := electionKey(ns, lease)
electionMutex.Lock()
state1 := elections[key]
electionMutex.Unlock()
select {
case <-state1.elected:
case <-time.After(15 * time.Second):
t.Fatal("timed out on first election")
}
_, trans1, _ := getHolder(ns, lease)
// Cancel first election and wait for loss
state1.cancel()
select {
case <-state1.lost:
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for first election loss")
}
// Wait for lease to expire / be released
time.Sleep(2 * time.Second)
// Second election
err = runElection(ns, lease, "node-epoch-2:8080", 5, 4, 1)
if err != nil {
t.Fatalf("second runElection failed: %v", err)
}
electionMutex.Lock()
state2 := elections[key]
electionMutex.Unlock()
select {
case <-state2.elected:
case <-time.After(15 * time.Second):
t.Fatal("timed out on second election")
}
_, trans2, _ := getHolder(ns, lease)
if trans2 <= trans1 {
t.Errorf("expected transitions to increment: first=%d, second=%d", trans1, trans2)
}
state2.cancel()
select {
case <-state2.lost:
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for second election loss")
}
}
// TestSequentialLeadershipHandoff tests that a second candidate can acquire
// leadership after the first one releases it.
func TestSequentialLeadershipHandoff(t *testing.T) {
ns := "default"
lease := "two-candidate-test"
err1 := runElection(ns, lease, "candidate-a:8080", 5, 4, 1)
if err1 != nil {
t.Fatalf("first runElection failed: %v", err1)
}
key := electionKey(ns, lease)
electionMutex.Lock()
stateA := elections[key]
electionMutex.Unlock()
// Wait for first candidate to win
select {
case <-stateA.elected:
case <-time.After(15 * time.Second):
t.Fatal("timed out waiting for first candidate")
}
// Verify holder is candidate-a
holder, _, err := getHolder(ns, lease)
if err != nil {
t.Fatalf("getHolder failed: %v", err)
}
if holder != "candidate-a:8080" {
t.Errorf("expected candidate-a, got %q", holder)
}
// Cancel candidate-a
stateA.cancel()
select {
case <-stateA.lost:
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for candidate-a loss")
}
// Wait for lease to expire
time.Sleep(2 * time.Second)
// Start candidate-b
err2 := runElection(ns, lease, "candidate-b:8080", 5, 4, 1)
if err2 != nil {
t.Fatalf("second runElection failed: %v", err2)
}
electionMutex.Lock()
stateB := elections[key]
electionMutex.Unlock()
select {
case <-stateB.elected:
case <-time.After(15 * time.Second):
t.Fatal("timed out waiting for candidate-b")
}
holder, _, err = getHolder(ns, lease)
if err != nil {
t.Fatalf("getHolder after takeover failed: %v", err)
}
if holder != "candidate-b:8080" {
t.Errorf("expected candidate-b, got %q", holder)
}
stateB.cancel()
select {
case <-stateB.lost:
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for candidate-b loss")
}
}
// TestConcurrentCandidateElection starts two candidates simultaneously and
// verifies that exactly one wins leadership.
func TestConcurrentCandidateElection(t *testing.T) {
ns := "default"
lease := "concurrent-election-test"
type result struct {
identity string
elected bool
}
candidates := []string{"candidate-a:8080", "candidate-b:8080"}
results := make(chan result, len(candidates))
lock := func(identity string) *resourcelock.LeaseLock {
return &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: lease,
Namespace: ns,
},
Client: globalClient.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: identity,
},
}
}
var wg sync.WaitGroup
for _, id := range candidates {
wg.Add(1)
go func(identity string) {
defer wg.Done()
// Short timeout: enough for one to acquire, but the loser
// times out before the winner's lease could expire.
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
elected := make(chan struct{})
le, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
Lock: lock(identity),
LeaseDuration: 5 * time.Second,
RenewDeadline: 3 * time.Second,
RetryPeriod: 1 * time.Second,
ReleaseOnCancel: true,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
close(elected)
<-ctx.Done()
},
OnStoppedLeading: func() {},
},
})
if err != nil {
t.Errorf("NewLeaderElector(%s): %v", identity, err)
return
}
go le.Run(ctx)
select {
case <-elected:
results <- result{identity, true}
// Keep holding until context expires (8s total).
// Winner does NOT release early, so loser cannot
// re-acquire within its own 8s window.
<-ctx.Done()
case <-ctx.Done():
results <- result{identity, false}
}
}(id)
}
wg.Wait()
close(results)
winners := 0
for r := range results {
if r.elected {
winners++
t.Logf("winner: %s", r.identity)
}
}
if winners != 1 {
t.Fatalf("expected exactly 1 winner, got %d", winners)
}
}
// TestCancelElection tests that cancelling an election makes WaitLost return.
func TestCancelElection(t *testing.T) {
ns := "default"
lease := "cancel-test"
err := runElection(ns, lease, "cancel-node:8080", 5, 4, 1)
if err != nil {
t.Fatalf("runElection failed: %v", err)
}
key := electionKey(ns, lease)
electionMutex.Lock()
state := elections[key]
electionMutex.Unlock()
// Wait for elected
select {
case <-state.elected:
case <-time.After(15 * time.Second):
t.Fatal("timed out waiting for election")
}
// Cancel
state.cancel()
// WaitLost should return promptly
select {
case <-state.lost:
// success
case <-time.After(10 * time.Second):
t.Fatal("WaitLost did not return after cancel")
}
}
// TestGetHolderDuringElection verifies getHolder works while election is active.
func TestGetHolderDuringElection(t *testing.T) {
ns := "default"
lease := "active-get-holder-test"
identity := "active-node:8080"
err := runElection(ns, lease, identity, 5, 4, 1)
if err != nil {
t.Fatalf("runElection failed: %v", err)
}
key := electionKey(ns, lease)
electionMutex.Lock()
state := elections[key]
electionMutex.Unlock()
select {
case <-state.elected:
case <-time.After(15 * time.Second):
t.Fatal("timed out waiting for election")
}
// Concurrent getHolder calls during active election
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
holder, _, err := getHolder(ns, lease)
if err != nil {
t.Errorf("getHolder during election failed: %v", err)
return
}
if holder != identity {
t.Errorf("expected %q, got %q", identity, holder)
}
}()
}
wg.Wait()
state.cancel()
<-state.lost
}
// TestGetHolderReturnsEmptyAfterLeaderDeath verifies that after a leader stops
// renewing its lease without releasing it, getHolder returns an empty holder
// once the lease expires. This is the integration-level counterpart to the
// unit test TestGetHolderReturnsEmptyForExpiredLease.
func TestGetHolderReturnsEmptyAfterLeaderDeath(t *testing.T) {
ns := "default"
lease := "expired-leader-test"
identity := "doomed-leader:8080"
// Acquire leadership without ReleaseOnCancel so canceling simulates a dead
// leader that stops renewing and leaves the old holder until expiry.
state, err := runElectionWithoutRelease(ns, lease, identity, 5, 4, 1)
if err != nil {
t.Fatalf("runElection failed: %v", err)
}
select {
case <-state.elected:
case <-time.After(15 * time.Second):
t.Fatal("timed out waiting for election")
}
// Verify holder while active.
holder, _, err := getHolder(ns, lease)
if err != nil {
t.Fatalf("getHolder (active) failed: %v", err)
}
if holder != identity {
t.Fatalf("expected active holder %q, got %q", identity, holder)
}
// Simulate leader death: stop renewing without explicitly releasing.
state.cancel()
select {
case <-state.lost:
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for loss")
}
// Wait for the lease to expire (leaseDuration=5s, add margin).
time.Sleep(7 * time.Second)
// After expiry, getHolder must return empty holder so that the
// supervisor will attempt acquisition.
holder, _, err = getHolder(ns, lease)
if err != nil {
t.Fatalf("getHolder (expired) failed: %v", err)
}
if holder != "" {
t.Errorf("expected empty holder after lease expiry, got %q", holder)
}
}
// TestFailoverAfterLeaderDeath verifies that a new candidate can acquire
// leadership after the previous leader dies and its lease expires.
func TestFailoverAfterLeaderDeath(t *testing.T) {
ns := "default"
lease := "failover-test"
// First leader acquires without ReleaseOnCancel so canceling leaves the
// old holder in place until the lease naturally expires.
state1, err := runElectionWithoutRelease(ns, lease, "leader-1:8080", 5, 4, 1)
if err != nil {
t.Fatalf("first runElection failed: %v", err)
}
select {
case <-state1.elected:
case <-time.After(15 * time.Second):
t.Fatal("timed out waiting for first election")
}
// Simulate crash: cancel without release, wait for expiry.
state1.cancel()
<-state1.lost
time.Sleep(7 * time.Second)
// Second candidate should be able to acquire.
err = runElection(ns, lease, "leader-2:8080", 5, 4, 1)
if err != nil {
t.Fatalf("second runElection failed: %v", err)
}
key := electionKey(ns, lease)
electionMutex.Lock()
state2 := elections[key]
electionMutex.Unlock()
select {
case <-state2.elected:
// success — failover worked
case <-time.After(15 * time.Second):
t.Fatal("second candidate failed to acquire after leader death")
}
holder, _, err := getHolder(ns, lease)
if err != nil {
t.Fatalf("getHolder after failover failed: %v", err)
}
if holder != "leader-2:8080" {
t.Errorf("expected new leader %q, got %q", "leader-2:8080", holder)
}
state2.cancel()
<-state2.lost
}

View File

@ -1,489 +0,0 @@
package main
/*
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// Trampoline to invoke C/C++ callback safely from Go via cgo.
typedef void (*holder_change_cb_t)(void* ctx,
const char* holder, size_t holderSize,
int64_t leaseTransitions);
static inline void call_holder_change_cb(holder_change_cb_t func, void* ctx,
const char* holder, size_t holderSize,
int64_t leaseTransitions) {
func(ctx, holder, holderSize, leaseTransitions);
}
*/
import "C"
import (
"context"
"fmt"
"os"
"sync"
"time"
"unsafe"
coordinationv1 "k8s.io/api/coordination/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
)
// electionState holds the runtime state for a single leader election.
type electionState struct {
cancel context.CancelFunc
elected chan struct{} // closed when OnStartedLeading fires
lost chan struct{} // closed when OnStoppedLeading fires
err error // set before lost is closed, if any
transitions int64 // set before elected is closed
}
// watchState holds the runtime state for a single Lease watch.
type watchState struct {
cancel context.CancelFunc
}
var (
globalClient kubernetes.Interface
clientMutex sync.Mutex
initClientFn = initClient
elections = make(map[string]*electionState)
electionMutex sync.Mutex
watches = make(map[string]*watchState)
watchMutex sync.Mutex
)
func electionKey(namespace, leaseName string) string {
return namespace + "/" + leaseName
}
func ensureClientInitialized() error {
clientMutex.Lock()
initialized := globalClient != nil
clientMutex.Unlock()
if initialized {
return nil
}
return initClientFn()
}
// initClient creates the K8s clientset from in-cluster config or KUBECONFIG.
func initClient() error {
clientMutex.Lock()
defer clientMutex.Unlock()
if globalClient != nil {
return nil
}
config, err := rest.InClusterConfig()
if err != nil {
// Fall back to KUBECONFIG
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
home := os.Getenv("HOME")
if home != "" {
kubeconfig = home + "/.kube/config"
}
}
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return fmt.Errorf("failed to build k8s config: %w", err)
}
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to create k8s clientset: %w", err)
}
globalClient = client
return nil
}
// runElection starts a leader election goroutine for the given namespace/leaseName.
func runElection(namespace, leaseName, identity string,
leaseDurationSec, renewDeadlineSec, retryPeriodSec int) error {
if err := ensureClientInitialized(); err != nil {
return err
}
key := electionKey(namespace, leaseName)
electionMutex.Lock()
if _, exists := elections[key]; exists {
electionMutex.Unlock()
return fmt.Errorf("election already running for %s", key)
}
ctx, cancel := context.WithCancel(context.Background())
state := &electionState{
cancel: cancel,
elected: make(chan struct{}),
lost: make(chan struct{}),
}
elections[key] = state
electionMutex.Unlock()
lock := &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: leaseName,
Namespace: namespace,
},
Client: globalClient.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: identity,
},
}
le, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
Lock: lock,
LeaseDuration: time.Duration(leaseDurationSec) * time.Second,
RenewDeadline: time.Duration(renewDeadlineSec) * time.Second,
RetryPeriod: time.Duration(retryPeriodSec) * time.Second,
ReleaseOnCancel: true,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
_, transitions, err := getHolder(namespace, leaseName)
if err == nil {
state.transitions = transitions
}
close(state.elected)
// Block until context is cancelled (leadership lost or explicit cancel)
<-ctx.Done()
},
OnStoppedLeading: func() {
close(state.lost)
// Auto-cleanup: remove from map so the same key can be reused.
electionMutex.Lock()
if elections[key] == state {
delete(elections, key)
}
electionMutex.Unlock()
},
},
})
if err != nil {
electionMutex.Lock()
delete(elections, key)
electionMutex.Unlock()
cancel()
return fmt.Errorf("failed to create leader elector: %w", err)
}
go le.Run(ctx)
return nil
}
// getHolder reads the current Lease holder identity and transitions.
func getHolder(namespace, leaseName string) (string, int64, error) {
if err := ensureClientInitialized(); err != nil {
return "", 0, err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
lease, err := globalClient.CoordinationV1().Leases(namespace).Get(ctx, leaseName, metav1.GetOptions{})
if err != nil {
return "", 0, fmt.Errorf("failed to get lease: %w", err)
}
holder := ""
if lease.Spec.HolderIdentity != nil {
holder = *lease.Spec.HolderIdentity
}
transitions := int64(0)
if lease.Spec.LeaseTransitions != nil {
transitions = int64(*lease.Spec.LeaseTransitions)
}
// Treat expired leases as having no holder so that the C++ supervisor
// will attempt acquisition instead of going to standby.
if holder != "" && lease.Spec.RenewTime != nil && lease.Spec.LeaseDurationSeconds != nil {
expiry := lease.Spec.RenewTime.Time.Add(time.Duration(*lease.Spec.LeaseDurationSeconds) * time.Second)
if time.Now().After(expiry) {
holder = ""
}
}
return holder, transitions, nil
}
//export K8sLeaseInit
func K8sLeaseInit(errMsg **C.char) C.int {
if err := ensureClientInitialized(); err != nil {
*errMsg = C.CString(err.Error())
return -1
}
return 0
}
//export K8sLeaseRunElection
func K8sLeaseRunElection(
ns, leaseName, identity *C.char,
leaseDurationSec, renewDeadlineSec, retryPeriodSec C.int,
errMsg **C.char,
) C.int {
nsStr := C.GoString(ns)
ln := C.GoString(leaseName)
id := C.GoString(identity)
err := runElection(nsStr, ln, id,
int(leaseDurationSec), int(renewDeadlineSec), int(retryPeriodSec))
if err != nil {
*errMsg = C.CString(err.Error())
return -1
}
return 0
}
//export K8sLeaseWaitElected
func K8sLeaseWaitElected(
ns, leaseName *C.char,
timeoutSec C.int,
leaseTransitions *C.longlong,
errMsg **C.char,
) C.int {
key := electionKey(C.GoString(ns), C.GoString(leaseName))
electionMutex.Lock()
state, exists := elections[key]
electionMutex.Unlock()
if !exists {
*errMsg = C.CString("no election running for " + key)
return -1
}
timeout := time.Duration(timeoutSec) * time.Second
// Wait for elected, lost, or timeout
select {
case <-state.elected:
*leaseTransitions = C.longlong(state.transitions)
return 0
case <-state.lost:
*errMsg = C.CString("election lost before becoming leader")
return -1
case <-time.After(timeout):
state.cancel()
<-state.lost
*errMsg = C.CString("election timed out after " + fmt.Sprintf("%d", int(timeoutSec)) + "s")
return -1
}
}
//export K8sLeaseWaitLost
func K8sLeaseWaitLost(
ns, leaseName *C.char,
errMsg **C.char,
) C.int {
key := electionKey(C.GoString(ns), C.GoString(leaseName))
electionMutex.Lock()
state, exists := elections[key]
electionMutex.Unlock()
if !exists {
// Already cleaned up by OnStoppedLeading — election is over.
return 0
}
<-state.lost
if state.err != nil {
*errMsg = C.CString(state.err.Error())
return -1
}
return 0
}
//export K8sLeaseCancelElection
func K8sLeaseCancelElection(
ns, leaseName *C.char,
errMsg **C.char,
) C.int {
key := electionKey(C.GoString(ns), C.GoString(leaseName))
electionMutex.Lock()
state, exists := elections[key]
electionMutex.Unlock()
if !exists {
// Idempotent — no error if no election
return 0
}
state.cancel()
return 0
}
//export K8sLeaseGetHolder
func K8sLeaseGetHolder(
ns, leaseName *C.char,
holderIdentity **C.char,
leaseTransitions *C.longlong,
errMsg **C.char,
) C.int {
nsStr := C.GoString(ns)
ln := C.GoString(leaseName)
holder, transitions, err := getHolder(nsStr, ln)
if err != nil {
if apierrors.IsNotFound(err) {
*holderIdentity = nil
*leaseTransitions = 0
return 1
}
errStr := err.Error()
*errMsg = C.CString(errStr)
return -1
}
if holder == "" {
*holderIdentity = nil
} else {
*holderIdentity = C.CString(holder)
}
*leaseTransitions = C.longlong(transitions)
return 0
}
//export K8sLeaseWatchHolder
func K8sLeaseWatchHolder(
ns, leaseName *C.char,
callbackCtx unsafe.Pointer,
callbackFunc C.holder_change_cb_t,
errMsg **C.char,
) C.int {
nsStr := C.GoString(ns)
ln := C.GoString(leaseName)
key := electionKey(nsStr, ln)
if callbackFunc == nil {
*errMsg = C.CString("callback function is nil")
return -1
}
if err := ensureClientInitialized(); err != nil {
*errMsg = C.CString(err.Error())
return -1
}
watchMutex.Lock()
if _, exists := watches[key]; exists {
watchMutex.Unlock()
*errMsg = C.CString("watch already running for " + key)
return -1
}
ctx, cancel := context.WithCancel(context.Background())
watches[key] = &watchState{cancel: cancel}
watchMutex.Unlock()
go func() {
defer func() {
watchMutex.Lock()
delete(watches, key)
watchMutex.Unlock()
}()
for {
select {
case <-ctx.Done():
return
default:
}
watcher, err := globalClient.CoordinationV1().Leases(nsStr).Watch(ctx, metav1.ListOptions{
FieldSelector: "metadata.name=" + ln,
})
if err != nil {
select {
case <-ctx.Done():
return
default:
time.Sleep(time.Second)
continue
}
}
for event := range watcher.ResultChan() {
select {
case <-ctx.Done():
watcher.Stop()
return
default:
}
if event.Type == watch.Modified || event.Type == watch.Added {
lease, ok := event.Object.(*coordinationv1.Lease)
if !ok {
continue
}
holder := ""
if lease.Spec.HolderIdentity != nil {
holder = *lease.Spec.HolderIdentity
}
transitions := int64(0)
if lease.Spec.LeaseTransitions != nil {
transitions = int64(*lease.Spec.LeaseTransitions)
}
var holderPtr *C.char
var holderSize C.size_t
if holder != "" {
holderPtr = C.CString(holder)
holderSize = C.size_t(len(holder))
}
C.call_holder_change_cb(callbackFunc, callbackCtx,
holderPtr, holderSize, C.int64_t(transitions))
if holderPtr != nil {
C.free(unsafe.Pointer(holderPtr))
}
}
}
// Watch channel closed — retry unless cancelled
select {
case <-ctx.Done():
return
default:
time.Sleep(time.Second)
}
}
}()
return 0
}
//export K8sLeaseCancelWatch
func K8sLeaseCancelWatch(
ns, leaseName *C.char,
errMsg **C.char,
) C.int {
key := electionKey(C.GoString(ns), C.GoString(leaseName))
watchMutex.Lock()
state, exists := watches[key]
watchMutex.Unlock()
if !exists {
// Idempotent
return 0
}
state.cancel()
return 0
}
func main() {}

View File

@ -1,293 +0,0 @@
package main
import (
"context"
"fmt"
"testing"
"time"
coordinationv1 "k8s.io/api/coordination/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/utils/ptr"
)
// swapClient replaces globalClient and returns the old one.
func swapClient(newClient kubernetes.Interface) kubernetes.Interface {
clientMutex.Lock()
defer clientMutex.Unlock()
old := globalClient
globalClient = newClient
return old
}
// TestGetHolderWithFakeClient tests getHolder using a fake K8s clientset.
func TestGetHolderWithFakeClient(t *testing.T) {
holderID := "node-1:8080"
transitions := int32(3)
lease := &coordinationv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-lease",
Namespace: "default",
},
Spec: coordinationv1.LeaseSpec{
HolderIdentity: &holderID,
LeaseTransitions: &transitions,
},
}
fakeClient := fake.NewSimpleClientset(lease)
old := swapClient(fakeClient)
defer swapClient(old)
holder, trans, err := getHolder("default", "test-lease")
if err != nil {
t.Fatalf("getHolder failed: %v", err)
}
if holder != holderID {
t.Errorf("expected holder %q, got %q", holderID, holder)
}
if trans != int64(transitions) {
t.Errorf("expected transitions %d, got %d", transitions, trans)
}
}
// TestGetHolderNotFound tests getHolder when the Lease does not exist.
func TestGetHolderNotFound(t *testing.T) {
fakeClient := fake.NewSimpleClientset()
old := swapClient(fakeClient)
defer swapClient(old)
_, _, err := getHolder("default", "nonexistent")
if err == nil {
t.Fatal("expected error for nonexistent lease, got nil")
}
}
// TestGetHolderEmptyIdentity tests getHolder when holder is nil.
func TestGetHolderEmptyIdentity(t *testing.T) {
lease := &coordinationv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: "empty-lease",
Namespace: "default",
},
Spec: coordinationv1.LeaseSpec{},
}
fakeClient := fake.NewSimpleClientset(lease)
old := swapClient(fakeClient)
defer swapClient(old)
holder, trans, err := getHolder("default", "empty-lease")
if err != nil {
t.Fatalf("getHolder failed: %v", err)
}
if holder != "" {
t.Errorf("expected empty holder, got %q", holder)
}
if trans != 0 {
t.Errorf("expected 0 transitions, got %d", trans)
}
}
// TestGetHolderReturnsEmptyForExpiredLease verifies that getHolder treats a
// lease whose renewTime + leaseDuration is in the past as having no holder.
// This is critical for failover: when a leader pod dies without releasing the
// lease, standbys must see an empty holder so the supervisor attempts
// acquisition instead of looping in standby.
func TestGetHolderReturnsEmptyForExpiredLease(t *testing.T) {
holderID := "dead-leader:8080"
leaseDuration := int32(5)
transitions := int32(2)
expiredRenewTime := metav1.NewMicroTime(time.Now().Add(-10 * time.Second))
lease := &coordinationv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: "expired-lease",
Namespace: "default",
},
Spec: coordinationv1.LeaseSpec{
HolderIdentity: &holderID,
LeaseDurationSeconds: &leaseDuration,
LeaseTransitions: &transitions,
RenewTime: &expiredRenewTime,
},
}
fakeClient := fake.NewSimpleClientset(lease)
old := swapClient(fakeClient)
defer swapClient(old)
holder, trans, err := getHolder("default", "expired-lease")
if err != nil {
t.Fatalf("getHolder failed: %v", err)
}
if holder != "" {
t.Errorf("expected empty holder for expired lease, got %q", holder)
}
// Transitions should still be reported even for expired leases.
if trans != int64(transitions) {
t.Errorf("expected transitions %d, got %d", transitions, trans)
}
}
// TestGetHolderReturnsHolderForActiveLease verifies that getHolder returns the
// holder identity when the lease is still active (renewTime + leaseDuration is
// in the future).
func TestGetHolderReturnsHolderForActiveLease(t *testing.T) {
holderID := "active-leader:8080"
leaseDuration := int32(15)
transitions := int32(1)
recentRenewTime := metav1.NewMicroTime(time.Now())
lease := &coordinationv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: "active-lease",
Namespace: "default",
},
Spec: coordinationv1.LeaseSpec{
HolderIdentity: &holderID,
LeaseDurationSeconds: &leaseDuration,
LeaseTransitions: &transitions,
RenewTime: &recentRenewTime,
},
}
fakeClient := fake.NewSimpleClientset(lease)
old := swapClient(fakeClient)
defer swapClient(old)
holder, trans, err := getHolder("default", "active-lease")
if err != nil {
t.Fatalf("getHolder failed: %v", err)
}
if holder != holderID {
t.Errorf("expected holder %q, got %q", holderID, holder)
}
if trans != int64(transitions) {
t.Errorf("expected transitions %d, got %d", transitions, trans)
}
}
// TestElectionKeyFormat tests the election key construction.
func TestElectionKeyFormat(t *testing.T) {
tests := []struct {
ns, name, want string
}{
{"default", "leader", "default/leader"},
{"kube-system", "my-lock", "kube-system/my-lock"},
{"", "bare", "/bare"},
}
for _, tc := range tests {
got := electionKey(tc.ns, tc.name)
if got != tc.want {
t.Errorf("electionKey(%q, %q) = %q, want %q", tc.ns, tc.name, got, tc.want)
}
}
}
// TestLeaseCRUDWithFakeClient tests basic Lease CRUD via the K8s API.
func TestLeaseCRUDWithFakeClient(t *testing.T) {
fakeClient := fake.NewSimpleClientset()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
holderID := "node-a:9090"
transitions := int32(0)
lease := &coordinationv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: "crud-test",
Namespace: "default",
},
Spec: coordinationv1.LeaseSpec{
HolderIdentity: &holderID,
LeaseTransitions: &transitions,
},
}
created, err := fakeClient.CoordinationV1().Leases("default").Create(ctx, lease, metav1.CreateOptions{})
if err != nil {
t.Fatalf("create lease failed: %v", err)
}
if *created.Spec.HolderIdentity != holderID {
t.Errorf("created holder = %q, want %q", *created.Spec.HolderIdentity, holderID)
}
newHolder := "node-b:9090"
newTransitions := int32(1)
created.Spec.HolderIdentity = &newHolder
created.Spec.LeaseTransitions = &newTransitions
updated, err := fakeClient.CoordinationV1().Leases("default").Update(ctx, created, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("update lease failed: %v", err)
}
if *updated.Spec.HolderIdentity != newHolder {
t.Errorf("updated holder = %q, want %q", *updated.Spec.HolderIdentity, newHolder)
}
if *updated.Spec.LeaseTransitions != newTransitions {
t.Errorf("updated transitions = %d, want %d", *updated.Spec.LeaseTransitions, newTransitions)
}
got, err := fakeClient.CoordinationV1().Leases("default").Get(ctx, "crud-test", metav1.GetOptions{})
if err != nil {
t.Fatalf("get lease failed: %v", err)
}
if *got.Spec.HolderIdentity != newHolder {
t.Errorf("got holder = %q, want %q", *got.Spec.HolderIdentity, newHolder)
}
err = fakeClient.CoordinationV1().Leases("default").Delete(ctx, "crud-test", metav1.DeleteOptions{})
if err != nil {
t.Fatalf("delete lease failed: %v", err)
}
_, err = fakeClient.CoordinationV1().Leases("default").Get(ctx, "crud-test", metav1.GetOptions{})
if err == nil {
t.Fatal("expected error after delete, got nil")
}
}
// TestConcurrentGetHolder tests concurrent calls to getHolder.
func TestConcurrentGetHolder(t *testing.T) {
holderID := "concurrent-node:8080"
lease := &coordinationv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: "concurrent-lease",
Namespace: "default",
},
Spec: coordinationv1.LeaseSpec{
HolderIdentity: &holderID,
LeaseTransitions: ptr.To(int32(5)),
},
}
fakeClient := fake.NewSimpleClientset(lease)
old := swapClient(fakeClient)
defer swapClient(old)
const n = 10
errCh := make(chan error, n)
for i := 0; i < n; i++ {
go func() {
holder, trans, err := getHolder("default", "concurrent-lease")
if err != nil {
errCh <- err
return
}
if holder != holderID {
errCh <- fmt.Errorf("expected holder %q, got %q", holderID, holder)
return
}
if trans != 5 {
errCh <- fmt.Errorf("expected transitions 5, got %d", trans)
return
}
errCh <- nil
}()
}
for i := 0; i < n; i++ {
if err := <-errCh; err != nil {
t.Fatalf("concurrent getHolder failed: %v", err)
}
}
}

View File

@ -1,64 +1,13 @@
find_package(yaml-cpp REQUIRED)
find_package(asio QUIET)
if(asio_FOUND)
message(STATUS "Found ASIO via find_package")
set(ASIO_INCLUDE_DIR ${asio_INCLUDE_DIR})
else()
find_path(ASIO_INCLUDE_DIR
NAMES asio.hpp
PATHS
/usr/local/include
/usr/include
${CMAKE_INSTALL_PREFIX}/include
DOC "Path to ASIO headers"
)
if(NOT ASIO_INCLUDE_DIR)
message(FATAL_ERROR "ASIO not found. Please install ASIO or set ASIO_INCLUDE_DIR manually.")
endif()
message(STATUS "Found ASIO at: ${ASIO_INCLUDE_DIR}")
endif()
set(MOONCAKE_COMMON_SOURCES
default_config.cpp
environ.cpp
)
add_library(asio_shared SHARED asio_impl.cpp)
target_compile_definitions(asio_shared
PUBLIC
ASIO_SEPARATE_COMPILATION
ASIO_DYN_LINK
)
target_include_directories(asio_shared
PUBLIC
${ASIO_INCLUDE_DIR}
)
set_target_properties(asio_shared PROPERTIES
POSITION_INDEPENDENT_CODE ON
INSTALL_RPATH "$ORIGIN"
BUILD_WITH_INSTALL_RPATH TRUE
OUTPUT_NAME "asio"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/mooncake-common"
)
target_link_libraries(asio_shared PUBLIC pthread)
add_library(mooncake_common
${MOONCAKE_COMMON_SOURCES}
)
target_include_directories(mooncake_common PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(mooncake_common PUBLIC
yaml-cpp
jsoncpp
@ -67,5 +16,3 @@ target_link_libraries(mooncake_common PUBLIC
if (BUILD_SHARED_LIBS)
install(TARGETS mooncake_common DESTINATION lib)
endif()
install(TARGETS asio_shared DESTINATION lib)

View File

@ -1563,8 +1563,7 @@ PYBIND11_MODULE(store, m) {
const std::string &rdma_devices = "",
const std::string &master_server_addr = "127.0.0.1:50051",
const py::object &engine = py::none(),
bool enable_ssd_offload = false,
const std::string &ssd_offload_path = "") {
bool enable_ssd_offload = false) {
auto real_client = self.init_real_client();
std::shared_ptr<mooncake::TransferEngine> transfer_engine =
nullptr;
@ -1575,15 +1574,14 @@ PYBIND11_MODULE(store, m) {
return real_client->setup_real(
local_hostname, metadata_server, global_segment_size,
local_buffer_size, protocol, rdma_devices,
master_server_addr, transfer_engine, "", enable_ssd_offload,
ssd_offload_path);
master_server_addr, transfer_engine, "",
enable_ssd_offload);
},
py::arg("local_hostname"), py::arg("metadata_server"),
py::arg("global_segment_size"), py::arg("local_buffer_size"),
py::arg("protocol"), py::arg("rdma_devices"),
py::arg("master_server_addr"), py::arg("engine") = py::none(),
py::arg("enable_ssd_offload") = false,
py::arg("ssd_offload_path") = "")
py::arg("enable_ssd_offload") = false)
.def(
"setup",
[](MooncakeStorePyWrapper &self, const py::dict &config_dict) {
@ -1612,9 +1610,7 @@ PYBIND11_MODULE(store, m) {
" rdma_devices: RDMA device list.\n"
" master_server_addr: Master server address.\n"
" ipc_socket_path: IPC socket path.\n"
" enable_ssd_offload: Enable SSD offload (default false).\n"
" ssd_offload_path: SSD storage directory path (overrides env "
"var).")
" enable_ssd_offload: Enable SSD offload (default false).")
.def(
"setup_dummy",
[](MooncakeStorePyWrapper &self, size_t mem_pool_size,
@ -2271,20 +2267,6 @@ PYBIND11_MODULE(store, m) {
return self.store_->batch_get_replica_desc(keys);
},
py::arg("keys"))
.def(
"batch_replica_clear",
[](MooncakeStorePyWrapper &self,
const std::vector<std::string> &keys,
const std::string &segment_name) {
if (!self.is_client_initialized()) {
LOG(ERROR) << "Client is not initialized";
return std::vector<std::string>{};
}
py::gil_scoped_release release;
return self.store_->batch_replica_clear(keys, segment_name);
},
py::arg("keys"), py::arg("segment_name") = "",
"Clear replicas for the given keys. Requires lease to be expired.")
.def(
"create_copy_task",
[](MooncakeStorePyWrapper &self, const std::string &key,

View File

@ -33,7 +33,7 @@ fi
EXT_LDFLAGS="-L$BUILD_DIR/mooncake-transfer-engine/src"
EXT_LDFLAGS+=" -L$BUILD_DIR/mooncake-transfer-engine/src/common/base"
EXT_LDFLAGS+=" -L$BUILD_DIR/mooncake-common"
EXT_LDFLAGS+=" -L$BUILD_DIR/mooncake-asio"
EXT_LDFLAGS+=" -ltransfer_engine -lbase -lasio -lstdc++ -lnuma -lglog -libverbs -ljsoncpp"
if [ -d "/usr/local/cuda/lib64/stubs" ]; then

View File

@ -53,7 +53,7 @@ __global__ struct Task {
size_t tensorSize; // In bytes
int64_t broadcastRoot;
int bufferOffset;
uint64_t submitSequence = 0;
uint64_t submit_sequence = 0;
BatchID batchID;
void* transferGroupMeta;
};

View File

@ -3,7 +3,6 @@
#include <memory>
#include <thread>
#include <mooncake_worker.cuh>
#include <ATen/cuda/CUDAGraphsUtils.cuh>
#include "pg_utils.h"
@ -46,68 +45,36 @@ class MooncakeWorkCuda : public ::c10d::Work {
bool wait(std::chrono::milliseconds timeout) override {
// Wait until the task has been submitted to TransferEngine:
// This tries to ensure that the CUDA kernels required for the transfer
// This ensures that the CUDA kernels required for the transfer
// have been launched by the time `waitUntilTasksSubmitted` returns.
//
// Why is this needed? PyTorch documentation implies that collective
// operations should be enqueued when `wait()` returns. In practice, we
// found that violating this causes hangs.
//
// Our current hypothesis for the hang is: PyTorch assumes the kernels
// needed for the transfer are already launched when `wait` returns
// true. It may then launch subsequent operations after the collective
// (e.g., `.cpu()`). Such operations may acquire a process-wide lock in
// the CUDA runtime. Also, they may rely on the data produced by the
// collective, thus causing a synchronization on enq_stream. However,
// holding that runtime lock prevents cudaMemcpy(Async) in TE/TENT from
// launching. This means the transfer can't finish, and enq_stream won't
// complete. Thus, a deadlock occurs.
// (In practice, we found that replacing all cudaMemcpyAsync in TENT
// with cuMemcpyAsync actually alleviates this, which further suggests a
// deadlock in the CUDA runtime. However, that change is too invasive
// for TE/TENT, so we do not adopt it here.)
//
// Strictly speaking, the wait is needed for another reason: The current
// stream will be blocked on the event below. Any subsequent work on
// `current_stream` will wait on that event, which effectively waits for
// the task to be done. Therefore, we must ensure all kernels needed for
// the transfer task are launched BEFORE blocking the current stream, in
// case TE/TENT use `current_stream` to launch those kernels (though it
// is rare).
// Although this is not required for CPU-only transports such as
// RdmaTransport, we keep this behavior to avoid invasive changes
// to TE/TENT.
//
// Please note that this logic relies on the assumption that TE/TENT
// will launch all CUDA operations in `submitTransfer`.
// Unfortunately, TcpTransport in TE and TENT currently violates this
// assumption (cudaMemcpy(Async) may be called later from a callback),
// which can cause hangs in PG when a CUDA operation such as
// `x.cpu().item()` follows the collective. For TE's TcpTransport, the
// use of cudaMemcpy on the default stream may also contribute to the
// hang.
// assumption (cudaMemcpy(Async) may be called from a callback), which
// can cause hangs in PG when a CUDA operation such as `x.cpu().item()`
// follows the collective. For TE's TcpTransport, the use of cudaMemcpy
// on the default stream may also contribute to the hang.
//
// Besides, for CPU-only transports (like RdmaTransport),
// waitUntilTasksSubmitted is totally unnecessary, but we keep it for
// uniform behavior to avoid invasive changes to TE/TENT.
bool submitted = true;
if (at::cuda::currentStreamCaptureStatus() ==
c10::cuda::CaptureStatus::None) {
// Normal execution: block until tasks are submitted.
submitted =
worker_->waitUntilTasksSubmitted(submitted_tasks_, timeout);
} else {
// During CUDA graph capture, kernels are recorded but not actually
// executed. The enqueueTaskKernel would never run, so
// waitUntilTasksSubmitted would hang because the CPU worker thread
// never sees task.active == true.
//
// Note that this also means NvlinkTransport (and TcpTransport too,
// of course) won't work with CUDA Graphs: Kernels launched inside
// TE/TENT can't be captured by the graph, and during replay they
// are not ordered with the graph execution. This may trigger the
// same deadlock described above.
}
// This wait is primarily needed for two reasons:
// 1. PyTorch documents that `wait` should ensure the operation is
// issued, though not necessarily completed, for CUDA work.
// In practice, this means the transfer kernel must at least be
// launched; otherwise, a hang may occur.
// 2. The current stream is blocked on the event below. Any subsequent
// work on `current_stream` will wait on that event, which
// effectively waits for the task to be done. Therefore, we must
// not block until all kernels needed for the transfer task have
// been launched, in case TE/TENT use `current_stream` to
// launch those kernels (though rare).
auto submitted =
worker_->waitUntilTasksSubmitted(submitted_tasks_, timeout);
if (!submitted) return false;
// Once all tasks have been submitted, use the event to synchronize
// Once all tasks have been submitted, create an event to synchronize
// the current stream and the enqueue stream, but do not wait on this
// event.
//
@ -134,18 +101,6 @@ class MooncakeBarrierWorkCuda : public MooncakeWorkCuda {
using MooncakeWorkCuda::MooncakeWorkCuda;
bool wait(std::chrono::milliseconds timeout) override {
// Skip host-side synchronization during CUDA graph capture.
// cudaEventSynchronize is not permitted while a stream is capturing.
if (at::cuda::currentStreamCaptureStatus() !=
c10::cuda::CaptureStatus::None) {
// We still need stream-level synchronization so that subsequent
// operations on the capture stream are ordered after the barrier
// task on the enqueue stream.
auto current_stream = at::cuda::getCurrentCUDAStream();
event_->block(current_stream);
return true;
}
if (timeout == kNoTimeout) {
event_->synchronize();
return true;
@ -168,7 +123,7 @@ __global__ void enqueueTaskKernel(c10d::OpType opType, size_t tensorSize,
tasks[taskId].tensorSize = tensorSize;
tasks[taskId].broadcastRoot = broadcastRoot;
tasks[taskId].bufferOffset = bufferOffset;
tasks[taskId].submitSequence = submitSequence;
tasks[taskId].submit_sequence = submitSequence;
tasks[taskId].transferGroupMeta = meta;
// Publish task metadata before notifying the host worker thread.
@ -401,7 +356,7 @@ MooncakeWorker::MooncakeWorker(int cuda_device_index)
}
for (size_t i = 0; i < kNumTasks_; ++i) {
tasks_[i].active = false;
tasks_[i].submitSequence = 0;
tasks_[i].submit_sequence = 0;
submitted_task_sequence_[i].store(0, std::memory_order_relaxed);
}
}

View File

@ -91,7 +91,7 @@ void MooncakeWorker::startWorker() {
group->rank != task.broadcastRoot) ||
task.opType == c10d::OpType::BARRIER;
if (task_status[i].load(std::memory_order_acquire) == IDLE) {
const auto submit_sequence = task.submitSequence;
const auto submit_sequence = task.submit_sequence;
if (skipTransfer) {
submitted_task_sequence_[i].store(
submit_sequence, std::memory_order_release);

View File

@ -39,7 +39,7 @@ CGO_LDFLAGS="-L${BUILD_DIR}/mooncake-store/src"
CGO_LDFLAGS+=" -L${BUILD_DIR}/mooncake-store/src/cachelib_memory_allocator"
CGO_LDFLAGS+=" -L${BUILD_DIR}/mooncake-transfer-engine/src"
CGO_LDFLAGS+=" -L${BUILD_DIR}/mooncake-transfer-engine/src/common/base"
CGO_LDFLAGS+=" -L${BUILD_DIR}/mooncake-common"
CGO_LDFLAGS+=" -L${BUILD_DIR}/mooncake-asio"
CGO_LDFLAGS+=" -lmooncake_store -lcachelib_memory_allocator -ltransfer_engine -lbase -lasio"
CGO_LDFLAGS+=" -lstdc++ -lnuma -lglog -lgflags -libverbs -ljsoncpp -lzstd -lcurl"

View File

@ -1,15 +1,10 @@
#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <iomanip>
#include <mutex>
#include <optional>
#include <sstream>
#include <thread>
#include <unordered_set>
#include <vector>
#include <ylt/metric/counter.hpp>
#include <ylt/metric/histogram.hpp>
@ -51,56 +46,6 @@ const inline std::map<std::string, std::string> merge_labels(
return merged_labels;
}
inline std::string format_metric_rate(double value, const char* suffix) {
const double KB = 1024.0;
const double MB = KB * 1024.0;
const double GB = MB * 1024.0;
const double TB = GB * 1024.0;
std::ostringstream oss;
oss << std::fixed << std::setprecision(2);
if (value >= TB) {
oss << value / TB << " T" << suffix;
} else if (value >= GB) {
oss << value / GB << " G" << suffix;
} else if (value >= MB) {
oss << value / MB << " M" << suffix;
} else if (value >= KB) {
oss << value / KB << " K" << suffix;
} else {
oss << value << " " << suffix;
}
return oss.str();
}
inline std::string format_metric_bandwidth(uint64_t total_bytes,
double elapsed_seconds) {
return format_metric_rate(total_bytes / elapsed_seconds, "B/s");
}
inline uint64_t elapsed_us_since(
std::chrono::steady_clock::time_point start_time) {
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start_time)
.count());
}
template <typename Result, typename Operation, typename SuccessFn,
typename ObserveFn>
Result execute_timed_operation(Operation&& operation, SuccessFn&& success_fn,
ObserveFn&& observe_fn) {
const auto start_time = std::chrono::steady_clock::now();
Result result = std::forward<Operation>(operation)();
if (std::forward<SuccessFn>(success_fn)(result)) {
std::forward<ObserveFn>(observe_fn)(elapsed_us_since(start_time),
result);
}
return result;
}
enum class TransferOperationKind { kRead, kWrite };
struct TransferMetric {
TransferMetric(std::map<std::string, std::string> labels = {})
: total_read_bytes("mooncake_transfer_read_bytes", "Total bytes read",
@ -116,8 +61,7 @@ struct TransferMetric {
get_latency_us("mooncake_transfer_get_latency",
"Get transfer latency (us)", kLatencyBucket, labels),
put_latency_us("mooncake_transfer_put_latency",
"Put transfer latency (us)", kLatencyBucket, labels),
start_time_(std::chrono::steady_clock::now()) {}
"Put transfer latency (us)", kLatencyBucket, labels) {}
ylt::metric::counter_t total_read_bytes;
ylt::metric::counter_t total_write_bytes;
@ -135,7 +79,7 @@ struct TransferMetric {
put_latency_us.serialize(str);
}
std::string summary_metrics(bool include_bandwidth = true) {
std::string summary_metrics() {
std::stringstream ss;
ss << "=== Transfer Metrics Summary ===\n";
@ -144,14 +88,6 @@ struct TransferMetric {
auto write_bytes = total_write_bytes.value();
ss << "Total Read: " << byte_size_to_string(read_bytes) << "\n";
ss << "Total Write: " << byte_size_to_string(write_bytes) << "\n";
if (include_bandwidth) {
ss << "Average Read Throughput: "
<< format_metric_bandwidth(read_bytes, elapsed_seconds())
<< "\n";
ss << "Average Write Throughput: "
<< format_metric_bandwidth(write_bytes, elapsed_seconds())
<< "\n";
}
// Latency summaries
ss << "\n=== Latency Summary (microseconds) ===\n";
@ -166,14 +102,6 @@ struct TransferMetric {
}
private:
std::chrono::steady_clock::time_point start_time_;
double elapsed_seconds() const {
const auto elapsed = std::chrono::duration<double>(
std::chrono::steady_clock::now() - start_time_);
return std::max(elapsed.count(), 1e-9);
}
std::string format_latency_summary(ylt::metric::histogram_t& hist) {
// Access the internal sum and bucket counts
auto sum_ptr =
@ -346,157 +274,6 @@ struct MasterClientMetric {
}
};
struct TransferOperationMetric {
std::array<std::string, 1> op_names = {"op_name"};
explicit TransferOperationMetric(
std::map<std::string, std::string> labels = {})
: read_op_count("mooncake_transfer_read_operation_count",
"Total read operations by interface type", labels,
op_names),
read_op_bytes("mooncake_transfer_read_operation_bytes",
"Total read bytes by interface type", labels, op_names),
read_op_latency_us("mooncake_transfer_read_operation_latency",
"Read operation latency by interface type (us)",
kLatencyBucket, labels, op_names),
write_op_count("mooncake_transfer_write_operation_count",
"Total write operations by interface type", labels,
op_names),
write_op_bytes("mooncake_transfer_write_operation_bytes",
"Total write bytes by interface type", labels,
op_names),
write_op_latency_us("mooncake_transfer_write_operation_latency",
"Write operation latency by interface type (us)",
kLatencyBucket, labels, op_names) {}
ylt::metric::hybrid_counter_1t read_op_count;
ylt::metric::hybrid_counter_1t read_op_bytes;
ylt::metric::hybrid_histogram_1t read_op_latency_us;
ylt::metric::hybrid_counter_1t write_op_count;
ylt::metric::hybrid_counter_1t write_op_bytes;
ylt::metric::hybrid_histogram_1t write_op_latency_us;
void Observe(TransferOperationKind kind, const std::string& op_name,
uint64_t bytes, uint64_t latency_us) {
const std::array<std::string, 1> label = {op_name};
{
std::lock_guard<std::mutex> lock(observed_ops_mutex_);
if (kind == TransferOperationKind::kRead) {
observed_read_ops_.insert(op_name);
} else {
observed_write_ops_.insert(op_name);
}
}
if (kind == TransferOperationKind::kRead) {
read_op_count.inc(label);
read_op_bytes.inc(label, bytes);
read_op_latency_us.observe(label, latency_us);
} else {
write_op_count.inc(label);
write_op_bytes.inc(label, bytes);
write_op_latency_us.observe(label, latency_us);
}
}
void serialize(std::string& str) {
read_op_count.serialize(str);
read_op_bytes.serialize(str);
read_op_latency_us.serialize(str);
write_op_count.serialize(str);
write_op_bytes.serialize(str);
write_op_latency_us.serialize(str);
}
std::string summary_metrics() {
std::stringstream ss;
ss << "=== Interface Operation Metrics Summary ===\n";
ss << format_operation_group_summary(
"Read Interfaces", snapshot_operations(observed_read_ops_),
read_op_count, read_op_bytes, read_op_latency_us)
<< "\n";
ss << format_operation_group_summary(
"Write Interfaces", snapshot_operations(observed_write_ops_),
write_op_count, write_op_bytes, write_op_latency_us);
return ss.str();
}
private:
std::mutex observed_ops_mutex_;
std::unordered_set<std::string> observed_read_ops_;
std::unordered_set<std::string> observed_write_ops_;
std::vector<std::string> snapshot_operations(
const std::unordered_set<std::string>& source) {
std::lock_guard<std::mutex> lock(observed_ops_mutex_);
std::vector<std::string> ops(source.begin(), source.end());
std::sort(ops.begin(), ops.end());
return ops;
}
std::string format_operation_group_summary(
const std::string& group_name, const std::vector<std::string>& ops,
ylt::metric::hybrid_counter_1t& op_count,
ylt::metric::hybrid_counter_1t& op_bytes,
ylt::metric::hybrid_histogram_1t& op_latency_us) {
std::stringstream ss;
ss << group_name << ":\n";
if (ops.empty()) {
ss << "No data";
return ss.str();
}
auto bucket_counts = op_latency_us.get_bucket_counts();
bool found_any = false;
for (const auto& op_name : ops) {
const std::array<std::string, 1> label = {op_name};
const int64_t total_count = op_count.value(label);
if (total_count == 0) {
continue;
}
found_any = true;
ss << op_name << ": count=" << total_count << ", bytes="
<< byte_size_to_string(
static_cast<uint64_t>(op_bytes.value(label)));
int64_t p95_target = (total_count * 95) / 100;
int64_t cumulative = 0;
double p95_bucket = 0;
for (size_t i = 0;
i < bucket_counts.size() && i < kLatencyBucket.size(); ++i) {
cumulative += bucket_counts[i]->value(label);
if (cumulative >= p95_target && p95_bucket == 0) {
p95_bucket = kLatencyBucket[i];
break;
}
}
if (p95_bucket > 0) {
ss << ", p95<" << p95_bucket << "μs";
}
double max_bucket = 0;
for (size_t i = bucket_counts.size(); i > 0; --i) {
const size_t idx = i - 1;
if (idx < kLatencyBucket.size() &&
bucket_counts[idx]->value(label) > 0) {
max_bucket = kLatencyBucket[idx];
break;
}
}
if (max_bucket > 0) {
ss << ", max<" << max_bucket << "μs";
}
ss << "\n";
}
if (!found_any) {
ss << "No data";
}
return ss.str();
}
};
// SSD latency bucket: microseconds, tuned for SSD/network storage
// Range: 50us (high-end NVMe) to 30s (3fs/nfs large object batch writes)
inline const std::vector<double> kSsdLatencyBucket = {
@ -647,6 +424,7 @@ struct SsdMetric {
std::stringstream ss;
ss << std::fixed << std::setprecision(1);
ss << "count=" << count;
// rates order matches quantiles {0.5, 0.9, 0.99}
if (rates.size() >= 1) ss << ", p50=" << rates[0] << "us";
if (rates.size() >= 2) ss << ", p90=" << rates[1] << "us";
if (rates.size() >= 3) ss << ", p99=" << rates[2] << "us";
@ -660,7 +438,6 @@ struct SsdMetric {
struct ClientMetric {
TransferMetric transfer_metric;
MasterClientMetric master_client_metric;
TransferOperationMetric transfer_operation_metric;
SsdMetric ssd_metric;
/**
@ -675,44 +452,25 @@ struct ClientMetric {
* (default: 0, 0 = collect but don't report)
*/
static std::unique_ptr<ClientMetric> Create(
const std::map<std::string, std::string>& labels = {},
bool master_rpc_metrics_enabled = true);
void ObserveTransferOperation(TransferOperationKind kind,
const std::string& op_name, uint64_t bytes,
uint64_t latency_us) {
transfer_operation_metric.Observe(kind, op_name, bytes, latency_us);
}
const std::map<std::string, std::string>& labels = {});
void serialize(std::string& str);
std::string summary_metrics();
uint64_t GetReportingInterval() const { return metrics_interval_seconds_; }
explicit ClientMetric(uint64_t interval_seconds = 0,
const std::map<std::string, std::string>& labels = {},
bool bandwidth_reporting_enabled = true,
bool master_rpc_metrics_enabled = true);
explicit ClientMetric(
uint64_t interval_seconds = 0,
const std::map<std::string, std::string>& labels = {});
~ClientMetric();
private:
struct TransferSnapshot {
uint64_t read_bytes;
uint64_t write_bytes;
std::chrono::steady_clock::time_point timestamp;
};
// Metrics reporting thread management
std::jthread metrics_reporting_thread_;
std::atomic<bool> should_stop_metrics_thread_{false};
uint64_t metrics_interval_seconds_{0};
bool bandwidth_reporting_enabled_{true};
bool master_rpc_metrics_enabled_{true};
std::mutex snapshot_mutex_;
std::optional<TransferSnapshot> last_report_snapshot_;
void StartMetricsReportingThread();
void StopMetricsReportingThread();
std::string BuildBandwidthReport();
};
}; // namespace mooncake
}; // namespace mooncake

View File

@ -24,7 +24,6 @@
#include "master_metric_manager.h"
#include "count_min_sketch.h"
#include "local_hot_cache.h"
#include "pinned_buffer_pool.h"
namespace mooncake {
@ -62,8 +61,6 @@ class Client {
public:
~Client();
const UUID& getClientId() const { return client_id_; }
/**
* @brief Creates and initializes a new Client instance
* @param local_hostname Local host address (IP:Port)
@ -447,15 +444,6 @@ class Client {
return master_client_.CalcCacheStats();
}
void ObserveTransferOperation(TransferOperationKind kind,
const std::string& op_name, uint64_t bytes,
uint64_t latency_us) {
if (metrics_ != nullptr) {
metrics_->ObserveTransferOperation(kind, op_name, bytes,
latency_us);
}
}
// For Prometheus-style metrics
tl::expected<std::string, ErrorCode> SerializeMetrics() {
if (metrics_ == nullptr) {
@ -672,9 +660,6 @@ class Client {
const std::string protocol_;
// Client persistent thread pool for async operations
// Pinned host memory pool for GPU D2H staging (must outlive
// write_thread_pool_)
std::unique_ptr<PinnedBufferPool> pinned_buffer_pool_;
ThreadPool write_thread_pool_;
std::shared_ptr<StorageBackend> storage_backend_;

View File

@ -7,7 +7,6 @@
#include "pyclient.h"
#include "real_client.h"
#include "shm_helper.h"
#include "client_metric.h"
#include <memory>
namespace mooncake {
@ -26,8 +25,7 @@ class DummyClient : public PyClient {
const std::string &master_server_addr,
const std::shared_ptr<TransferEngine> &transfer_engine,
const std::string &ipc_socket_path,
bool enable_ssd_offload = false,
const std::string &ssd_offload_path = "") {
bool enable_ssd_offload = false) {
// Dummy client does not support real setup
return -1;
};
@ -151,12 +149,6 @@ class DummyClient : public PyClient {
batch_get_replica_desc(const std::vector<std::string> &keys);
std::vector<Replica::Descriptor> get_replica_desc(const std::string &key);
std::vector<std::string> batch_replica_clear(
const std::vector<std::string> &keys,
const std::string &segment_name = "") override {
return {};
}
int tearDownAll();
int health_check() override;
@ -204,22 +196,6 @@ class DummyClient : public PyClient {
[[nodiscard]] std::vector<tl::expected<ResultType, ErrorCode>>
invoke_batch_rpc(size_t input_size, Args &&...args);
template <auto ServiceMethod, typename... Args>
int invoke_observed_void_rpc(TransferOperationKind kind,
const char *op_name, size_t bytes, bool batch,
Args &&...args) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() {
return invoke_rpc<ServiceMethod, void>(
std::forward<Args>(args)...);
},
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
ObserveTransferMetric(kind, op_name, bytes, latency_us, batch);
});
return to_py_ret(result);
}
/**
* @brief Accessor for the coro_rpc_client pool. Since coro_rpc_client
* pool cannot reconnect to a different address, a new coro_rpc_client
@ -278,11 +254,6 @@ class DummyClient : public PyClient {
// Ascend physical device id for dummy-real RPC to real, set in setup_dummy
int32_t device_id_ = 0;
std::unique_ptr<ClientMetric> metrics_;
void ObserveTransferMetric(TransferOperationKind kind, const char *op_name,
size_t bytes, uint64_t latency_us, bool batch);
};
} // namespace mooncake

View File

@ -3,7 +3,6 @@
#include "client_service.h"
#include "client_buffer.hpp"
#include "storage_backend.h"
#include "pinned_buffer_pool.h"
namespace mooncake {
@ -106,8 +105,6 @@ class FileStorage {
std::shared_ptr<Client> client_;
SsdMetric* ssd_metric_{nullptr};
std::string local_rpc_addr_;
// Pinned host memory pool for GPU D2H staging in OffloadObjects
std::unique_ptr<PinnedBufferPool> pinned_buffer_pool_;
std::shared_ptr<StorageBackendInterface> storage_backend_;
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator_;
mutable Mutex client_buffer_mutex_;

View File

@ -1,76 +0,0 @@
#pragma once
#include "cuda_alike.h"
#if defined(USE_ASCEND) || defined(USE_ASCEND_DIRECT) || defined(USE_UBSHMEM)
#include <acl/acl_rt.h>
#endif
#include <cstddef>
#include <glog/logging.h>
namespace mooncake {
namespace gpu_staging {
// Detect whether ptr resides in accelerator device memory.
// If so, writes the device ID to *out_device_id for subsequent SetDevice.
inline bool IsDevicePointer(const void* ptr, int* out_device_id) {
#if defined(USE_CUDA) || defined(USE_MUSA) || defined(USE_MACA)
cudaPointerAttributes attr{};
if (cudaPointerGetAttributes(&attr, ptr) == cudaSuccess &&
attr.type == cudaMemoryTypeDevice) {
if (out_device_id) *out_device_id = attr.device;
return true;
}
#elif defined(USE_HIP)
hipPointerAttribute_t attr{};
if (hipPointerGetAttributes(&attr, ptr) == hipSuccess &&
attr.type == hipMemoryTypeDevice) {
if (out_device_id) *out_device_id = attr.device;
return true;
}
#elif defined(USE_ASCEND) || defined(USE_ASCEND_DIRECT) || defined(USE_UBSHMEM)
aclrtPtrAttributes attr{};
if (aclrtPointerGetAttributes(const_cast<void*>(ptr), &attr) ==
ACL_SUCCESS &&
attr.location.type == ACL_MEM_LOCATION_TYPE_DEVICE) {
if (out_device_id) *out_device_id = static_cast<int>(attr.location.id);
return true;
}
#endif
(void)ptr;
(void)out_device_id;
return false;
}
// Copy device memory to host. Caller must have called SetDevice first.
inline bool CopyDeviceToHost(void* dst, const void* src, size_t size) {
#if defined(USE_CUDA) || defined(USE_MUSA) || defined(USE_MACA)
return cudaMemcpy(dst, src, size, cudaMemcpyDeviceToHost) == cudaSuccess;
#elif defined(USE_HIP)
return hipMemcpy(dst, src, size, hipMemcpyDeviceToHost) == hipSuccess;
#elif defined(USE_ASCEND) || defined(USE_ASCEND_DIRECT) || defined(USE_UBSHMEM)
return aclrtMemcpy(dst, size, src, size, ACL_MEMCPY_DEVICE_TO_HOST) ==
ACL_SUCCESS;
#else
(void)dst;
(void)src;
(void)size;
return false;
#endif
}
// Bind the calling thread to the given device context.
inline void SetDevice(int device_id) {
if (device_id < 0) return;
#if defined(USE_CUDA) || defined(USE_MUSA) || defined(USE_MACA)
cudaSetDevice(device_id);
#elif defined(USE_HIP)
hipSetDevice(device_id);
#elif defined(USE_ASCEND) || defined(USE_ASCEND_DIRECT) || defined(USE_UBSHMEM)
aclrtSetDevice(device_id);
#endif
}
} // namespace gpu_staging
} // namespace mooncake

View File

@ -81,10 +81,6 @@ struct MasterConfig {
std::string cxl_path;
size_t cxl_size;
bool enable_cxl = false;
// Offload-on-evict: defer LOCAL_DISK offload to eviction time
bool offload_on_evict = false;
bool offload_force_evict = false;
};
class MasterServiceSupervisorConfig {
@ -144,8 +140,6 @@ class MasterServiceSupervisorConfig {
std::string cxl_path = DEFAULT_CXL_PATH;
size_t cxl_size = DEFAULT_CXL_SIZE;
bool enable_cxl = false;
bool offload_on_evict = false;
bool offload_force_evict = false;
MasterServiceSupervisorConfig() = default;
// From MasterConfig
@ -161,8 +155,6 @@ class MasterServiceSupervisorConfig {
eviction_high_watermark_ratio = config.eviction_high_watermark_ratio;
client_live_ttl_sec = config.client_live_ttl_sec;
enable_offload = config.enable_offload;
offload_on_evict = config.offload_on_evict;
offload_force_evict = config.offload_force_evict;
rpc_port = static_cast<int>(config.rpc_port);
rpc_thread_num = static_cast<size_t>(config.rpc_thread_num);
@ -275,8 +267,6 @@ class WrappedMasterServiceConfig {
int64_t client_live_ttl_sec = DEFAULT_CLIENT_LIVE_TTL_SEC;
bool enable_ha = false;
bool enable_offload = false;
bool offload_on_evict = false;
bool offload_force_evict = false;
std::string ha_backend_type = "etcd";
std::string ha_backend_connstring;
std::string cluster_id = DEFAULT_CLUSTER_ID;
@ -332,8 +322,6 @@ class WrappedMasterServiceConfig {
client_live_ttl_sec = config.client_live_ttl_sec;
enable_ha = config.enable_ha;
enable_offload = config.enable_offload;
offload_on_evict = config.offload_on_evict;
offload_force_evict = config.offload_force_evict;
ha_backend_type = config.ha_backend_type;
ha_backend_connstring = config.ha_backend_connstring;
if (ha_backend_connstring.empty()) {
@ -412,8 +400,6 @@ class WrappedMasterServiceConfig {
enable_ha =
true; // This is used in HA mode, so enable_ha should be true
enable_offload = config.enable_offload;
offload_on_evict = config.offload_on_evict;
offload_force_evict = config.offload_force_evict;
ha_backend_type = config.ha_backend_type;
ha_backend_connstring = config.ha_backend_connstring;
if (ha_backend_connstring.empty()) {
@ -750,8 +736,6 @@ class MasterServiceConfig {
int64_t client_live_ttl_sec = DEFAULT_CLIENT_LIVE_TTL_SEC;
bool enable_ha = false;
bool enable_offload = false;
bool offload_on_evict = false;
bool offload_force_evict = false;
std::string ha_backend_type = "etcd";
std::string ha_backend_connstring;
std::string cluster_id = DEFAULT_CLUSTER_ID;
@ -803,8 +787,6 @@ class MasterServiceConfig {
client_live_ttl_sec = config.client_live_ttl_sec;
enable_ha = config.enable_ha;
enable_offload = config.enable_offload;
offload_on_evict = config.offload_on_evict;
offload_force_evict = config.offload_force_evict;
ha_backend_type = config.ha_backend_type;
ha_backend_connstring = config.ha_backend_connstring;
cluster_id = config.cluster_id;

View File

@ -562,14 +562,8 @@ class MasterService {
// fulfill evict ratio lowerbound.
void BatchEvict(double evict_ratio_target, double evict_ratio_lowerbound);
// Helper to get a snapshot of alive clients (under client_mutex_ shared
// lock)
std::unordered_set<UUID, boost::hash<UUID>> getAliveClientsSnapshot() const;
// Clear invalid handles in all shards
void ClearInvalidHandles();
void ClearInvalidHandles(
const std::unordered_set<UUID, boost::hash<UUID>>& alive_clients);
std::string FormatTimestamp(
const std::chrono::system_clock::time_point& tp);
@ -897,10 +891,7 @@ class MasterService {
}
// Helper to clean up stale handles pointing to unmounted segments
// or local_disk replicas whose owner client is no longer alive.
bool CleanupStaleHandles(
ObjectMetadata& metadata,
const std::unordered_set<UUID, boost::hash<UUID>>& alive_clients);
bool CleanupStaleHandles(ObjectMetadata& metadata);
// Helper: allocate replicas, create ObjectMetadata, insert into shard,
// and return descriptor list. Shared by PutStart and UpsertStart.
@ -971,21 +962,11 @@ class MasterService {
it_(shard_guard_->metadata.find(key)),
processing_it_(shard_guard_->processing_keys.find(key)),
replication_task_it_(shard_guard_->replication_tasks.find(key)) {
// Automatically clean up invalid handles (memory replicas only).
// Note: We only check memory replicas here to avoid lock order
// violation (client_mutex_ must be acquired before metadata shard).
// local_disk replicas are cleaned up by ClearInvalidHandles() in
// ClientMonitorFunc.
// Automatically clean up invalid handles
if (it_ != shard_guard_->metadata.end()) {
// Erase invalid memory replicas (those with unmounted
// segments). No client_mutex_ needed since we only check memory
// replicas.
it_->second.EraseReplicas([](const Replica& replica) {
return replica.has_invalid_mem_handle();
});
// If no valid replicas remain, delete the whole object.
if (!it_->second.IsValid()) {
if (service_->CleanupStaleHandles(it_->second)) {
this->Erase();
if (processing_it_ != shard_guard_->processing_keys.end()) {
this->EraseFromProcessing();
}
@ -1168,14 +1149,6 @@ class MasterService {
const bool enable_offload_;
// Offload-on-evict: defer disk offload to eviction time
// (config: offload_on_evict)
bool offload_on_evict_{false};
// Force-evict: allow evicting MEMORY replicas without disk offload when cap
// exceeded (config: offload_force_evict, only effective when
// offload_on_evict_=true)
bool offload_force_evict_{false};
const std::string ha_backend_type_;
const std::string ha_backend_connstring_;

View File

@ -1,138 +0,0 @@
#pragma once
#include <mutex>
#include <vector>
#include <cstdlib>
#include "cuda_alike.h"
// Ascend CANN is not covered by cuda_alike.h
#if defined(USE_ASCEND) || defined(USE_ASCEND_DIRECT) || defined(USE_UBSHMEM)
#include <acl/acl_rt.h>
#endif
namespace mooncake {
/**
* PinnedBufferPool: Thread-safe pool of reusable pinned host memory buffers.
*
* Platform pinned alloc APIs:
* CUDA / MUSA / MACA : cudaMallocHost (mapped via cuda_alike.h)
* HIP : hipHostMalloc (not mapped in hip.h, native API)
* Ascend : aclrtMallocHost
* Other : new char[] (pageable fallback)
*
* Pinned memory provides 10x~100x higher D2H bandwidth than pageable memory.
* Falls back to new char[] if pinned allocation fails.
*
* The pool enforces a maximum number of cached buffers (kDefaultMaxPoolSize).
* When the pool is full, Release() frees the buffer immediately instead of
* caching it, preventing unbounded pinned memory growth.
*/
class PinnedBufferPool {
public:
static constexpr size_t kDefaultMaxPoolSize = 32;
struct Buffer {
char* data = nullptr;
size_t capacity = 0;
bool is_pinned = false; // Selects correct free API in FreeBuffer
};
explicit PinnedBufferPool(size_t max_pool_size = kDefaultMaxPoolSize)
: max_pool_size_(max_pool_size) {}
~PinnedBufferPool() { Clear(); }
Buffer Acquire(size_t size) {
{
std::lock_guard<std::mutex> lk(mutex_);
for (size_t i = 0; i < pool_.size(); ++i) {
if (pool_[i].capacity >= size) {
Buffer buf = pool_[i];
// O(1) erase: swap with back then pop
pool_[i] = pool_.back();
pool_.pop_back();
return buf;
}
}
}
return AllocNew(size);
}
void Release(Buffer buf) {
std::lock_guard<std::mutex> lk(mutex_);
if (pool_.size() < max_pool_size_) {
pool_.push_back(buf);
} else {
// Pool full — free immediately to bound pinned memory usage
FreeBuffer(buf);
}
}
void Clear() {
std::lock_guard<std::mutex> lk(mutex_);
for (auto& buf : pool_) {
FreeBuffer(buf);
}
pool_.clear();
}
private:
static Buffer AllocNew(size_t size) {
Buffer buf;
buf.capacity = size;
buf.is_pinned = false;
#if defined(USE_CUDA) || defined(USE_MUSA) || defined(USE_MACA)
if (cudaMallocHost(reinterpret_cast<void**>(&buf.data), size) ==
cudaSuccess) {
buf.is_pinned = true;
} else {
buf.data = new char[size];
}
#elif defined(USE_HIP)
if (hipHostMalloc(reinterpret_cast<void**>(&buf.data), size, 0) ==
hipSuccess) {
buf.is_pinned = true;
} else {
buf.data = new char[size];
}
#elif defined(USE_ASCEND) || defined(USE_ASCEND_DIRECT) || defined(USE_UBSHMEM)
if (aclrtMallocHost(reinterpret_cast<void**>(&buf.data), size) ==
ACL_SUCCESS) {
buf.is_pinned = true;
} else {
buf.data = new char[size];
}
#else
buf.data = new char[size];
#endif
return buf;
}
static void FreeBuffer(Buffer& buf) {
if (!buf.data) return;
if (!buf.is_pinned) {
delete[] buf.data;
return;
}
#if defined(USE_CUDA) || defined(USE_MUSA) || defined(USE_MACA)
cudaFreeHost(buf.data);
#elif defined(USE_HIP)
hipHostFree(buf.data);
#elif defined(USE_ASCEND) || defined(USE_ASCEND_DIRECT) || defined(USE_UBSHMEM)
aclrtFreeHost(buf.data);
#else
delete[] buf.data;
#endif
}
const size_t max_pool_size_;
std::mutex mutex_;
std::vector<Buffer> pool_;
};
} // namespace mooncake

View File

@ -211,8 +211,8 @@ class PyClient {
const std::string &protocol, const std::string &rdma_devices,
const std::string &master_server_addr,
const std::shared_ptr<TransferEngine> &transfer_engine,
const std::string &ipc_socket_path, bool enable_ssd_offload = false,
const std::string &ssd_offload_path = "") = 0;
const std::string &ipc_socket_path,
bool enable_ssd_offload = false) = 0;
virtual int setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
const std::string &server_address,
@ -330,10 +330,6 @@ class PyClient {
virtual std::vector<Replica::Descriptor> get_replica_desc(
const std::string &key) = 0;
virtual std::vector<std::string> batch_replica_clear(
const std::vector<std::string> &keys,
const std::string &segment_name = "") = 0;
virtual int tearDownAll() = 0;
virtual int health_check() = 0;

View File

@ -79,8 +79,7 @@ class RealClient : public PyClient {
const std::string &master_server_addr = "127.0.0.1:50051",
const std::shared_ptr<TransferEngine> &transfer_engine = nullptr,
const std::string &ipc_socket_path = "",
bool enable_ssd_offload = false,
const std::string &ssd_offload_path = "");
bool enable_ssd_offload = false);
int setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
const std::string &server_address,
@ -484,8 +483,7 @@ class RealClient : public PyClient {
const std::string &master_server_addr = "127.0.0.1:50051",
const std::shared_ptr<TransferEngine> &transfer_engine = nullptr,
const std::string &ipc_socket_path = "", int local_rpc_port = 50052,
bool enable_ssd_offload = false, bool start_offload_rpc_server = false,
const std::string &ssd_offload_path = "");
bool enable_ssd_offload = false, bool start_offload_rpc_server = false);
// Overload that accepts a configuration dictionary
tl::expected<void, ErrorCode> setup_internal(const ConfigDict &config);
@ -639,10 +637,6 @@ class RealClient : public PyClient {
batch_get_replica_desc(const std::vector<std::string> &keys);
std::vector<Replica::Descriptor> get_replica_desc(const std::string &key);
std::vector<std::string> batch_replica_clear(
const std::vector<std::string> &keys,
const std::string &segment_name = "") override;
tl::expected<PingResponse, ErrorCode> ping(const UUID &client_id);
tl::expected<BatchGetOffloadObjectResponse, ErrorCode>

View File

@ -2,12 +2,9 @@
#include <glog/logging.h>
#include <boost/functional/hash.hpp>
#include <cstdint>
#include <memory>
#include <string>
#include <unordered_set>
#include <variant>
#include <vector>
#include <unordered_map>
@ -285,36 +282,6 @@ class Replica {
return false; // DiskReplicaData does not have handles
}
/**
* @brief Check if a local_disk replica's owner client is still alive.
* Used by CleanupStaleHandles to remove replicas belonging to expired
* clients. For non-local_disk replicas, always returns false.
* @param alive_clients Set of currently alive client IDs.
* @return true if this is a local_disk replica whose client is not alive.
*/
[[nodiscard]] bool has_stale_local_disk_client(
const std::unordered_set<UUID, boost::hash<UUID>>& alive_clients)
const {
auto client_id = get_local_disk_client_id();
if (client_id.has_value()) {
return alive_clients.find(client_id.value()) == alive_clients.end();
}
return false;
}
/**
* @brief Get the client_id for local_disk replicas.
* @return The client_id if this is a local_disk replica, std::nullopt
* otherwise.
*/
[[nodiscard]] std::optional<UUID> get_local_disk_client_id() const {
if (is_local_disk_replica()) {
const auto& disk_data = std::get<LocalDiskReplicaData>(data_);
return disk_data.client_id;
}
return std::nullopt;
}
[[nodiscard]] size_t get_memory_buffer_size() const {
if (is_memory_replica()) {
const auto& mem_data = std::get<MemoryReplicaData>(data_);

View File

@ -170,12 +170,6 @@ class ScopedSegmentAccess {
ErrorCode SetSegmentStatusByName(const std::string& segment_name,
SegmentStatus status);
/**
* @brief Remove the local disk segment entry for a client.
* Called when a client expires to clean up its local disk segment.
*/
void UnmountLocalDiskSegment(const UUID& client_id);
private:
SegmentManager* segment_manager_;
std::unique_lock<std::shared_mutex> lock_;

View File

@ -174,10 +174,7 @@ class TransferEngineOperationState : public OperationState {
public:
TransferEngineOperationState(TransferEngine& engine, BatchID batch_id,
size_t batch_size)
: engine_(engine),
batch_id_(batch_id),
batch_size_(batch_size),
start_ts_(getCurrentTimeInMilli()) {}
: engine_(engine), batch_id_(batch_id), batch_size_(batch_size) {}
~TransferEngineOperationState() { engine_.freeBatchID(batch_id_); }
@ -202,7 +199,6 @@ class TransferEngineOperationState : public OperationState {
TransferEngine& engine_;
BatchID batch_id_;
size_t batch_size_;
const int64_t start_ts_;
};
/**

View File

@ -202,62 +202,15 @@ if(STORE_USE_ETCD)
add_dependencies(mooncake_master build_etcd_wrapper)
endif()
target_compile_options(mooncake_master PRIVATE -Os)
target_link_options(mooncake_master PRIVATE -Os -s)
# Client server binary
add_executable(mooncake_client real_client_main.cpp)
# Client needs transfer_engine for data transfer operations
target_link_libraries(mooncake_client PRIVATE mooncake_store transfer_engine
asio_shared)
target_compile_options(mooncake_client PRIVATE -Os)
target_link_options(mooncake_client PRIVATE -Os -s)
# Optimize binary sizes only in Release mode
string(TOUPPER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_UPPER)
if (CMAKE_BUILD_TYPE_UPPER STREQUAL "RELEASE")
target_compile_options(mooncake_master PRIVATE -Os)
target_link_options(mooncake_master PRIVATE -Os -s)
target_compile_options(mooncake_client PRIVATE -Os)
target_link_options(mooncake_client PRIVATE -Os -s)
endif()
# GPU runtime library for D2H staging in PutToLocalFile / OffloadObjects.
# transfer_engine is PRIVATE-linked, so its CUDA/HIP/Ascend dependencies
# are not propagated; we must detect and link them independently.
#
# Auto-detect each toolkit regardless of global USE_CUDA/USE_HIP flags,
# because USE_CUDA may be OFF even when GPU pointers are present
# (e.g. WITH_NVIDIA_PEERMEM=ON uses nvidia-peermem for RDMA without cudart).
# Each detected toolkit gets both link libraries AND compile definitions,
# so that gpu_staging_utils.h / pinned_buffer_pool.h pick the correct backend.
#
# NOTE: mooncake_store is a static library (.a). External consumers (Go CGo,
# Python pybind) that link it must also link the GPU runtime (e.g. -lcudart).
# Go's build.sh already does this; CI workflows must do the same.
find_package(CUDAToolkit QUIET)
if(CUDAToolkit_FOUND)
message(STATUS "mooncake_store: CUDAToolkit detected, enabling D2H staging")
target_compile_definitions(mooncake_store PRIVATE USE_CUDA)
target_compile_definitions(mooncake_client PRIVATE USE_CUDA)
target_include_directories(mooncake_store PRIVATE ${CUDAToolkit_INCLUDE_DIRS})
target_include_directories(mooncake_client PRIVATE ${CUDAToolkit_INCLUDE_DIRS})
target_link_libraries(mooncake_store PRIVATE CUDA::cudart)
target_link_libraries(mooncake_client PRIVATE CUDA::cudart)
endif()
if(NOT CUDAToolkit_FOUND)
find_package(hip QUIET)
if(hip_FOUND)
message(STATUS "mooncake_store: HIP detected, enabling D2H staging")
target_compile_definitions(mooncake_store PRIVATE USE_HIP)
target_compile_definitions(mooncake_client PRIVATE USE_HIP)
target_link_libraries(mooncake_store PRIVATE hip::host)
target_link_libraries(mooncake_client PRIVATE hip::host)
endif()
endif()
if(USE_ASCEND OR USE_ASCEND_DIRECT OR USE_UBSHMEM)
target_include_directories(mooncake_store PRIVATE $ENV{ASCEND_PATH}/include)
target_link_libraries(mooncake_store PRIVATE ascendcl)
target_include_directories(mooncake_client PRIVATE $ENV{ASCEND_PATH}/include)
target_link_libraries(mooncake_client PRIVATE ascendcl)
endif()
install(TARGETS mooncake_master mooncake_client DESTINATION bin)

View File

@ -28,27 +28,6 @@ bool parseMetricsEnabled() {
value == "on" || value == "enable");
}
bool parseBoolEnv(const char* env_name, bool default_value) {
const char* env_value = std::getenv(env_name);
if (!env_value) {
return default_value;
}
std::string value = toLower(env_value);
if (value == "1" || value == "true" || value == "yes" || value == "on" ||
value == "enable") {
return true;
}
if (value == "0" || value == "false" || value == "no" || value == "off" ||
value == "disable") {
return false;
}
LOG(WARNING) << "Failed to parse " << env_name << ": " << env_value
<< ", fallback to default=" << default_value;
return default_value;
}
uint64_t parseMetricsInterval() {
const char* interval_env = std::getenv("MC_STORE_CLIENT_METRIC_INTERVAL");
if (!interval_env) {
@ -76,21 +55,12 @@ uint64_t parseMetricsInterval() {
} // anonymous namespace
ClientMetric::ClientMetric(uint64_t interval_seconds,
const std::map<std::string, std::string>& labels,
bool bandwidth_reporting_enabled,
bool master_rpc_metrics_enabled)
const std::map<std::string, std::string>& labels)
: transfer_metric(labels),
master_client_metric(labels),
transfer_operation_metric(labels),
ssd_metric(labels),
should_stop_metrics_thread_(false),
metrics_interval_seconds_(interval_seconds),
bandwidth_reporting_enabled_(bandwidth_reporting_enabled),
master_rpc_metrics_enabled_(master_rpc_metrics_enabled) {
last_report_snapshot_ = TransferSnapshot{
static_cast<uint64_t>(transfer_metric.total_read_bytes.value()),
static_cast<uint64_t>(transfer_metric.total_write_bytes.value()),
std::chrono::steady_clock::now()};
metrics_interval_seconds_(interval_seconds) {
if (metrics_interval_seconds_ > 0) {
StartMetricsReportingThread();
}
@ -99,8 +69,7 @@ ClientMetric::ClientMetric(uint64_t interval_seconds,
ClientMetric::~ClientMetric() { StopMetricsReportingThread(); }
std::unique_ptr<ClientMetric> ClientMetric::Create(
const std::map<std::string, std::string>& labels,
bool master_rpc_metrics_enabled) {
const std::map<std::string, std::string>& labels) {
if (!parseMetricsEnabled()) {
LOG(INFO) << "Client metrics disabled (set MC_STORE_CLIENT_METRIC=0 to "
"disable)";
@ -108,83 +77,29 @@ std::unique_ptr<ClientMetric> ClientMetric::Create(
}
uint64_t interval = parseMetricsInterval();
bool bandwidth_reporting_enabled =
parseBoolEnv("MC_STORE_CLIENT_METRIC_BANDWIDTH", true);
LOG(INFO) << "Client metrics enabled (default enabled)";
LOG(INFO) << "Client bandwidth summary "
<< (bandwidth_reporting_enabled ? "enabled" : "disabled")
<< " via MC_STORE_CLIENT_METRIC_BANDWIDTH";
return std::make_unique<ClientMetric>(interval, labels,
bandwidth_reporting_enabled,
master_rpc_metrics_enabled);
return std::make_unique<ClientMetric>(interval, labels);
}
void ClientMetric::serialize(std::string& str) {
transfer_metric.serialize(str);
if (master_rpc_metrics_enabled_) {
master_client_metric.serialize(str);
}
transfer_operation_metric.serialize(str);
master_client_metric.serialize(str);
ssd_metric.serialize(str);
}
std::string ClientMetric::summary_metrics() {
std::stringstream ss;
ss << "Client Metrics Summary\n";
ss << transfer_metric.summary_metrics(bandwidth_reporting_enabled_);
ss << transfer_metric.summary_metrics();
ss << "\n";
if (master_rpc_metrics_enabled_) {
ss << master_client_metric.summary_metrics();
ss << "\n";
}
ss << transfer_operation_metric.summary_metrics();
ss << master_client_metric.summary_metrics();
ss << "\n";
ss << ssd_metric.summary_metrics();
return ss.str();
}
std::string ClientMetric::BuildBandwidthReport() {
if (!bandwidth_reporting_enabled_) {
return "";
}
const auto now = std::chrono::steady_clock::now();
const uint64_t read_bytes = transfer_metric.total_read_bytes.value();
const uint64_t write_bytes = transfer_metric.total_write_bytes.value();
std::lock_guard<std::mutex> lock(snapshot_mutex_);
if (!last_report_snapshot_.has_value()) {
last_report_snapshot_ = TransferSnapshot{read_bytes, write_bytes, now};
return "";
}
const auto previous = *last_report_snapshot_;
last_report_snapshot_ = TransferSnapshot{read_bytes, write_bytes, now};
const double elapsed_seconds = std::max(
std::chrono::duration<double>(now - previous.timestamp).count(), 1e-9);
const uint64_t read_delta = read_bytes >= previous.read_bytes
? read_bytes - previous.read_bytes
: 0;
const uint64_t write_delta = write_bytes >= previous.write_bytes
? write_bytes - previous.write_bytes
: 0;
std::stringstream ss;
ss << "=== Interval Throughput Summary ===\n";
ss << "Read Throughput: "
<< format_metric_rate(read_delta / elapsed_seconds, "B/s") << " ("
<< byte_size_to_string(read_delta) << " over " << std::fixed
<< std::setprecision(2) << elapsed_seconds << "s)\n";
ss << "Write Throughput: "
<< format_metric_rate(write_delta / elapsed_seconds, "B/s") << " ("
<< byte_size_to_string(write_delta) << " over " << std::fixed
<< std::setprecision(2) << elapsed_seconds << "s)";
return ss.str();
}
void ClientMetric::StartMetricsReportingThread() {
should_stop_metrics_thread_ = false;
metrics_reporting_thread_ =
@ -209,12 +124,7 @@ void ClientMetric::StartMetricsReportingThread() {
// Print metrics summary
std::string summary = summary_metrics();
std::string bandwidth_report = BuildBandwidthReport();
std::string report = "Client Metrics Report:\n" + summary;
if (!bandwidth_report.empty()) {
report += "\n" + bandwidth_report;
}
LOG(INFO) << report;
LOG(INFO) << "Client Metrics Report:\n" << summary;
}
LOG(INFO) << "Client metrics reporting thread stopped";
});
@ -230,4 +140,4 @@ void ClientMetric::StopMetricsReportingThread() {
}
}
} // namespace mooncake
} // namespace mooncake

View File

@ -27,14 +27,9 @@
#include "utils.h"
#include "rpc_types.h"
#include "local_hot_cache.h"
#include "gpu_staging_utils.h"
namespace mooncake {
using gpu_staging::CopyDeviceToHost;
using gpu_staging::IsDevicePointer;
using gpu_staging::SetDevice;
[[nodiscard]] size_t CalculateSliceSize(const std::vector<Slice>& slices) {
size_t slice_size = 0;
for (const auto& slice : slices) {
@ -62,7 +57,6 @@ Client::Client(const std::string& local_hostname,
local_hostname_(local_hostname),
metadata_connstring_(metadata_connstring),
protocol_(protocol),
pinned_buffer_pool_(std::make_unique<PinnedBufferPool>()),
write_thread_pool_(2),
task_thread_pool_(4) {
LOG(INFO) << "client_id=" << client_id_;
@ -2501,34 +2495,19 @@ void Client::PutToLocalFile(const std::string& key,
}
std::string path = disk_descriptor.file_path;
// Currently, persistence is achieved through asynchronous writes, but
// before asynchronous writing in 3FS, significant performance degradation
// may occur due to data copying. Profiling reveals that the number of page
// faults triggered in this scenario is nearly double the normal count.
// Future plans include introducing a reuse buffer list to address this
// performance degradation issue.
// Synchronous D2H staging + copy into std::string.
// Done on the calling thread to guarantee GPU buffers are still valid
// (BatchPut has not yet returned to Python, so blocks are not reused).
std::string value;
value.reserve(total_size);
for (const auto& slice : slices) {
int device_id = -1;
if (IsDevicePointer(slice.ptr, &device_id)) {
SetDevice(device_id);
auto buf = pinned_buffer_pool_->Acquire(slice.size);
if (!CopyDeviceToHost(buf.data, slice.ptr, slice.size)) {
LOG(ERROR) << "D2H copy failed for key: " << key
<< ", triggering PutRevoke for disk replica";
pinned_buffer_pool_->Release(buf);
// Must revoke to avoid phantom replica in master
master_client_.PutRevoke(key, ReplicaType::DISK);
return;
}
value.append(buf.data, slice.size);
pinned_buffer_pool_->Release(buf);
} else {
value.append(static_cast<char*>(slice.ptr), slice.size);
}
value.append(static_cast<char*>(slice.ptr), slice.size);
}
// Async StoreObject + PutEnd (unchanged from original)
write_thread_pool_.enqueue([this, backend = storage_backend_, key,
value = std::move(value), path] {
// Store the object

View File

@ -7,7 +7,6 @@
#include <sys/stat.h> // For S_IRUSR, S_IWUSR
#include <fcntl.h> // For O_CREAT, O_RDWR
#include <unistd.h> // For ftruncate, close, shm_unlink
#include <chrono>
#include <cstdlib>
#include "real_client.h"
@ -24,75 +23,6 @@
#endif
namespace {
size_t sum_value_sizes(const std::vector<std::span<const char>>& values) {
size_t total = 0;
for (const auto& value : values) {
total += value.size_bytes();
}
return total;
}
size_t sum_sizes(const std::vector<size_t>& sizes) {
size_t total = 0;
for (size_t size : sizes) {
total += size;
}
return total;
}
size_t sum_successful_sizes(const std::vector<int>& results,
const std::vector<size_t>& sizes) {
size_t total = 0;
for (size_t i = 0; i < results.size() && i < sizes.size(); ++i) {
if (results[i] == 0) {
total += sizes[i];
}
}
return total;
}
size_t sum_successful_nested_sizes(
const std::vector<int>& results,
const std::vector<std::vector<size_t>>& nested_sizes) {
size_t total = 0;
for (size_t i = 0; i < results.size() && i < nested_sizes.size(); ++i) {
if (results[i] == 0) {
total += sum_sizes(nested_sizes[i]);
}
}
return total;
}
size_t sum_positive_results(const std::vector<int64_t>& results) {
size_t total = 0;
for (int64_t result : results) {
if (result > 0) {
total += static_cast<size_t>(result);
}
}
return total;
}
size_t sum_positive_results(const std::vector<int>& results) {
size_t total = 0;
for (int result : results) {
if (result > 0) {
total += static_cast<size_t>(result);
}
}
return total;
}
size_t sum_positive_ranges(
const std::vector<std::vector<std::vector<int64_t>>>& results) {
size_t total = 0;
for (const auto& key_rows : results) {
for (const auto& row : key_rows) {
total += sum_positive_results(row);
}
}
return total;
}
std::vector<uint64_t> void_ptrs_to_u64(const std::vector<void*>& ptrs) {
std::vector<uint64_t> out;
@ -199,10 +129,7 @@ std::vector<tl::expected<ResultType, ErrorCode>> DummyClient::invoke_batch_rpc(
}());
}
DummyClient::DummyClient()
: client_id_(generate_uuid()),
metrics_(ClientMetric::Create(merge_labels({{"client_mode", "dummy"}}),
false)) {
DummyClient::DummyClient() : client_id_(generate_uuid()) {
// Initialize logging severity (leave as before)
mooncake::init_ylt_log_level();
// Initialize client pools
@ -214,30 +141,6 @@ DummyClient::DummyClient()
DummyClient::~DummyClient() { tearDownAll(); }
void DummyClient::ObserveTransferMetric(TransferOperationKind kind,
const char* op_name, size_t bytes,
uint64_t latency_us, bool batch) {
if (!metrics_) {
return;
}
metrics_->ObserveTransferOperation(kind, op_name, bytes, latency_us);
if (kind == TransferOperationKind::kRead) {
metrics_->transfer_metric.total_read_bytes.inc(bytes);
if (batch) {
metrics_->transfer_metric.batch_get_latency_us.observe(latency_us);
} else {
metrics_->transfer_metric.get_latency_us.observe(latency_us);
}
} else {
metrics_->transfer_metric.total_write_bytes.inc(bytes);
if (batch) {
metrics_->transfer_metric.batch_put_latency_us.observe(latency_us);
} else {
metrics_->transfer_metric.put_latency_us.observe(latency_us);
}
}
}
ErrorCode DummyClient::connect(const std::string& server_address) {
ScopedVLogTimer timer(1, "DummyClient::Connect");
timer.LogRequest("real_client_addr=", server_address);
@ -621,40 +524,35 @@ uint64_t DummyClient::alloc_from_mem_pool(size_t size) {
int DummyClient::put(const std::string& key, std::span<const char> value,
const ReplicateConfig& config) {
return invoke_observed_void_rpc<&RealClient::put_dummy_helper>(
TransferOperationKind::kWrite, "put", value.size_bytes(), false, key,
value, config, client_id_);
return to_py_ret(invoke_rpc<&RealClient::put_dummy_helper, void>(
key, value, config, client_id_));
}
int DummyClient::put_batch(const std::vector<std::string>& keys,
const std::vector<std::span<const char>>& values,
const ReplicateConfig& config) {
return invoke_observed_void_rpc<&RealClient::put_batch_dummy_helper>(
TransferOperationKind::kWrite, "put_batch", sum_value_sizes(values),
true, keys, values, config, client_id_);
return to_py_ret(invoke_rpc<&RealClient::put_batch_dummy_helper, void>(
keys, values, config, client_id_));
}
int DummyClient::put_parts(const std::string& key,
std::vector<std::span<const char>> values,
const ReplicateConfig& config) {
return invoke_observed_void_rpc<&RealClient::put_parts_dummy_helper>(
TransferOperationKind::kWrite, "put_parts", sum_value_sizes(values),
false, key, values, config, client_id_);
return to_py_ret(invoke_rpc<&RealClient::put_parts_dummy_helper, void>(
key, values, config, client_id_));
}
int DummyClient::upsert(const std::string& key, std::span<const char> value,
const ReplicateConfig& config) {
return invoke_observed_void_rpc<&RealClient::upsert_dummy_helper>(
TransferOperationKind::kWrite, "upsert", value.size_bytes(), false, key,
value, config, client_id_);
return to_py_ret(invoke_rpc<&RealClient::upsert_dummy_helper, void>(
key, value, config, client_id_));
}
int DummyClient::upsert_from(const std::string& key, void* buffer, size_t size,
const ReplicateConfig& config) {
uint64_t dummy_addr = reinterpret_cast<uint64_t>(buffer);
return invoke_observed_void_rpc<&RealClient::upsert_from_dummy_helper>(
TransferOperationKind::kWrite, "upsert_from", size, false, key,
dummy_addr, size, config, client_id_);
return to_py_ret(invoke_rpc<&RealClient::upsert_from_dummy_helper, void>(
key, dummy_addr, size, config, client_id_));
}
std::vector<int> DummyClient::batch_upsert_from(
@ -664,7 +562,6 @@ std::vector<int> DummyClient::batch_upsert_from(
for (auto ptr : buffer_ptrs) {
buffers.push_back(reinterpret_cast<uint64_t>(ptr));
}
const auto start_time = std::chrono::steady_clock::now();
auto internal_results =
invoke_batch_rpc<&RealClient::batch_upsert_from_dummy_helper, void>(
keys.size(), keys, buffers, sizes, config, client_id_);
@ -673,29 +570,21 @@ std::vector<int> DummyClient::batch_upsert_from(
for (const auto& result : internal_results) {
results.push_back(to_py_ret(result));
}
const size_t successful_bytes = sum_successful_sizes(results, sizes);
if (successful_bytes > 0) {
ObserveTransferMetric(TransferOperationKind::kWrite,
"batch_upsert_from", successful_bytes,
elapsed_us_since(start_time), true);
}
return results;
}
int DummyClient::upsert_parts(const std::string& key,
std::vector<std::span<const char>> values,
const ReplicateConfig& config) {
return invoke_observed_void_rpc<&RealClient::upsert_parts_dummy_helper>(
TransferOperationKind::kWrite, "upsert_parts", sum_value_sizes(values),
false, key, values, config, client_id_);
return to_py_ret(invoke_rpc<&RealClient::upsert_parts_dummy_helper, void>(
key, values, config, client_id_));
}
int DummyClient::upsert_batch(const std::vector<std::string>& keys,
const std::vector<std::span<const char>>& values,
const ReplicateConfig& config) {
return invoke_observed_void_rpc<&RealClient::upsert_batch_dummy_helper>(
TransferOperationKind::kWrite, "upsert_batch", sum_value_sizes(values),
true, keys, values, config, client_id_);
return to_py_ret(invoke_rpc<&RealClient::upsert_batch_dummy_helper, void>(
keys, values, config, client_id_));
}
int DummyClient::remove(const std::string& key, bool force) {
@ -761,7 +650,6 @@ int64_t DummyClient::getSize(const std::string& key) {
}
std::shared_ptr<BufferHandle> DummyClient::get_buffer(const std::string& key) {
const auto start_time = std::chrono::steady_clock::now();
// Try hot cache path if shm is mapped
if (hot_cache_base_) {
auto result = invoke_rpc<&RealClient::acquire_hot_cache,
@ -780,8 +668,6 @@ std::shared_ptr<BufferHandle> DummyClient::get_buffer(const std::string& key) {
(void)invoke_rpc<&RealClient::release_hot_cache, void>(
key_copy);
};
ObserveTransferMetric(TransferOperationKind::kRead, "get_buffer",
size, elapsed_us_since(start_time), false);
return std::make_shared<BufferHandle>(local_ptr, size,
std::move(release));
}
@ -800,14 +686,11 @@ std::shared_ptr<BufferHandle> DummyClient::get_buffer(const std::string& key) {
(void)invoke_rpc<&RealClient::release_buffer_dummy, void>(dummy_addr,
client_id_);
};
ObserveTransferMetric(TransferOperationKind::kRead, "get_buffer", size,
elapsed_us_since(start_time), false);
return std::make_shared<BufferHandle>(local_ptr, size, std::move(release));
}
std::vector<std::shared_ptr<BufferHandle>> DummyClient::batch_get_buffer(
const std::vector<std::string>& keys) {
const auto start_time = std::chrono::steady_clock::now();
std::vector<std::shared_ptr<BufferHandle>> results(keys.size(), nullptr);
if (keys.empty()) return results;
@ -868,37 +751,19 @@ std::vector<std::shared_ptr<BufferHandle>> DummyClient::batch_get_buffer(
std::make_shared<BufferHandle>(ptr, size, std::move(release));
}
size_t total_bytes = 0;
for (const auto& result : results) {
if (result != nullptr) {
total_bytes += result->size();
}
}
if (total_bytes > 0) {
ObserveTransferMetric(TransferOperationKind::kRead, "batch_get_buffer",
total_bytes, elapsed_us_since(start_time), true);
}
return results;
}
int64_t DummyClient::get_into(const std::string& key, void* buffer,
size_t size) {
uint64_t buf_addr = reinterpret_cast<uint64_t>(buffer);
const auto start_time = std::chrono::steady_clock::now();
auto result = invoke_rpc<&RealClient::get_into_range_shm_helper,
tl::expected<int64_t, ErrorCode>>(
key, buf_addr, 0, 0, size, client_id_);
if (!result) {
return static_cast<int64_t>(toInt(result.error()));
}
const int64_t bytes_read = to_py_ret(*result);
if (bytes_read >= 0) {
ObserveTransferMetric(TransferOperationKind::kRead, "get_into",
static_cast<size_t>(bytes_read),
elapsed_us_since(start_time), false);
}
return bytes_read;
return to_py_ret(*result);
}
std::vector<std::vector<std::vector<int64_t>>> DummyClient::get_into_ranges(
@ -908,7 +773,6 @@ std::vector<std::vector<std::vector<int64_t>>> DummyClient::get_into_ranges(
const std::vector<std::vector<std::vector<size_t>>>& all_src_offsets,
const std::vector<std::vector<std::vector<size_t>>>& all_sizes) {
std::vector<uint64_t> dummy_buffers = void_ptrs_to_u64(buffers);
const auto start_time = std::chrono::steady_clock::now();
auto internal_results =
invoke_rpc<&RealClient::get_into_ranges_shm_helper,
std::vector<std::vector<
@ -922,13 +786,8 @@ std::vector<std::vector<std::vector<int64_t>>> DummyClient::get_into_ranges(
all_dst_offsets,
internal_results.error());
}
auto results = convert_ranged_read_results(internal_results.value());
const size_t total_bytes = sum_positive_ranges(results);
if (total_bytes > 0) {
ObserveTransferMetric(TransferOperationKind::kRead, "get_into_ranges",
total_bytes, elapsed_us_since(start_time), true);
}
return results;
return convert_ranged_read_results(internal_results.value());
}
std::string DummyClient::get_hostname() const {
@ -940,7 +799,6 @@ std::vector<int> DummyClient::batch_put_from(
const std::vector<std::string>& keys, const std::vector<void*>& buffer_ptrs,
const std::vector<size_t>& sizes, const ReplicateConfig& config) {
std::vector<uint64_t> buffers = void_ptrs_to_u64(buffer_ptrs);
const auto start_time = std::chrono::steady_clock::now();
auto internal_results =
invoke_batch_rpc<&RealClient::batch_put_from_dummy_helper, void>(
keys.size(), keys, buffers, sizes, config, device_id_, client_id_);
@ -951,13 +809,6 @@ std::vector<int> DummyClient::batch_put_from(
results.push_back(to_py_ret(result));
}
const size_t successful_bytes = sum_successful_sizes(results, sizes);
if (successful_bytes > 0) {
ObserveTransferMetric(TransferOperationKind::kWrite, "batch_put_from",
successful_bytes, elapsed_us_since(start_time),
true);
}
return results;
}
@ -971,7 +822,6 @@ std::vector<int64_t> DummyClient::batch_get_into(
const std::vector<std::string>& keys, const std::vector<void*>& buffer_ptrs,
const std::vector<size_t>& sizes) {
std::vector<uint64_t> buffers = void_ptrs_to_u64(buffer_ptrs);
const auto start_time = std::chrono::steady_clock::now();
auto internal_results =
invoke_batch_rpc<&RealClient::batch_get_into_dummy_helper, int64_t>(
keys.size(), keys, buffers, sizes, device_id_, client_id_);
@ -982,12 +832,6 @@ std::vector<int64_t> DummyClient::batch_get_into(
results.push_back(to_py_ret(result));
}
const size_t total_bytes = sum_positive_results(results);
if (total_bytes > 0) {
ObserveTransferMetric(TransferOperationKind::kRead, "batch_get_into",
total_bytes, elapsed_us_since(start_time), true);
}
return results;
}
@ -1006,7 +850,6 @@ std::vector<int> DummyClient::batch_put_from_multi_buffers(
const ReplicateConfig& config) {
std::vector<std::vector<uint64_t>> dummy_nested =
void_ptr_rows_to_u64_nested(all_buffer_ptrs);
const auto start_time = std::chrono::steady_clock::now();
auto internal_results =
invoke_batch_rpc<&RealClient::batch_put_from_multi_buffers_dummy_helper,
void>(keys.size(), keys, dummy_nested, all_sizes,
@ -1016,13 +859,6 @@ std::vector<int> DummyClient::batch_put_from_multi_buffers(
for (const auto& result : internal_results) {
results.push_back(to_py_ret(result));
}
const size_t successful_bytes =
sum_successful_nested_sizes(results, all_sizes);
if (successful_bytes > 0) {
ObserveTransferMetric(TransferOperationKind::kWrite,
"batch_put_from_multi_buffers", successful_bytes,
elapsed_us_since(start_time), true);
}
return results;
}
@ -1033,7 +869,6 @@ std::vector<int> DummyClient::batch_get_into_multi_buffers(
bool prefer_alloc_in_same_node) {
std::vector<std::vector<uint64_t>> dummy_nested =
void_ptr_rows_to_u64_nested(all_buffer_ptrs);
const auto start_time = std::chrono::steady_clock::now();
auto internal_results =
invoke_batch_rpc<&RealClient::batch_get_into_multi_buffers_dummy_helper,
int64_t>(keys.size(), keys, dummy_nested, all_sizes,
@ -1044,12 +879,6 @@ std::vector<int> DummyClient::batch_get_into_multi_buffers(
for (const auto& result : internal_results) {
results.push_back(to_py_ret(result));
}
const size_t total_bytes = sum_positive_results(results);
if (total_bytes > 0) {
ObserveTransferMetric(TransferOperationKind::kRead,
"batch_get_into_multi_buffers", total_bytes,
elapsed_us_since(start_time), true);
}
return results;
}

View File

@ -7,17 +7,12 @@
#include "storage_backend.h"
#include "client_metric.h"
#include "utils.h"
#include "gpu_staging_utils.h"
#ifdef USE_URING
#include "file_interface.h"
#endif
namespace mooncake {
using gpu_staging::CopyDeviceToHost;
using gpu_staging::IsDevicePointer;
using gpu_staging::SetDevice;
FileStorageConfig FileStorageConfig::FromEnvironment() {
FileStorageConfig config;
@ -42,10 +37,9 @@ FileStorageConfig FileStorageConfig::FromEnvironment() {
config.local_buffer_size = GetEnvOr<int64_t>(
"MOONCAKE_OFFLOAD_LOCAL_BUFFER_SIZE_BYTES", config.local_buffer_size);
config.scanmeta_iterator_keys_limit = GetEnvOr<int64_t>(
"MOONCAKE_OFFLOAD_SCANMETA_ITERATOR_KEYS_LIMIT",
config.scanmeta_iterator_keys_limit =
GetEnvOr<int64_t>("MOONCAKE_SCANMETA_ITERATOR_KEYS_LIMIT",
config.scanmeta_iterator_keys_limit));
config.scanmeta_iterator_keys_limit);
config.total_keys_limit = GetEnvOr<int64_t>(
"MOONCAKE_OFFLOAD_TOTAL_KEYS_LIMIT", config.total_keys_limit);
@ -64,9 +58,7 @@ FileStorageConfig FileStorageConfig::FromEnvironment() {
GetEnvOr<uint64_t>("MOONCAKE_OFFLOAD_CLIENT_BUFFER_GC_TTL_MS",
config.client_buffer_gc_ttl_ms);
auto use_uring_str =
GetEnvStringOr("MOONCAKE_OFFLOAD_USE_URING",
GetEnvStringOr("MOONCAKE_USE_URING", "false"));
auto use_uring_str = GetEnvStringOr("MOONCAKE_USE_URING", "false");
config.use_uring = (use_uring_str == "true" || use_uring_str == "1");
return config;
@ -160,7 +152,6 @@ FileStorage::FileStorage(const FileStorageConfig& config,
client_(client),
ssd_metric_(ssd_metric),
local_rpc_addr_(local_rpc_addr),
pinned_buffer_pool_(std::make_unique<PinnedBufferPool>()),
client_buffer_allocator_(
AlignedClientBufferAllocator::create(config.local_buffer_size, "")) {
if (!config.Validate()) {
@ -381,37 +372,6 @@ tl::expected<void, ErrorCode> FileStorage::OffloadObjects(
}
};
// D2H staging: replace device slices with host memory slices
// so that storage_backend (ConcatSlicesToString / BuildBucket /
// WriteBucket) always receives host pointers.
std::unordered_map<std::string, std::vector<Slice>> host_batch_object;
std::vector<PinnedBufferPool::Buffer> staging_bufs;
for (auto& [obj_key, slices] : batch_object) {
std::vector<Slice> host_slices;
bool obj_success = true;
for (const auto& slice : slices) {
int device_id = -1;
if (IsDevicePointer(slice.ptr, &device_id)) {
SetDevice(device_id);
auto buf = pinned_buffer_pool_->Acquire(slice.size);
if (!CopyDeviceToHost(buf.data, slice.ptr, slice.size)) {
LOG(ERROR) << "D2H staging failed for key: " << obj_key;
pinned_buffer_pool_->Release(buf);
obj_success = false;
break;
}
host_slices.emplace_back(Slice{buf.data, slice.size});
staging_bufs.push_back(buf);
} else {
host_slices.push_back(slice);
}
}
if (obj_success) {
host_batch_object[obj_key] = std::move(host_slices);
}
}
auto offload_start = std::chrono::steady_clock::now();
auto bucket_complete_handler =
[this, offload_start, complete_handler](
@ -439,12 +399,7 @@ tl::expected<void, ErrorCode> FileStorage::OffloadObjects(
return res;
};
auto offload_res = storage_backend_->BatchOffload(
host_batch_object, bucket_complete_handler, eviction_handler);
// Release staging buffers back to pool (Buffer is POD, no destructor)
for (auto& buf : staging_bufs) {
pinned_buffer_pool_->Release(buf);
}
batch_object, bucket_complete_handler, eviction_handler);
if (!offload_res) {
LOG(ERROR) << "Failed to store objects with error: "
<< offload_res.error();

View File

@ -109,10 +109,6 @@ DEFINE_validator(eviction_ratio, [](const char* flagname, double value) {
DEFINE_bool(enable_ha, false,
"Enable high availability, which depends on etcd");
DEFINE_bool(enable_offload, false, "Enable offload availability");
DEFINE_bool(offload_on_evict, false,
"Defer LOCAL_DISK offload to eviction time instead of PutEnd");
DEFINE_bool(offload_force_evict, false,
"Force-evict objects exceeding offload cap without disk offload");
DEFINE_string(ha_backend_type, "etcd",
"HA backend type, e.g. etcd | redis | k8s");
DEFINE_string(ha_backend_connstring, "",
@ -297,11 +293,6 @@ void InitMasterConf(const mooncake::DefaultConfig& default_config,
FLAGS_enable_ha);
default_config.GetBool("enable_offload", &master_config.enable_offload,
FLAGS_enable_offload);
default_config.GetBool("offload_on_evict", &master_config.offload_on_evict,
FLAGS_offload_on_evict);
default_config.GetBool("offload_force_evict",
&master_config.offload_force_evict,
FLAGS_offload_force_evict);
default_config.GetString("ha_backend_type", &master_config.ha_backend_type,
FLAGS_ha_backend_type);
default_config.GetString("ha_backend_connstring",
@ -876,8 +867,6 @@ int main(int argc, char* argv[]) {
<< master_config.eviction_high_watermark_ratio
<< ", enable_ha=" << master_config.enable_ha
<< ", enable_offload=" << master_config.enable_offload
<< ", offload_on_evict=" << master_config.offload_on_evict
<< ", offload_force_evict=" << master_config.offload_force_evict
<< ", ha_backend_type=" << master_config.ha_backend_type
<< ", ha_backend_connstring=" << ha_backend_connstring
<< ", etcd_endpoints=" << master_config.etcd_endpoints

View File

@ -48,13 +48,6 @@ namespace {
constexpr size_t kUnlimitedSnapshotList = 0;
// Per-cycle offload cap as a fraction of `offloading_queue_limit_`. Used only
// when offload-on-evict mode is active. Defers memory eviction for at most
// this fraction of the queue limit per BatchEvict cycle; beyond that, eviction
// falls back according to `offload_force_evict_`. A future change may expose
// this as a configurable parameter if workloads demand tuning.
constexpr double kOffloadCapRatio = 0.5;
enum class SnapshotCatalogBackendKind {
kEmbedded,
kRedis,
@ -166,19 +159,6 @@ MasterService::MasterService(const MasterServiceConfig& config)
"put_start_discard_timeout_sec");
}
// Offload-on-evict: defer LOCAL_DISK offload to eviction time
offload_on_evict_ = enable_offload_ && config.offload_on_evict;
if (offload_on_evict_) {
LOG(INFO) << "Offload-on-evict mode enabled: DRAM offload to "
"LOCAL_DISK will occur at eviction time instead of "
"PutEnd";
offload_force_evict_ = config.offload_force_evict;
if (offload_force_evict_) {
LOG(INFO) << "Force-evict enabled: objects exceeding offload "
"cap will be evicted without disk offload";
}
}
eviction_running_ = true;
eviction_thread_ = std::thread(&MasterService::EvictionThreadFunc, this);
VLOG(1) << "action=start_eviction_thread";
@ -371,23 +351,12 @@ auto MasterService::ReMountSegment(const std::vector<Segment>& segments,
return {};
}
std::unordered_set<UUID, boost::hash<UUID>>
MasterService::getAliveClientsSnapshot() const {
std::shared_lock<std::shared_mutex> lock(client_mutex_);
return ok_client_;
}
void MasterService::ClearInvalidHandles() {
ClearInvalidHandles(getAliveClientsSnapshot());
}
void MasterService::ClearInvalidHandles(
const std::unordered_set<UUID, boost::hash<UUID>>& alive_clients) {
for (size_t i = 0; i < kNumShards; i++) {
MetadataShardAccessorRW shard(this, i);
auto it = shard->metadata.begin();
while (it != shard->metadata.end()) {
if (CleanupStaleHandles(it->second, alive_clients)) {
if (CleanupStaleHandles(it->second)) {
// If the object is empty, we need to erase the iterator and
// also erase the key from processing_keys,
// replication_tasks, and offloading_tasks.
@ -873,15 +842,13 @@ auto MasterService::PutStart(const UUID& client_id, const std::string& key,
VLOG(1) << "key=" << key << ", value_length=" << slice_length
<< ", config=" << config << ", action=put_start_begin";
auto alive_clients = getAliveClientsSnapshot();
std::shared_lock<std::shared_mutex> shared_lock(snapshot_mutex_);
// Lock the shard and check if object already exists
MetadataShardAccessorRW shard(this, getShardIndex(key));
const auto now = std::chrono::system_clock::now();
auto it = shard->metadata.find(key);
if (it != shard->metadata.end() &&
!CleanupStaleHandles(it->second, alive_clients)) {
if (it != shard->metadata.end() && !CleanupStaleHandles(it->second)) {
auto& metadata = it->second;
// If the object's PutStart expired and has not completed any
// replicas, we can discard it and allow the new PutStart to
@ -930,7 +897,7 @@ auto MasterService::PutEnd(const UUID& client_id, const std::string& key,
},
[](Replica& replica) { replica.mark_complete(); });
if (enable_offload_ && !offload_on_evict_) {
if (enable_offload_) {
auto& shard = accessor.GetShard();
metadata.VisitReplicas(
&Replica::fn_is_completed, [this, &key, &shard](Replica& replica) {
@ -1118,7 +1085,6 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
// during full metadata snapshots.
// shard lock (exclusive via MetadataShardAccessorRW): serializes all
// operations on keys that hash to the same shard.
auto alive_clients = getAliveClientsSnapshot();
std::shared_lock<std::shared_mutex> shared_lock(snapshot_mutex_);
MetadataShardAccessorRW shard(this, getShardIndex(key));
@ -1128,9 +1094,7 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
// --- Step 0: stale handle cleanup ---
// If all memory replicas point to unmounted segments (node crashed and
// restarted), the metadata is useless — erase it and treat as new key.
// Also clean up local_disk replicas whose owner client has expired.
if (it != shard->metadata.end() &&
CleanupStaleHandles(it->second, alive_clients)) {
if (it != shard->metadata.end() && CleanupStaleHandles(it->second)) {
shard->processing_keys.erase(key);
shard->metadata.erase(it);
it = shard->metadata.end();
@ -1967,8 +1931,6 @@ auto MasterService::BatchRemove(const std::vector<std::string>& keys,
std::shared_lock<std::shared_mutex> snapshot_lock(snapshot_mutex_);
auto alive_clients = getAliveClientsSnapshot();
// Process each shard once, acquiring lock per shard
for (auto& [shard_idx, key_group] : keys_by_shard) {
MetadataShardAccessorRW shard(this, shard_idx);
@ -1986,7 +1948,7 @@ auto MasterService::BatchRemove(const std::vector<std::string>& keys,
}
// Clean up stale replica handles (consistent with single Remove)
if (CleanupStaleHandles(it->second, alive_clients)) {
if (CleanupStaleHandles(it->second)) {
shard->processing_keys.erase(key);
shard->replication_tasks.erase(key);
shard->offloading_tasks.erase(key);
@ -2034,14 +1996,10 @@ auto MasterService::BatchRemove(const std::vector<std::string>& keys,
return results;
}
bool MasterService::CleanupStaleHandles(
ObjectMetadata& metadata,
const std::unordered_set<UUID, boost::hash<UUID>>& alive_clients) {
// Remove those with invalid allocators (memory replicas on unmounted
// segments) and local_disk replicas whose owner client is no longer alive.
metadata.EraseReplicas([&alive_clients](const Replica& replica) {
return replica.has_invalid_mem_handle() ||
replica.has_stale_local_disk_client(alive_clients);
bool MasterService::CleanupStaleHandles(ObjectMetadata& metadata) {
// Remove those with invalid allocators
metadata.EraseReplicas([](const Replica& replica) {
return replica.has_invalid_mem_handle();
});
// Return true if no valid replicas remain after cleanup
@ -3511,81 +3469,6 @@ void MasterService::BatchEvict(double evict_ratio_target,
});
};
// --- Offload-on-evict support ---
long offload_queued_this_cycle = 0;
long offload_deferred_count = 0;
long offload_cap_forced_count = 0; // #keys force-evicted due to cap
long offload_push_failed_forced = 0; // #keys force-evicted on push fail
const long offload_cap =
offload_on_evict_
? static_cast<long>(offloading_queue_limit_ * kOffloadCapRatio)
: 0;
auto has_local_disk_replica = [](const ObjectMetadata& metadata) {
return metadata.HasReplica(&Replica::fn_is_local_disk_replica);
};
// Returns freed bytes. Returns 0 if offload-queued and no additional
// replicas were evicted (all MEMORY replicas of the key are now pinned).
auto try_evict_or_offload =
[&, this](const std::string& key, ObjectMetadata& metadata,
MetadataShardAccessorRW& shard) -> uint64_t {
if (!offload_on_evict_) {
// Original behavior
return metadata.size * evict_replicas(metadata);
}
// LOCAL_DISK replica already exists — safe to delete MEMORY immediately
if (has_local_disk_replica(metadata)) {
return metadata.size * evict_replicas(metadata);
}
// Force-evict cap: if force_evict enabled and cap reached, force
// delete. Warning is aggregated at the end of the cycle to avoid log
// flooding.
if (offload_force_evict_ && offload_queued_this_cycle >= offload_cap) {
offload_cap_forced_count++;
return metadata.size * evict_replicas(metadata);
}
// Queue one MEMORY replica for offload; others will be evicted below.
bool queued = false;
metadata.VisitReplicas(
[](const Replica& r) {
return r.is_memory_replica() && r.is_completed() &&
r.get_refcnt() == 0;
},
[this, &key, &shard, &queued, &now](Replica& replica) {
if (queued) return; // only need to pin one replica for offload
auto result = PushOffloadingQueue(key, replica);
if (result) {
replica.inc_refcnt();
shard->offloading_tasks.emplace(
key, OffloadingTask{replica.id(), now});
queued = true;
}
});
if (queued) {
offload_queued_this_cycle++;
offload_deferred_count++;
// Any remaining MEMORY replicas with refcnt==0 are redundant copies
// (data survives via the pinned replica → disk). Evict them now to
// reclaim memory immediately rather than waiting another cycle.
return metadata.size * evict_replicas(metadata);
}
// PushOffloadingQueue failed. Default (data-preserving) behavior is to
// skip this cycle — the outer eviction loop will retry after the
// offload queue drains. Only force-evict when explicitly opted in, to
// prevent silent data loss when the queue is unavailable.
if (offload_force_evict_) {
offload_push_failed_forced++;
return metadata.size * evict_replicas(metadata);
}
return 0;
};
// Randomly select a starting shard to avoid imbalance eviction between
// shards. No need to use expensive random_device here.
size_t start_idx = rand() % kNumShards;
@ -3658,18 +3541,16 @@ void MasterService::BatchEvict(double evict_ratio_target,
continue;
}
if (it->second.lease_timeout <= target_timeout) {
// Evict this object (or defer for offload)
uint64_t freed =
try_evict_or_offload(it->first, it->second, shard);
total_freed_size += freed;
// Evict this object
total_freed_size +=
it->second.size *
evict_replicas(it->second); // Erase memory replicas
if (it->second.IsValid() == false) {
it = shard->metadata.erase(it);
} else {
++it;
}
if (freed > 0) {
shard_evicted_count++;
}
shard_evicted_count++;
} else {
// second pass candidates
no_pin_objects.push_back(it->second.lease_timeout);
@ -3717,22 +3598,20 @@ void MasterService::BatchEvict(double evict_ratio_target,
auto it = shard->metadata.begin();
while (it != shard->metadata.end() && target_evict_num > 0) {
if (!it->second.IsHardPinned() &&
it->second.IsLeaseExpired(now) &&
it->second.lease_timeout <= target_timeout &&
!it->second.IsSoftPinned(now) &&
can_evict_replicas(it->second)) {
// Evict this object (or defer for offload)
uint64_t freed =
try_evict_or_offload(it->first, it->second, shard);
total_freed_size += freed;
// Evict this object
total_freed_size +=
it->second.size *
evict_replicas(
it->second); // Erase memory replicas
if (it->second.IsValid() == false) {
it = shard->metadata.erase(it);
} else {
++it;
}
if (freed > 0) {
evicted_count++;
}
evicted_count++;
target_evict_num--;
} else {
++it;
@ -3772,18 +3651,16 @@ void MasterService::BatchEvict(double evict_ratio_target,
// and lease timeout less than or equal to target.
if (!it->second.IsSoftPinned(now) ||
it->second.lease_timeout <= soft_target_timeout) {
// Evict this object (or defer for offload)
uint64_t freed =
try_evict_or_offload(it->first, it->second, shard);
total_freed_size += freed;
total_freed_size +=
it->second.size *
evict_replicas(
it->second); // Erase memory replicas
if (it->second.IsValid() == false) {
it = shard->metadata.erase(it);
} else {
++it;
}
if (freed > 0) {
evicted_count++;
}
evicted_count++;
target_evict_num--;
} else {
++it;
@ -3804,11 +3681,7 @@ void MasterService::BatchEvict(double evict_ratio_target,
}
}
if (evicted_count > 0 || released_discarded_cnt > 0 ||
offload_deferred_count > 0) {
// Offload-deferred counts as partial success: work was done (objects
// queued for disk offload), so suppress re-triggering until the next
// watermark breach or explicit need_eviction_ signal.
if (evicted_count > 0 || released_discarded_cnt > 0) {
need_eviction_ = false;
MasterMetricManager::instance().inc_eviction_success(evicted_count,
total_freed_size);
@ -3820,27 +3693,7 @@ void MasterService::BatchEvict(double evict_ratio_target,
MasterMetricManager::instance().inc_eviction_fail();
}
VLOG(1) << "action=evict_objects" << ", evicted_count=" << evicted_count
<< ", offload_deferred=" << offload_deferred_count
<< ", offload_cap_forced=" << offload_cap_forced_count
<< ", offload_push_failed_forced=" << offload_push_failed_forced
<< ", total_freed_size=" << total_freed_size;
if (offload_on_evict_ && evicted_count == 0 && offload_deferred_count > 0) {
LOG(WARNING) << "[EVICT] No memory freed this cycle; "
<< offload_deferred_count
<< " objects deferred for disk offload. "
"Consider lowering eviction_high_watermark_ratio.";
}
if (offload_cap_forced_count > 0) {
LOG(WARNING) << "[EVICT] Offload cap (" << offload_cap
<< ") reached; force-evicted " << offload_cap_forced_count
<< " object(s) without disk offload this cycle.";
}
if (offload_push_failed_forced > 0) {
LOG(WARNING) << "[EVICT] PushOffloadingQueue failed for "
<< offload_push_failed_forced
<< " object(s); force-evicted without disk offload "
"(offload_force_evict=true).";
}
}
void MasterService::ClientMonitorFunc() {
@ -3917,15 +3770,9 @@ void MasterService::ClientMonitorFunc() {
} // Release the mutex before long-running ClearInvalidHandles and
// avoid deadlocks
// Always clean up invalid handles when there are expired clients,
// even if no memory segments were unmounted. This is necessary
// to clean up local_disk replicas whose owner client has expired.
ClearInvalidHandles();
if (!unmount_segments.empty()) {
ClearInvalidHandles();
// Commit unmount of memory segments and clean up local_disk
// segments for expired clients. Both require the exclusive
// segment lock.
{
ScopedSegmentAccess segment_access =
segment_manager_.getSegmentAccess();
for (size_t i = 0; i < unmount_segments.size(); i++) {
@ -3935,9 +3782,6 @@ void MasterService::ClientMonitorFunc() {
<< ", segment_name=" << segment_names[i]
<< ", action=unmount_expired_segment";
}
for (auto& client_id : expired_clients) {
segment_access.UnmountLocalDiskSegment(client_id);
}
}
}

View File

@ -62,87 +62,6 @@ struct PreparedRangedReadRequest {
bool has_any_valid_fragment = false;
};
size_t sum_value_sizes(const std::vector<std::span<const char>> &values) {
size_t total = 0;
for (const auto &value : values) {
total += value.size_bytes();
}
return total;
}
size_t sum_sizes(const std::vector<size_t> &sizes) {
size_t total = 0;
for (size_t size : sizes) {
total += size;
}
return total;
}
size_t sum_successful_sizes(const std::vector<int> &results,
const std::vector<size_t> &sizes) {
size_t total = 0;
for (size_t i = 0; i < results.size() && i < sizes.size(); ++i) {
if (results[i] == 0) {
total += sizes[i];
}
}
return total;
}
size_t sum_successful_nested_sizes(
const std::vector<int> &results,
const std::vector<std::vector<size_t>> &nested_sizes) {
size_t total = 0;
for (size_t i = 0; i < results.size() && i < nested_sizes.size(); ++i) {
if (results[i] == 0) {
total += sum_sizes(nested_sizes[i]);
}
}
return total;
}
size_t sum_positive_results(const std::vector<int64_t> &results) {
size_t total = 0;
for (int64_t result : results) {
if (result > 0) {
total += static_cast<size_t>(result);
}
}
return total;
}
size_t sum_positive_results(const std::vector<int> &results) {
size_t total = 0;
for (int result : results) {
if (result > 0) {
total += static_cast<size_t>(result);
}
}
return total;
}
size_t sum_positive_ranges(
const std::vector<std::vector<std::vector<int64_t>>> &results) {
size_t total = 0;
for (const auto &key_rows : results) {
for (const auto &row : key_rows) {
total += sum_positive_results(row);
}
}
return total;
}
size_t sum_buffer_handle_sizes(
const std::vector<std::shared_ptr<BufferHandle>> &buffers) {
size_t total = 0;
for (const auto &buffer : buffers) {
if (buffer != nullptr) {
total += buffer->size();
}
}
return total;
}
PreparedRangedReadRequest prepare_ranged_read_request(
size_t buffer_count, const std::vector<std::vector<std::string>> &all_keys,
const std::vector<std::vector<std::vector<size_t>>> &all_dst_offsets,
@ -535,8 +454,7 @@ tl::expected<void, ErrorCode> RealClient::setup_internal(
const std::string &master_server_addr,
const std::shared_ptr<TransferEngine> &transfer_engine,
const std::string &ipc_socket_path, int local_rpc_port,
bool enable_ssd_offload, bool start_offload_rpc_server,
const std::string &ssd_offload_path) {
bool enable_ssd_offload, bool start_offload_rpc_server) {
this->protocol = protocol;
this->ipc_socket_path_ = ipc_socket_path;
const bool should_use_hugepage = use_hugepage_ &&
@ -575,7 +493,7 @@ tl::expected<void, ErrorCode> RealClient::setup_internal(
hostname.substr(0, colon_pos + 1) + std::to_string(local_rpc_port);
auto client_opt = mooncake::Client::Create(
this->local_hostname, metadata_server, protocol, device_name,
master_server_addr, transfer_engine, {{"client_mode", "real"}});
master_server_addr, transfer_engine);
if (!client_opt) {
LOG(ERROR) << "Failed to create client";
return tl::unexpected(ErrorCode::INVALID_PARAMS);
@ -604,7 +522,7 @@ tl::expected<void, ErrorCode> RealClient::setup_internal(
hostname + ":" + std::to_string(local_rpc_port);
auto client_opt = mooncake::Client::Create(
this->local_hostname, metadata_server, protocol, device_name,
master_server_addr, transfer_engine, {{"client_mode", "real"}});
master_server_addr, transfer_engine);
if (client_opt) {
client_ = *client_opt;
success = true;
@ -797,9 +715,6 @@ tl::expected<void, ErrorCode> RealClient::setup_internal(
}
if (enable_ssd_offload) {
auto file_storage_config = FileStorageConfig::FromEnvironment();
if (!ssd_offload_path.empty()) {
file_storage_config.storage_filepath = ssd_offload_path;
}
file_storage_ = std::make_shared<FileStorage>(
file_storage_config, client_, this->local_rpc_addr,
client_->GetSsdMetricPtr());
@ -827,12 +742,11 @@ int RealClient::setup_real(
const std::string &protocol, const std::string &rdma_devices,
const std::string &master_server_addr,
const std::shared_ptr<TransferEngine> &transfer_engine,
const std::string &ipc_socket_path, bool enable_ssd_offload,
const std::string &ssd_offload_path) {
const std::string &ipc_socket_path, bool enable_ssd_offload) {
return to_py_ret(setup_internal(
local_hostname, metadata_server, global_segment_size, local_buffer_size,
protocol, rdma_devices, master_server_addr, transfer_engine,
ipc_socket_path, 50052, enable_ssd_offload, true, ssd_offload_path));
ipc_socket_path, 50052, enable_ssd_offload, true));
}
namespace {
@ -923,8 +837,6 @@ tl::expected<void, ErrorCode> RealClient::setup_internal(
return tl::unexpected(ErrorCode::INVALID_PARAMS);
}
std::string ssd_offload_path = get_config(config, "ssd_offload_path");
std::string enable_ssd_offload_str =
get_config(config, "enable_ssd_offload", "false");
std::transform(enable_ssd_offload_str.begin(), enable_ssd_offload_str.end(),
@ -936,7 +848,7 @@ tl::expected<void, ErrorCode> RealClient::setup_internal(
return setup_internal(local_hostname, metadata_server, global_segment_size,
local_buffer_size, protocol, rdma_devices,
master_server_addr, nullptr, ipc_socket_path, 50052,
enable_ssd_offload, true, ssd_offload_path);
enable_ssd_offload, true);
}
tl::expected<void, ErrorCode> RealClient::initAll_internal(
@ -1165,17 +1077,8 @@ tl::expected<void, ErrorCode> RealClient::put_dummy_helper(
int RealClient::put(const std::string &key, std::span<const char> value,
const ReplicateConfig &config) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() {
return put_internal(key, value, config, client_buffer_allocator_);
},
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
client_->ObserveTransferOperation(TransferOperationKind::kWrite,
"put", value.size_bytes(),
latency_us);
});
return to_py_ret(result);
return to_py_ret(
put_internal(key, value, config, client_buffer_allocator_));
}
tl::expected<void, ErrorCode> RealClient::put_batch_internal(
@ -1264,18 +1167,8 @@ tl::expected<void, ErrorCode> RealClient::put_batch_dummy_helper(
int RealClient::put_batch(const std::vector<std::string> &keys,
const std::vector<std::span<const char>> &values,
const ReplicateConfig &config) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() {
return put_batch_internal(keys, values, config,
client_buffer_allocator_);
},
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
client_->ObserveTransferOperation(
TransferOperationKind::kWrite, "put_batch",
sum_value_sizes(values), latency_us);
});
return to_py_ret(result);
return to_py_ret(
put_batch_internal(keys, values, config, client_buffer_allocator_));
}
tl::expected<void, ErrorCode> RealClient::put_parts_internal(
@ -1356,18 +1249,8 @@ tl::expected<void, ErrorCode> RealClient::put_parts_dummy_helper(
int RealClient::put_parts(const std::string &key,
std::vector<std::span<const char>> values,
const ReplicateConfig &config) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() {
return put_parts_internal(key, values, config,
client_buffer_allocator_);
},
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
client_->ObserveTransferOperation(
TransferOperationKind::kWrite, "put_parts",
sum_value_sizes(values), latency_us);
});
return to_py_ret(result);
return to_py_ret(
put_parts_internal(key, values, config, client_buffer_allocator_));
}
tl::expected<void, ErrorCode> RealClient::remove_internal(
@ -1964,14 +1847,7 @@ std::shared_ptr<BufferHandle> RealClient::get_buffer_internal(
// Implementation of get_buffer method
std::shared_ptr<BufferHandle> RealClient::get_buffer(const std::string &key) {
return execute_timed_operation<std::shared_ptr<BufferHandle>>(
[&]() { return get_buffer_internal(key, client_buffer_allocator_); },
[](const auto &buffer) { return buffer != nullptr; },
[&](uint64_t latency_us, const auto &buffer) {
client_->ObserveTransferOperation(TransferOperationKind::kRead,
"get_buffer", buffer->size(),
latency_us);
});
return get_buffer_internal(key, client_buffer_allocator_);
}
tl::expected<std::tuple<uint64_t, size_t>, ErrorCode>
@ -2261,14 +2137,7 @@ RealClient::batch_get_buffer_internal(
// Implementation of batch_get_buffer method
std::vector<std::shared_ptr<BufferHandle>> RealClient::batch_get_buffer(
const std::vector<std::string> &keys) {
return execute_timed_operation<std::vector<std::shared_ptr<BufferHandle>>>(
[&]() { return batch_get_buffer_internal(keys); },
[](const auto &) { return true; },
[&](uint64_t latency_us, const auto &buffers) {
client_->ObserveTransferOperation(
TransferOperationKind::kRead, "batch_get_buffer",
sum_buffer_handle_sizes(buffers), latency_us);
});
return batch_get_buffer_internal(keys);
}
tl::expected<void, ErrorCode> RealClient::register_buffer_internal(
@ -2423,17 +2292,7 @@ tl::expected<int64_t, ErrorCode> RealClient::get_into_range_internal(
int64_t RealClient::get_into(const std::string &key, void *buffer,
size_t size) {
auto result = execute_timed_operation<tl::expected<int64_t, ErrorCode>>(
[&]() {
return get_into_range_internal(key, buffer, 0, 0, size, true);
},
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &ret) {
client_->ObserveTransferOperation(
TransferOperationKind::kRead, "get_into",
static_cast<uint64_t>(ret.value()), latency_us);
});
return to_py_ret(result);
return to_py_ret(get_into_range_internal(key, buffer, 0, 0, size, true));
}
std::vector<std::vector<std::vector<tl::expected<int64_t, ErrorCode>>>>
@ -2553,20 +2412,8 @@ std::vector<std::vector<std::vector<int64_t>>> RealClient::get_into_ranges(
const std::vector<std::vector<std::vector<size_t>>> &all_dst_offsets,
const std::vector<std::vector<std::vector<size_t>>> &all_src_offsets,
const std::vector<std::vector<std::vector<size_t>>> &all_sizes) {
auto results =
execute_timed_operation<std::vector<std::vector<std::vector<int64_t>>>>(
[&]() {
return convert_ranged_read_results(
get_into_ranges_internal(buffers, all_keys, all_dst_offsets,
all_src_offsets, all_sizes));
},
[](const auto &) { return true; },
[&](uint64_t latency_us, const auto &ret) {
client_->ObserveTransferOperation(
TransferOperationKind::kRead, "get_into_ranges",
sum_positive_ranges(ret), latency_us);
});
return results;
return convert_ranged_read_results(get_into_ranges_internal(
buffers, all_keys, all_dst_offsets, all_src_offsets, all_sizes));
}
std::string RealClient::get_hostname() const { return local_hostname; }
@ -2575,21 +2422,7 @@ std::vector<int> RealClient::batch_put_from(
const std::vector<std::string> &keys, const std::vector<void *> &buffers,
const std::vector<size_t> &sizes, const ReplicateConfig &config) {
auto internal_results =
execute_timed_operation<std::vector<tl::expected<void, ErrorCode>>>(
[&]() {
return batch_put_from_internal(keys, buffers, sizes, config);
},
[](const auto &) { return true; },
[&](uint64_t latency_us, const auto &ret) {
std::vector<int> py_results;
py_results.reserve(ret.size());
for (const auto &item : ret) {
py_results.push_back(to_py_ret(item));
}
client_->ObserveTransferOperation(
TransferOperationKind::kWrite, "batch_put_from",
sum_successful_sizes(py_results, sizes), latency_us);
});
batch_put_from_internal(keys, buffers, sizes, config);
std::vector<int> results;
results.reserve(internal_results.size());
@ -2732,14 +2565,7 @@ tl::expected<void, ErrorCode> RealClient::put_from_internal(
int RealClient::put_from(const std::string &key, void *buffer, size_t size,
const ReplicateConfig &config) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() { return put_from_internal(key, buffer, size, config); },
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
client_->ObserveTransferOperation(TransferOperationKind::kWrite,
"put_from", size, latency_us);
});
return to_py_ret(result);
return to_py_ret(put_from_internal(key, buffer, size, config));
}
// --- Upsert implementations ---
@ -2780,18 +2606,8 @@ tl::expected<void, ErrorCode> RealClient::upsert_internal(
int RealClient::upsert(const std::string &key, std::span<const char> value,
const ReplicateConfig &config) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() {
return upsert_internal(key, value, config,
client_buffer_allocator_);
},
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
client_->ObserveTransferOperation(TransferOperationKind::kWrite,
"upsert", value.size_bytes(),
latency_us);
});
return to_py_ret(result);
return to_py_ret(
upsert_internal(key, value, config, client_buffer_allocator_));
}
tl::expected<void, ErrorCode> RealClient::upsert_dummy_helper(
@ -2841,14 +2657,7 @@ tl::expected<void, ErrorCode> RealClient::upsert_from_internal(
int RealClient::upsert_from(const std::string &key, void *buffer, size_t size,
const ReplicateConfig &config) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() { return upsert_from_internal(key, buffer, size, config); },
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
client_->ObserveTransferOperation(TransferOperationKind::kWrite,
"upsert_from", size, latency_us);
});
return to_py_ret(result);
return to_py_ret(upsert_from_internal(key, buffer, size, config));
}
std::vector<tl::expected<void, ErrorCode>>
@ -2894,21 +2703,7 @@ std::vector<int> RealClient::batch_upsert_from(
const std::vector<std::string> &keys, const std::vector<void *> &buffers,
const std::vector<size_t> &sizes, const ReplicateConfig &config) {
auto internal_results =
execute_timed_operation<std::vector<tl::expected<void, ErrorCode>>>(
[&]() {
return batch_upsert_from_internal(keys, buffers, sizes, config);
},
[](const auto &) { return true; },
[&](uint64_t latency_us, const auto &ret) {
std::vector<int> py_results;
py_results.reserve(ret.size());
for (const auto &item : ret) {
py_results.push_back(to_py_ret(item));
}
client_->ObserveTransferOperation(
TransferOperationKind::kWrite, "batch_upsert_from",
sum_successful_sizes(py_results, sizes), latency_us);
});
batch_upsert_from_internal(keys, buffers, sizes, config);
std::vector<int> results;
results.reserve(internal_results.size());
for (const auto &result : internal_results) {
@ -3032,18 +2827,8 @@ tl::expected<void, ErrorCode> RealClient::upsert_parts_internal(
int RealClient::upsert_parts(const std::string &key,
std::vector<std::span<const char>> values,
const ReplicateConfig &config) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() {
return upsert_parts_internal(key, values, config,
client_buffer_allocator_);
},
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
client_->ObserveTransferOperation(
TransferOperationKind::kWrite, "upsert_parts",
sum_value_sizes(values), latency_us);
});
return to_py_ret(result);
return to_py_ret(
upsert_parts_internal(key, values, config, client_buffer_allocator_));
}
tl::expected<void, ErrorCode> RealClient::upsert_parts_dummy_helper(
@ -3146,18 +2931,8 @@ tl::expected<void, ErrorCode> RealClient::upsert_batch_dummy_helper(
int RealClient::upsert_batch(const std::vector<std::string> &keys,
const std::vector<std::span<const char>> &values,
const ReplicateConfig &config) {
auto result = execute_timed_operation<tl::expected<void, ErrorCode>>(
[&]() {
return upsert_batch_internal(keys, values, config,
client_buffer_allocator_);
},
[](const auto &ret) { return ret.has_value(); },
[&](uint64_t latency_us, const auto &) {
client_->ObserveTransferOperation(
TransferOperationKind::kWrite, "upsert_batch",
sum_value_sizes(values), latency_us);
});
return to_py_ret(result);
return to_py_ret(
upsert_batch_internal(keys, values, config, client_buffer_allocator_));
}
// --- End Upsert implementations ---
@ -3165,20 +2940,7 @@ int RealClient::upsert_batch(const std::vector<std::string> &keys,
std::vector<int64_t> RealClient::batch_get_into(
const std::vector<std::string> &keys, const std::vector<void *> &buffers,
const std::vector<size_t> &sizes) {
auto internal_results =
execute_timed_operation<std::vector<tl::expected<int64_t, ErrorCode>>>(
[&]() { return batch_get_into_internal(keys, buffers, sizes); },
[](const auto &) { return true; },
[&](uint64_t latency_us, const auto &ret) {
std::vector<int64_t> py_results;
py_results.reserve(ret.size());
for (const auto &item : ret) {
py_results.push_back(to_py_ret(item));
}
client_->ObserveTransferOperation(
TransferOperationKind::kRead, "batch_get_into",
sum_positive_results(py_results), latency_us);
});
auto internal_results = batch_get_into_internal(keys, buffers, sizes);
std::vector<int64_t> results;
results.reserve(internal_results.size());
@ -3582,7 +3344,6 @@ int RealClient::put_from_with_metadata(const std::string &key, void *buffer,
void *metadata_buffer, size_t size,
size_t metadata_size,
const ReplicateConfig &config) {
const auto start_time = std::chrono::steady_clock::now();
// NOTE: The buffer address must be previously registered with
// register_buffer() for zero-copy RDMA operations to work correctly
if (config.prefer_alloc_in_same_node) {
@ -3625,10 +3386,6 @@ int RealClient::put_from_with_metadata(const std::string &key, void *buffer,
<< toString(put_result.error());
return -toInt(put_result.error());
}
client_->ObserveTransferOperation(
TransferOperationKind::kWrite, "put_from_with_metadata",
size + metadata_size, elapsed_us_since(start_time));
return 0;
}
@ -3637,24 +3394,10 @@ std::vector<int> RealClient::batch_put_from_multi_buffers(
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &sizes,
const ReplicateConfig &config) {
auto start = std::chrono::steady_clock::now();
auto internal_results =
execute_timed_operation<std::vector<tl::expected<void, ErrorCode>>>(
[&]() {
return batch_put_from_multi_buffers_internal(keys, all_buffers,
sizes, config);
},
[](const auto &) { return true; },
[&](uint64_t latency_us, const auto &ret) {
std::vector<int> py_results;
py_results.reserve(ret.size());
for (const auto &item : ret) {
py_results.push_back(to_py_ret(item));
}
client_->ObserveTransferOperation(
TransferOperationKind::kWrite,
"batch_put_from_multi_buffers",
sum_successful_nested_sizes(py_results, sizes), latency_us);
});
batch_put_from_multi_buffers_internal(keys, all_buffers, sizes, config);
std::vector<int> results;
results.reserve(internal_results.size());
@ -3662,6 +3405,10 @@ std::vector<int> RealClient::batch_put_from_multi_buffers(
results.push_back(to_py_ret(result));
}
auto duration_call = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start);
VLOG(1) << "batch_put_from_multi_buffers: " << duration_call.count()
<< " us";
return results;
}
@ -3707,30 +3454,19 @@ std::vector<int> RealClient::batch_get_into_multi_buffers(
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
bool prefer_alloc_in_same_node) {
auto internal_results =
execute_timed_operation<std::vector<tl::expected<int64_t, ErrorCode>>>(
[&]() {
return batch_get_into_multi_buffers_internal(
keys, all_buffers, all_sizes, prefer_alloc_in_same_node);
},
[](const auto &) { return true; },
[&](uint64_t latency_us, const auto &ret) {
std::vector<int> py_results;
py_results.reserve(ret.size());
for (const auto &item : ret) {
py_results.push_back(to_py_ret(item));
}
client_->ObserveTransferOperation(
TransferOperationKind::kRead,
"batch_get_into_multi_buffers",
sum_positive_results(py_results), latency_us);
});
auto start = std::chrono::steady_clock::now();
auto internal_results = batch_get_into_multi_buffers_internal(
keys, all_buffers, all_sizes, prefer_alloc_in_same_node);
std::vector<int> results;
results.reserve(internal_results.size());
for (const auto &result : internal_results) {
results.push_back(to_py_ret(result));
}
auto duration_call = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start);
VLOG(1) << "batch_get_into_multi_buffers: " << duration_call.count()
<< " us";
return results;
}
@ -4156,21 +3892,6 @@ RealClient::batch_get_replica_desc(const std::vector<std::string> &keys) {
return replica_map;
}
std::vector<std::string> RealClient::batch_replica_clear(
const std::vector<std::string> &keys, const std::string &segment_name) {
if (!client_) {
LOG(ERROR) << "batch_replica_clear: client not initialized";
return {};
}
auto result =
client_->BatchReplicaClear(keys, client_->getClientId(), segment_name);
if (result) {
return result.value();
}
LOG(ERROR) << "batch_replica_clear failed: " << toString(result.error());
return {};
}
tl::expected<UUID, ErrorCode> RealClient::create_copy_task(
const std::string &key, const std::vector<std::string> &targets) {
return client_->CreateCopyTask(key, targets);
@ -4266,18 +3987,6 @@ ClientRequester::ClientRequester() {
pool_conf.client_config.socket_config =
coro_io::ib_socket_t::config_t{};
}
// Configure reasonable retry limits for SSD offload RPC connections.
// - connect_retry_count: Maximum connection retry attempts (default: 3)
// - reconnect_wait_time: Wait time between retries (default: 1000ms)
// - host_alive_detect_duration: Duration for background alive detection.
// Set to 0 to disable infinite background reconnection attempts when
// a Store node goes down. This prevents continuous "Connection refused"
// logs. When Master cleans up stale local_disk replicas (via
// CleanupStaleHandles), new requests won't route to dead nodes anyway.
pool_conf.connect_retry_count = 3;
pool_conf.reconnect_wait_time = std::chrono::milliseconds{1000};
pool_conf.host_alive_detect_duration = std::chrono::milliseconds{0};
client_pools_ =
std::make_shared<coro_io::client_pools<coro_rpc::coro_rpc_client>>(
pool_conf);

View File

@ -91,10 +91,6 @@ int main(int argc, char *argv[]) {
mooncake::ResourceTracker::getInstance();
gflags::ParseCommandLineFlags(&argc, &argv, true);
if (!FLAGS_log_dir.empty()) {
google::InitGoogleLogging(argv[0]);
}
size_t global_segment_size = string_to_byte_size(FLAGS_global_segment_size);
#ifdef USE_ASCEND_DIRECT
// just set to true, does not affect GPU process.

View File

@ -273,15 +273,6 @@ ErrorCode ScopedSegmentAccess::GetClientSegments(
return ErrorCode::OK;
}
void ScopedSegmentAccess::UnmountLocalDiskSegment(const UUID& client_id) {
auto it = segment_manager_->client_local_disk_segment_.find(client_id);
if (it != segment_manager_->client_local_disk_segment_.end()) {
segment_manager_->client_local_disk_segment_.erase(it);
LOG(INFO) << "client_id=" << client_id
<< ", action=unmount_local_disk_segment";
}
}
ErrorCode ScopedSegmentAccess::GetAllSegments(
std::vector<std::string>& all_segments) {
all_segments.clear();

View File

@ -49,9 +49,8 @@ FilePerKeyConfig FilePerKeyConfig::FromEnvironment() {
config.fsdir = GetEnvStringOr("MOONCAKE_OFFLOAD_FSDIR", config.fsdir);
config.enable_eviction = GetEnvOr<bool>(
"MOONCAKE_OFFLOAD_ENABLE_EVICTION",
GetEnvOr<bool>("ENABLE_EVICTION", config.enable_eviction));
config.enable_eviction =
GetEnvOr<bool>("ENABLE_EVICTION", config.enable_eviction);
return config;
}
@ -65,14 +64,11 @@ BucketBackendConfig BucketBackendConfig::FromEnvironment() {
config.bucket_size_limit = GetEnvOr<int64_t>(
"MOONCAKE_OFFLOAD_BUCKET_SIZE_LIMIT_BYTES", config.bucket_size_limit);
config.max_total_size =
GetEnvOr<int64_t>("MOONCAKE_OFFLOAD_BUCKET_MAX_TOTAL_SIZE",
GetEnvOr<int64_t>("MOONCAKE_BUCKET_MAX_TOTAL_SIZE",
config.max_total_size));
config.max_total_size = GetEnvOr<int64_t>("MOONCAKE_BUCKET_MAX_TOTAL_SIZE",
config.max_total_size);
const auto policy_str = GetEnvStringOr(
"MOONCAKE_OFFLOAD_BUCKET_EVICTION_POLICY",
GetEnvStringOr("MOONCAKE_BUCKET_EVICTION_POLICY", "none"));
const auto policy_str =
GetEnvStringOr("MOONCAKE_BUCKET_EVICTION_POLICY", "none");
if (policy_str == "fifo") {
config.eviction_policy = BucketEvictionPolicy::FIFO;
} else if (policy_str == "lru") {
@ -1722,18 +1718,6 @@ tl::expected<void, ErrorCode> BucketStorageBackend::Init() {
<< orphaned_space_freed << " bytes";
}
// When max_total_size is not explicitly set (<= 0), default to 90% of
// the physical disk capacity to match FilePerKey backend behavior.
if (bucket_backend_config_.max_total_size <= 0) {
constexpr double kDefaultQuotaPercentage = 0.9;
const auto space_info = fs::space(storage_path_);
bucket_backend_config_.max_total_size = static_cast<int64_t>(
space_info.capacity * kDefaultQuotaPercentage);
LOG(INFO) << "Bucket backend max_total_size not set; using "
<< kDefaultQuotaPercentage * 100 << "% of disk capacity: "
<< bucket_backend_config_.max_total_size << " bytes";
}
bucket_id_generator_.emplace(max_bucket_id);
if (max_bucket_id == BucketIdGenerator::INIT_NEW_START_ID) {
LOG(INFO) << "Initialized BucketIdGenerator with fresh start. "
@ -2203,7 +2187,8 @@ BucketStorageBackend::PendingEviction BucketStorageBackend::PrepareEviction(
int64_t required_size) {
PendingEviction result;
if (bucket_backend_config_.eviction_policy == BucketEvictionPolicy::NONE) {
if (bucket_backend_config_.eviction_policy == BucketEvictionPolicy::NONE ||
bucket_backend_config_.max_total_size <= 0) {
return result;
}

View File

@ -4,8 +4,6 @@
#include <algorithm>
#include <cstdlib>
#include <sstream>
#include <vector>
#include "transfer_engine.h"
#include "transport/transport.h"
@ -225,13 +223,9 @@ bool TransferEngineOperationState::is_completed() {
}
void TransferEngineOperationState::check_task_status() {
// Check all transfers in the batch.
// Wait for ALL tasks to reach a terminal state before setting the result,
// even if some have already failed. This prevents the caller from seeing
// "completed" while background transfers are still in progress, which
// could cause issues when freeBatchID is called in the destructor.
bool all_terminated = true;
std::vector<size_t> failed_task_ids;
// Check all transfers in the batch
bool all_completed = true;
bool has_failure = false;
for (size_t i = 0; i < batch_size_; ++i) {
TransferStatus status;
@ -246,45 +240,38 @@ void TransferEngineOperationState::check_task_status() {
switch (status.s) {
case TransferStatusEnum::COMPLETED:
// This transfer is done successfully
// This transfer is done, continue checking others
break;
case TransferStatusEnum::FAILED:
case TransferStatusEnum::CANCELED:
case TransferStatusEnum::INVALID:
#ifndef USE_ASCEND_DIRECT
VLOG(1) << "Transfer failed for batch " << batch_id_ << " task "
<< i << " with status " << static_cast<int>(status.s);
LOG(ERROR) << "Transfer failed for batch " << batch_id_
<< " task " << i << " with status "
<< static_cast<int>(status.s);
#endif
failed_task_ids.push_back(i);
has_failure = true;
break;
default:
// Transfer is still in progress (WAITING, PENDING, etc.)
all_terminated = false;
// Transfer is still pending (PENDING, RUNNING, etc.)
all_completed = false;
break;
}
}
if (!all_terminated) {
// Some tasks are still in progress; wait for next poll iteration.
// Do NOT set result yet, even if some tasks have already failed.
if (has_failure) {
VLOG(1) << "Setting batch " << batch_id_
<< " result to TRANSFER_FAIL due to task failures";
set_result_internal(ErrorCode::TRANSFER_FAIL);
return;
}
// All tasks have reached a terminal state.
ErrorCode ec = ErrorCode::OK;
if (!failed_task_ids.empty()) {
std::ostringstream oss;
for (size_t j = 0; j < failed_task_ids.size(); ++j) {
if (j > 0) oss << ", ";
oss << failed_task_ids[j];
}
LOG(ERROR) << "Batch " << batch_id_
<< " completed with task failures: task_ids=[" << oss.str()
<< "]";
ec = ErrorCode::TRANSFER_FAIL;
if (all_completed) {
set_result_internal(ErrorCode::OK);
return;
}
set_result_internal(ec);
return;
}
void TransferEngineOperationState::set_result_internal(ErrorCode error_code) {
@ -307,8 +294,7 @@ void TransferEngineOperationState::wait_for_completion() {
return;
}
// 60 seconds
constexpr int64_t timeout_milliseconds = 60 * 1000;
constexpr int64_t timeout_seconds = 60;
#ifdef USE_EVENT_DRIVEN_COMPLETION
VLOG(1) << "Waiting for transfer engine completion for batch " << batch_id_;
@ -328,18 +314,10 @@ void TransferEngineOperationState::wait_for_completion() {
// lock. Under the mutex, relaxed is sufficient; the mutex acquire
// orders prior writes.
std::unique_lock<std::mutex> lock(batch_desc.completion_mutex);
const int64_t elapsed_milliseconds =
getCurrentTimeInMilli() - start_ts_;
if (elapsed_milliseconds < timeout_milliseconds) {
completed = batch_desc.completion_cv.wait_for(
lock,
std::chrono::milliseconds(timeout_milliseconds -
elapsed_milliseconds),
[&batch_desc] {
return batch_desc.is_finished.load(
std::memory_order_relaxed);
});
}
completed = batch_desc.completion_cv.wait_for(
lock, std::chrono::seconds(timeout_seconds), [&batch_desc] {
return batch_desc.is_finished.load(std::memory_order_relaxed);
});
} // Explicitly release completion_mutex before acquiring mutex_
// Once completion is observed, read failure flag.
@ -360,18 +338,20 @@ void TransferEngineOperationState::wait_for_completion() {
VLOG(1) << "Transfer engine operation completed for batch " << batch_id_
<< " with result: " << static_cast<int>(error_code);
} else {
LOG(ERROR) << "Failed to complete transfers after "
<< timeout_milliseconds << " milliseconds for batch "
<< batch_id_;
LOG(ERROR) << "Failed to complete transfers after " << timeout_seconds
<< " seconds for batch " << batch_id_;
}
#else
VLOG(1) << "Starting transfer engine polling for batch " << batch_id_;
constexpr int64_t kOneSecondInNano = 1000 * 1000 * 1000;
const int64_t start_ts = getCurrentTimeInNano();
while (true) {
if (getCurrentTimeInMilli() - start_ts_ > timeout_milliseconds) {
if (getCurrentTimeInNano() - start_ts >
timeout_seconds * kOneSecondInNano) {
LOG(ERROR) << "Failed to complete transfers after "
<< timeout_milliseconds << " milliseconds for batch "
<< batch_id_;
<< timeout_seconds << " seconds for batch " << batch_id_;
set_result_internal(ErrorCode::TRANSFER_FAIL);
return;
}
@ -429,17 +409,10 @@ TransferSubmitter::TransferSubmitter(TransferEngine& engine,
memcpy_pool_(std::make_unique<MemcpyWorkerPool>()),
fileread_pool_(std::make_unique<FilereadWorkerPool>(backend)),
transfer_metric_(transfer_metric) {
// Read MC_STORE_MEMCPY environment variable.
// When not set, auto-detect based on transport type:
// - TCP-only environment: enable memcpy (avoids TCP loopback overhead)
// - RDMA/other transports: disable memcpy (RDMA is more efficient)
// Read MC_STORE_MEMCPY environment variable, default to false (disabled)
const char* env_value = std::getenv("MC_STORE_MEMCPY");
if (env_value == nullptr) {
memcpy_enabled_ = engine_.isTcpOnly();
LOG(INFO) << "MC_STORE_MEMCPY not set, auto-detected: "
<< (memcpy_enabled_ ? "TCP-only environment, memcpy enabled"
: "non-TCP transport available, memcpy "
"disabled");
memcpy_enabled_ = false; // Default: disabled
} else {
std::string env_str(env_value);
// Convert to lowercase for case-insensitive comparison

View File

@ -39,7 +39,6 @@ add_store_test(eviction_strategy_test eviction_strategy_test.cpp)
add_store_test(master_service_test master_service_test.cpp)
add_store_test(batch_remove_test batch_remove_test.cpp)
add_store_test(master_service_ssd_test master_service_ssd_test.cpp)
add_store_test(offload_on_evict_test offload_on_evict_test.cpp)
add_store_test(master_service_ssd_test_for_snapshot
ha/snapshot/master_service_ssd_test_for_snapshot.cpp)
add_store_test(client_integration_test client_integration_test.cpp)

View File

@ -45,8 +45,6 @@ TEST_F(ClientMetricsTest, TransferMetricsSummaryTest) {
// Check byte formatting
EXPECT_TRUE(summary.find("Total Read: 1.00 KB") != std::string::npos);
EXPECT_TRUE(summary.find("Total Write: 2.00 MB") != std::string::npos);
EXPECT_TRUE(summary.find("Average Read Throughput:") != std::string::npos);
EXPECT_TRUE(summary.find("Average Write Throughput:") != std::string::npos);
// Check latency summaries
EXPECT_TRUE(summary.find("Get: count=3") != std::string::npos);
@ -111,23 +109,15 @@ TEST_F(ClientMetricsTest, ClientMetricsSummaryTest) {
std::array<std::string, 1> exist_key_label = {"ExistKey"};
metrics.master_client_metric.rpc_count.inc(exist_key_label);
metrics.master_client_metric.rpc_latency.observe(exist_key_label, 180);
metrics.ObserveTransferOperation(TransferOperationKind::kRead, "get_buffer",
2 * 1024, 220);
metrics.ObserveTransferOperation(TransferOperationKind::kWrite, "put_batch",
4 * 1024, 420);
std::string summary = metrics.summary_metrics();
// Should contain transfer, RPC, and interface metrics
// Should contain both transfer and RPC metrics
EXPECT_TRUE(summary.find("Transfer Metrics Summary") != std::string::npos);
EXPECT_TRUE(summary.find("RPC Metrics Summary") != std::string::npos);
EXPECT_TRUE(summary.find("Interface Operation Metrics Summary") !=
std::string::npos);
EXPECT_TRUE(summary.find("Total Read: 5.00 MB") != std::string::npos);
EXPECT_TRUE(summary.find("Total Write: 10.00 MB") != std::string::npos);
EXPECT_TRUE(summary.find("ExistKey: count=1") != std::string::npos);
EXPECT_TRUE(summary.find("get_buffer: count=1") != std::string::npos);
EXPECT_TRUE(summary.find("put_batch: count=1") != std::string::npos);
std::cout << "Full Client Metrics Summary:\n" << summary << std::endl;
}
@ -185,33 +175,6 @@ TEST_F(ClientMetricsTest, CompareWithSerializedMetrics) {
summary.find("No data") != std::string::npos);
}
TEST_F(ClientMetricsTest, BandwidthSummaryRespectsEnvFlag) {
setenv("MC_STORE_CLIENT_METRIC_BANDWIDTH", "0", 1);
auto metrics = ClientMetric::Create();
ASSERT_NE(metrics, nullptr);
metrics->transfer_metric.total_read_bytes.inc(1024);
std::string summary = metrics->summary_metrics();
EXPECT_TRUE(summary.find("Average Read Throughput:") == std::string::npos);
unsetenv("MC_STORE_CLIENT_METRIC_BANDWIDTH");
}
TEST_F(ClientMetricsTest, SummaryCanOmitMasterRpcMetrics) {
auto metrics = ClientMetric::Create({}, false);
ASSERT_NE(metrics, nullptr);
metrics->ObserveTransferOperation(TransferOperationKind::kRead,
"get_buffer", 1024, 200);
std::string summary = metrics->summary_metrics();
std::string serialized;
metrics->serialize(serialized);
EXPECT_TRUE(summary.find("RPC Metrics Summary") == std::string::npos);
EXPECT_TRUE(serialized.find("mooncake_client_rpc_count") ==
std::string::npos);
}
TEST_F(ClientMetricsTest, SerializeWithDynamicLabels) {
auto verify = [](const std::string& str) {
EXPECT_TRUE(str.find("instance_id=\"12345\"") != std::string::npos);

View File

@ -26,8 +26,7 @@ class FileStorageTest : public ::testing::Test {
FLAGS_logtostderr = true;
UnsetEnv("MOONCAKE_OFFLOAD_FILE_STORAGE_PATH");
UnsetEnv("MOONCAKE_OFFLOAD_LOCAL_BUFFER_SIZE_BYTES");
UnsetEnv("MOONCAKE_OFFLOAD_SCANMETA_ITERATOR_KEYS_LIMIT");
UnsetEnv("MOONCAKE_SCANMETA_ITERATOR_KEYS_LIMIT");
UnsetEnv("MOONCAKE_OFFLOAD_BUCKET_ITERATOR_KEYS_LIMIT");
UnsetEnv("MOONCAKE_OFFLOAD_BUCKET_KEYS_LIMIT");
UnsetEnv("MOONCAKE_OFFLOAD_BUCKET_SIZE_LIMIT_BYTES");
UnsetEnv("MOONCAKE_OFFLOAD_TOTAL_KEYS_LIMIT");

View File

@ -1,388 +0,0 @@
#include "master_service.h"
#include <glog/logging.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include "types.h"
namespace mooncake::test {
class OffloadOnEvictTest : public ::testing::Test {
protected:
void SetUp() override {
google::InitGoogleLogging("OffloadOnEvictTest");
FLAGS_logtostderr = true;
}
void TearDown() override { google::ShutdownGoogleLogging(); }
static constexpr size_t kDefaultSegmentBase = 0x300000000;
Segment MakeSegment(std::string name, size_t base, size_t size) const {
Segment segment;
segment.id = generate_uuid();
segment.name = std::move(name);
segment.base = base;
segment.size = size;
segment.te_endpoint = segment.name;
return segment;
}
struct MountedSegmentContext {
UUID segment_id;
UUID client_id;
};
MountedSegmentContext PrepareSegment(MasterService& service,
std::string name, size_t base,
size_t size) const {
Segment segment = MakeSegment(std::move(name), base, size);
UUID client_id = generate_uuid();
auto mount_result = service.MountSegment(segment, client_id);
EXPECT_TRUE(mount_result.has_value());
return {.segment_id = segment.id, .client_id = client_id};
}
// Put an object and complete it.
void PutObject(MasterService& service, const UUID& client_id,
const std::string& key, size_t size = 1024) {
ReplicateConfig config;
config.replica_num = 1;
auto put_start = service.PutStart(client_id, key, size, config);
ASSERT_TRUE(put_start.has_value()) << "PutStart failed for key=" << key;
auto put_end = service.PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value()) << "PutEnd failed for key=" << key;
}
// Drain the offload queue via OffloadObjectHeartbeat.
std::unordered_map<std::string, int64_t> DrainOffloadQueue(
MasterService& service, const UUID& client_id) {
auto res = service.OffloadObjectHeartbeat(client_id, true);
if (!res) {
return {};
}
return std::move(res.value());
}
template <typename Predicate>
void WaitUntil(
Predicate&& predicate,
std::chrono::milliseconds timeout = std::chrono::milliseconds(4000),
std::chrono::milliseconds interval =
std::chrono::milliseconds(50)) const {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
if (predicate()) {
return;
}
std::this_thread::sleep_for(interval);
}
EXPECT_TRUE(predicate());
}
// Fill a segment until PutStart fails, triggering eviction.
// Returns the number of successful puts.
int FillSegmentUntilEviction(MasterService& service, const UUID& client_id,
const std::string& key_prefix,
size_t object_size, int max_puts) {
int success_puts = 0;
for (int i = 0; i < max_puts; ++i) {
std::string key = key_prefix + std::to_string(i);
ReplicateConfig config;
config.replica_num = 1;
auto result = service.PutStart(client_id, key, object_size, config);
if (result.has_value()) {
auto end = service.PutEnd(client_id, key, ReplicaType::MEMORY);
EXPECT_TRUE(end.has_value());
success_puts++;
} else {
// Wait for eviction to process
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
return success_puts;
}
};
// =============================================================================
// Combo A: Default config (offload at PutEnd)
// =============================================================================
TEST_F(OffloadOnEvictTest, ComboA_OffloadAtPutEnd) {
MasterServiceConfig config;
config.enable_offload = true;
config.default_kv_lease_ttl = 2000;
auto service = std::make_unique<MasterService>(config);
constexpr size_t seg_size = 1024 * 1024 * 16;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
// Mount local disk segment with offloading ENABLED
auto mount_ld = service->MountLocalDiskSegment(ctx.client_id, true);
ASSERT_TRUE(mount_ld.has_value());
// Put objects
PutObject(*service, ctx.client_id, "key_a1");
PutObject(*service, ctx.client_id, "key_a2");
PutObject(*service, ctx.client_id, "key_a3");
// Default mode: PutEnd pushes to offload queue immediately
auto queued = DrainOffloadQueue(*service, ctx.client_id);
EXPECT_EQ(queued.size(), 3u)
<< "Default: all 3 objects should be in offload queue after PutEnd";
EXPECT_TRUE(queued.count("key_a1"));
EXPECT_TRUE(queued.count("key_a2"));
EXPECT_TRUE(queued.count("key_a3"));
service->RemoveAll();
}
TEST_F(OffloadOnEvictTest, ComboA_EvictionWorks) {
// Regression: eviction still works in default mode
const uint64_t kv_lease_ttl = 2000;
MasterServiceConfig config;
config.enable_offload = true;
config.default_kv_lease_ttl = kv_lease_ttl;
auto service = std::make_unique<MasterService>(config);
// Large segment: can hold ~16K objects of 15KB
constexpr size_t seg_size = 1024 * 1024 * 16 * 15;
constexpr size_t object_size = 1024 * 15;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
// Put more objects than the segment can hold
int success_puts = FillSegmentUntilEviction(
*service, ctx.client_id, "evict_a_", object_size, 1024 * 16 + 50);
EXPECT_GT(success_puts, 1024 * 16)
<< "Default: eviction should allow more puts than capacity";
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service->RemoveAll();
}
// =============================================================================
// Combo B: offload_on_evict=true (offload on evict, no force-evict)
// =============================================================================
TEST_F(OffloadOnEvictTest, ComboB_PutEndSkipsOffloadQueue) {
MasterServiceConfig config;
config.enable_offload = true;
config.offload_on_evict = true;
config.default_kv_lease_ttl = 2000;
auto service = std::make_unique<MasterService>(config);
constexpr size_t seg_size = 1024 * 1024 * 16;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
auto mount_ld = service->MountLocalDiskSegment(ctx.client_id, true);
ASSERT_TRUE(mount_ld.has_value());
PutObject(*service, ctx.client_id, "key_b1");
PutObject(*service, ctx.client_id, "key_b2");
PutObject(*service, ctx.client_id, "key_b3");
// Offload-on-evict: PutEnd should NOT push to offload queue
auto queued = DrainOffloadQueue(*service, ctx.client_id);
EXPECT_EQ(queued.size(), 0u)
<< "Offload-on-evict: queue should be empty after PutEnd";
service->RemoveAll();
}
TEST_F(OffloadOnEvictTest, ComboB_EvictionTriggersOffload) {
const uint64_t kv_lease_ttl = 2000;
MasterServiceConfig config;
config.enable_offload = true;
config.offload_on_evict = true;
config.default_kv_lease_ttl = kv_lease_ttl;
auto service = std::make_unique<MasterService>(config);
constexpr size_t seg_size = 1024 * 1024 * 16 * 15;
constexpr size_t object_size = 1024 * 15;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
auto mount_ld = service->MountLocalDiskSegment(ctx.client_id, true);
ASSERT_TRUE(mount_ld.has_value());
// Fill segment to trigger eviction
bool eviction_triggered = false;
int success_puts = 0;
for (int i = 0; i < 1024 * 16 + 50; ++i) {
std::string key = "evict_b_" + std::to_string(i);
ReplicateConfig config;
config.replica_num = 1;
auto result =
service->PutStart(ctx.client_id, key, object_size, config);
if (result.has_value()) {
auto end = service->PutEnd(ctx.client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(end.has_value());
success_puts++;
} else {
eviction_triggered = true;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
EXPECT_TRUE(eviction_triggered)
<< "Eviction should trigger when segment fills up";
// Offload-on-evict: eviction should push objects to offload queue
auto queued = DrainOffloadQueue(*service, ctx.client_id);
EXPECT_GT(queued.size(), 0u)
<< "Offload-on-evict: eviction should push to offload queue";
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service->RemoveAll();
}
TEST_F(OffloadOnEvictTest, ComboB_NoFallbackWithoutForceEvict) {
// Without force_evict AND without a LocalDiskSegment, offload queue push
// fails and eviction does NOT force-delete MEMORY (data-preserving).
// The segment fills and subsequent puts fail — this is the safe default.
const uint64_t kv_lease_ttl = 2000;
MasterServiceConfig config;
config.enable_offload = true;
config.offload_on_evict = true;
config.default_kv_lease_ttl = kv_lease_ttl;
auto service = std::make_unique<MasterService>(config);
// NO local disk segment mounted — PushOffloadingQueue will fail
constexpr size_t seg_size = 1024 * 1024 * 16 * 15;
constexpr size_t object_size = 1024 * 15;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
// Without force_evict, push failures mean DRAM cannot be freed,
// so we can only put up to segment capacity (no overflow).
int success_puts = FillSegmentUntilEviction(
*service, ctx.client_id, "evict_b2_", object_size, 1024 * 16 + 50);
EXPECT_LE(success_puts, 1024 * 16)
<< "Without force_evict, segment should fill and stay full";
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service->RemoveAll();
}
// =============================================================================
// Combo C: offload_on_evict=true + offload_force_evict=true
// =============================================================================
TEST_F(OffloadOnEvictTest, ComboC_PutEndSkipsOffloadQueue) {
MasterServiceConfig config;
config.enable_offload = true;
config.offload_on_evict = true;
config.offload_force_evict = true;
config.default_kv_lease_ttl = 2000;
auto service = std::make_unique<MasterService>(config);
constexpr size_t seg_size = 1024 * 1024 * 16;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
auto mount_ld = service->MountLocalDiskSegment(ctx.client_id, true);
ASSERT_TRUE(mount_ld.has_value());
PutObject(*service, ctx.client_id, "key_c1");
PutObject(*service, ctx.client_id, "key_c2");
// Same as Combo B: PutEnd should skip offload queue
auto queued = DrainOffloadQueue(*service, ctx.client_id);
EXPECT_EQ(queued.size(), 0u)
<< "Combo C: offload queue should be empty after PutEnd";
service->RemoveAll();
}
TEST_F(OffloadOnEvictTest, ComboC_EvictionWithForceEvict) {
const uint64_t kv_lease_ttl = 2000;
MasterServiceConfig config;
config.enable_offload = true;
config.offload_on_evict = true;
config.offload_force_evict = true;
config.default_kv_lease_ttl = kv_lease_ttl;
auto service = std::make_unique<MasterService>(config);
constexpr size_t seg_size = 1024 * 1024 * 16 * 15;
constexpr size_t object_size = 1024 * 15;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
auto mount_ld = service->MountLocalDiskSegment(ctx.client_id, true);
ASSERT_TRUE(mount_ld.has_value());
// With force-evict, eviction should work effectively.
// Note: without a real FileStorage heartbeat, offloaded objects' refcnt
// never decreases, so DRAM isn't fully freed beyond what direct eviction
// allows. We verify eviction doesn't deadlock (can fill to capacity).
int success_puts = FillSegmentUntilEviction(
*service, ctx.client_id, "evict_c_", object_size, 1024 * 16 + 50);
EXPECT_GE(success_puts, 1024 * 16)
<< "Combo C: eviction should work with force-evict enabled";
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service->RemoveAll();
}
// =============================================================================
// Combo D: offload_force_evict=true only (should be no-op without on_evict)
// =============================================================================
TEST_F(OffloadOnEvictTest, ComboD_ForceEvictAloneIsIgnored) {
MasterServiceConfig config;
config.enable_offload = true;
config.offload_force_evict = true; // on_evict is false → force is ignored
config.default_kv_lease_ttl = 2000;
auto service = std::make_unique<MasterService>(config);
constexpr size_t seg_size = 1024 * 1024 * 16;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
auto mount_ld = service->MountLocalDiskSegment(ctx.client_id, true);
ASSERT_TRUE(mount_ld.has_value());
// Should behave like Combo A (default: offload at PutEnd)
PutObject(*service, ctx.client_id, "key_d1");
PutObject(*service, ctx.client_id, "key_d2");
auto queued = DrainOffloadQueue(*service, ctx.client_id);
EXPECT_EQ(queued.size(), 2u)
<< "Combo D: FORCE_EVICT alone should not change default behavior";
service->RemoveAll();
}
TEST_F(OffloadOnEvictTest, ComboD_EvictionWorks) {
const uint64_t kv_lease_ttl = 2000;
MasterServiceConfig config;
config.enable_offload = true;
config.offload_force_evict = true; // on_evict is false → force is ignored
config.default_kv_lease_ttl = kv_lease_ttl;
auto service = std::make_unique<MasterService>(config);
constexpr size_t seg_size = 1024 * 1024 * 16 * 15;
constexpr size_t object_size = 1024 * 15;
auto ctx =
PrepareSegment(*service, "test_segment", kDefaultSegmentBase, seg_size);
int success_puts = FillSegmentUntilEviction(
*service, ctx.client_id, "evict_d_", object_size, 1024 * 16 + 50);
EXPECT_GT(success_puts, 1024 * 16)
<< "Combo D: eviction should work normally";
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service->RemoveAll();
}
} // namespace mooncake::test
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -9,11 +9,10 @@ if (NOT GLOBAL_CONFIG)
include(../mooncake-common/FindGLOG.cmake)
include(../mooncake-common/common.cmake)
# Build asio_shared in mooncake-common for standalone builds.
# Set compile definitions before adding targets that include ASIO headers.
# Build asio_shared library for standalone build
# Set compile definitions before add_subdirectory to ensure proper compilation
add_compile_definitions(ASIO_SEPARATE_COMPILATION ASIO_DYN_LINK)
add_subdirectory(../mooncake-common/src
${CMAKE_CURRENT_BINARY_DIR}/mooncake-common-src)
add_subdirectory(../mooncake-asio ${CMAKE_CURRENT_BINARY_DIR}/mooncake-asio)
endif() # GLOBAL_CONFIG
if (USE_ASCEND)

View File

@ -17,4 +17,4 @@ endif()
# Set RPATH for finding libasio.so at runtime
set_target_properties(
tebench PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE
INSTALL_RPATH "$ORIGIN/../lib:$ORIGIN/../../mooncake-common")
INSTALL_RPATH "$ORIGIN/../lib:$ORIGIN/../../mooncake-asio")

View File

@ -1,357 +0,0 @@
#!/usr/bin/env python3
"""
Batch Memory Registration Benchmark for Mooncake EFA Transport.
Tests registering multiple independent memory blocks (simulating
multi-tenant or sharded KV cache pools) and measures registration
time and transfer throughput across blocks.
Usage:
# Target node (registers memory blocks):
python batch_register_bench.py --mode target \
--local_server_name <target_ip>:12345 \
--num_blocks 40 --block_size_gb 4 --protocol efa
# Initiator node (pulls data from random blocks):
python batch_register_bench.py --mode initiator \
--local_server_name <initiator_ip>:12346 \
--target_server_name <target_ip>:12345 \
--num_blocks 40 --block_size_gb 4 --protocol efa
Requires: mooncake Python package (pip install -e mooncake-wheel)
"""
import argparse
import ctypes
import ctypes.util
import json
import os
import random
import signal
import statistics
import sys
import time
def parse_args():
parser = argparse.ArgumentParser(
description="Batch Memory Registration Benchmark"
)
parser.add_argument(
"--mode",
choices=["target", "initiator"],
required=True,
help="Run as target (memory holder) or initiator (puller)",
)
parser.add_argument(
"--local_server_name",
required=True,
help="Local address, e.g. 172.31.6.162:12345",
)
parser.add_argument(
"--target_server_name",
default="",
help="Target address (initiator mode only)",
)
parser.add_argument(
"--metadata_server",
default="P2PHANDSHAKE",
help="Metadata server address (default: P2PHANDSHAKE)",
)
parser.add_argument(
"--protocol", default="efa", help="Transport protocol (default: efa)"
)
parser.add_argument(
"--num_blocks",
type=int,
default=40,
help="Number of memory blocks to register (default: 40)",
)
parser.add_argument(
"--block_size_gb",
type=float,
default=4.0,
help="Size of each memory block in GB (default: 4)",
)
parser.add_argument(
"--transfer_size_mb",
type=float,
default=368.0,
help="Transfer size in MB per iteration (default: 368, ~4K tokens GLM-5.1)",
)
parser.add_argument(
"--iterations",
type=int,
default=20,
help="Number of transfer iterations (default: 20)",
)
parser.add_argument(
"--warmup",
type=int,
default=5,
help="Number of warmup iterations (default: 5)",
)
parser.add_argument(
"--use_batch_api",
action="store_true",
help="Use batch_register_memory API instead of per-block register_memory",
)
return parser.parse_args()
def allocate_block(size_bytes):
"""Allocate a single page-aligned memory block using mmap."""
libc_name = ctypes.util.find_library("c")
libc = ctypes.CDLL(libc_name, use_errno=True)
PROT_READ = 0x1
PROT_WRITE = 0x2
MAP_PRIVATE = 0x02
MAP_ANONYMOUS = 0x20
MAP_HUGETLB = 0x40000
MAP_FAILED = ctypes.c_void_p(-1).value
libc.mmap.restype = ctypes.c_void_p
libc.mmap.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int,
ctypes.c_int, ctypes.c_int, ctypes.c_long,
]
flags = MAP_PRIVATE | MAP_ANONYMOUS
# Try hugepages first
ptr = libc.mmap(None, size_bytes, PROT_READ | PROT_WRITE,
flags | MAP_HUGETLB, -1, 0)
if ptr and ptr != MAP_FAILED:
return ptr, True
# Fall back to regular pages
ptr = libc.mmap(None, size_bytes, PROT_READ | PROT_WRITE, flags, -1, 0)
if not ptr or ptr == MAP_FAILED:
raise RuntimeError(
f"mmap failed for {size_bytes} bytes: errno={ctypes.get_errno()}"
)
return ptr, False
def run_target(args):
"""Run as target: allocate blocks, register, and wait."""
from mooncake.engine import TransferEngine
block_bytes = int(args.block_size_gb * 1024 * 1024 * 1024)
total_gb = args.num_blocks * args.block_size_gb
print(f"=== Target Node ===")
print(f"Blocks: {args.num_blocks} x {args.block_size_gb} GB = {total_gb} GB total")
print(f"Protocol: {args.protocol}")
print(f"Registration API: {'batch' if args.use_batch_api else 'per-block'}")
engine = TransferEngine()
ret = engine.initialize(
args.local_server_name, args.metadata_server, args.protocol, ""
)
if ret != 0:
raise RuntimeError(f"Engine initialization failed: {ret}")
if args.metadata_server == "P2PHANDSHAKE":
host = args.local_server_name.rpartition(":")[0]
rpc_port = engine.get_rpc_port()
actual_name = f"{host}:{rpc_port}"
print(f"Actual server name: {actual_name}")
# Allocate blocks
print(f"\nAllocating {args.num_blocks} blocks of {args.block_size_gb} GB...")
blocks = [] # (addr, size, is_hugepage)
for i in range(args.num_blocks):
try:
ptr, hp = allocate_block(block_bytes)
blocks.append((ptr, block_bytes, hp))
if (i + 1) % 10 == 0 or i == 0:
page_type = "hugepage" if hp else "4KB"
print(f" Block {i+1}/{args.num_blocks}: 0x{ptr:x} ({page_type})")
except RuntimeError as e:
print(f" Block {i+1} allocation FAILED: {e}")
break
if not blocks:
raise RuntimeError("No blocks allocated")
hp_count = sum(1 for _, _, hp in blocks if hp)
reg_count = sum(1 for _, _, hp in blocks if not hp)
print(f" Allocated {len(blocks)} blocks: {hp_count} hugepage, {reg_count} regular")
# Register
addrs = [b[0] for b in blocks]
sizes = [b[1] for b in blocks]
print(f"\nRegistering {len(blocks)} blocks...")
t0 = time.time()
if args.use_batch_api:
ret = engine.batch_register_memory(addrs, sizes)
if ret != 0:
print(f" batch_register_memory FAILED: {ret}")
print(f" Registration took {time.time() - t0:.1f}s before failure")
return
else:
for i, (addr, size, _) in enumerate(blocks):
ret = engine.register_memory(addr, size)
if ret != 0:
print(f" register_memory FAILED at block {i}: {ret}")
print(f" Registration took {time.time() - t0:.1f}s before failure")
return
reg_time = time.time() - t0
print(f" Registration OK: {reg_time:.1f}s for {len(blocks)} blocks")
print(f" Per-block: {reg_time / len(blocks) * 1000:.0f}ms")
# Publish block info
info = {
"blocks": [{"addr": b[0], "size": b[1]} for b in blocks],
"num_blocks": len(blocks),
}
print(f"\nTarget ready with {len(blocks)} blocks.")
print(f"TARGET_INFO:{json.dumps(info)}")
print("Waiting for initiator (Ctrl+C to stop)...")
try:
signal.pause()
except KeyboardInterrupt:
print("\nShutting down target.")
def run_initiator(args):
"""Run as initiator: pull data from random blocks."""
from mooncake.engine import TransferEngine
block_bytes = int(args.block_size_gb * 1024 * 1024 * 1024)
transfer_bytes = int(args.transfer_size_mb * 1024 * 1024)
print(f"=== Initiator Node ===")
print(f"Target: {args.target_server_name}")
print(f"Blocks: {args.num_blocks} x {args.block_size_gb} GB")
print(f"Transfer size: {args.transfer_size_mb} MB")
print(f"Protocol: {args.protocol}")
if not args.target_server_name:
raise RuntimeError("--target_server_name required in initiator mode")
engine = TransferEngine()
ret = engine.initialize(
args.local_server_name, args.metadata_server, args.protocol, ""
)
if ret != 0:
raise RuntimeError(f"Engine initialization failed: {ret}")
if args.metadata_server == "P2PHANDSHAKE":
host = args.local_server_name.rpartition(":")[0]
rpc_port = engine.get_rpc_port()
actual_name = f"{host}:{rpc_port}"
print(f"Actual server name: {actual_name}")
# Allocate local receive buffer
recv_bytes = transfer_bytes
print(f"\nAllocating {recv_bytes / 1e6:.0f} MB receive buffer...")
recv_addr, hp = allocate_block(recv_bytes)
print(f" 0x{recv_addr:x} ({'hugepage' if hp else '4KB'})")
ret = engine.register_memory(recv_addr, recv_bytes)
if ret != 0:
raise RuntimeError(f"Local memory registration failed: {ret}")
# Get target's first buffer address
print(f"Connecting to target {args.target_server_name}...")
remote_base = engine.get_first_buffer_address(args.target_server_name)
if remote_base == 0:
raise RuntimeError("Cannot get target buffer address")
print(f" Remote first buffer at 0x{remote_base:x}")
# Connection warmup
print("Warming up connection...")
for _ in range(3):
engine.transfer_sync_read(
args.target_server_name, recv_addr, remote_base, transfer_bytes
)
print(" Connection ready.")
# Note: blocks are independently mmap'd on the target, so they are NOT
# contiguous. We can only access the first block via get_first_buffer_address.
# The primary goal is to validate that registration of N separate blocks works
# and that data can be transferred from a registered block.
print(f"\nBenchmarking transfers from first registered block...")
print(f" Each transfer: {args.transfer_size_mb} MB")
# Warmup
print(f" Warming up ({args.warmup} iterations)...")
for w in range(args.warmup):
ret = engine.transfer_sync_read(
args.target_server_name, recv_addr, remote_base, transfer_bytes
)
if ret != 0:
print(f" WARNING: warmup failed iter {w}: {ret}")
# Benchmark
latencies = []
errors = 0
for i in range(args.iterations):
t0 = time.perf_counter()
ret = engine.transfer_sync_read(
args.target_server_name, recv_addr, remote_base, transfer_bytes
)
elapsed = time.perf_counter() - t0
if ret != 0:
errors += 1
if errors <= 3:
print(f" ERROR: transfer failed iter {i}: {ret}")
continue
latencies.append(elapsed * 1000)
if not latencies:
print(f" ALL FAILED ({errors} errors)")
return
latencies.sort()
avg_ms = statistics.mean(latencies)
p50_ms = latencies[len(latencies) // 2]
p99_ms = latencies[int(len(latencies) * 0.99)]
throughput_gbs = (transfer_bytes / 1e9) / (p50_ms / 1000)
print(f"\n{'='*60}")
print(f" Registered blocks: {args.num_blocks} x {args.block_size_gb}GB")
print(f" Transfer: {args.transfer_size_mb} MB")
print(f" Iterations: {len(latencies)} (errors: {errors})")
print(f" Avg latency: {avg_ms:.2f} ms")
print(f" p50 latency: {p50_ms:.2f} ms")
print(f" p99 latency: {p99_ms:.2f} ms")
print(f" Throughput: {throughput_gbs:.2f} GB/s")
print(f"{'='*60}")
results = {
"num_blocks": args.num_blocks,
"block_size_gb": args.block_size_gb,
"transfer_size_mb": args.transfer_size_mb,
"iterations": len(latencies),
"errors": errors,
"avg_latency_ms": round(avg_ms, 3),
"p50_latency_ms": round(p50_ms, 3),
"p99_latency_ms": round(p99_ms, 3),
"throughput_gbs": round(throughput_gbs, 3),
}
out_file = f"batch_bench_{args.num_blocks}x{args.block_size_gb}gb.json"
with open(out_file, "w") as f:
json.dump(results, f, indent=2)
print(f"Results saved to {out_file}")
def main():
args = parse_args()
if args.mode == "target":
run_target(args)
else:
run_initiator(args)
if __name__ == "__main__":
main()

View File

@ -1,452 +0,0 @@
#!/usr/bin/env python3
"""
KV Cache Prefix Transfer Benchmark for Mooncake EFA Transport.
Simulates cross-node KV cache transfer for prefix cache hits in LLM inference.
Tests how registered memory pool size affects transfer latency/throughput
under the per-NIC partition auto-split strategy.
Usage:
# Target node (holds KV cache pool):
python kvcache_prefix_bench.py --mode target \
--local_server_name <target_ip>:12345 \
--pool_size_gb 100 --protocol efa
# Initiator node (pulls prefix KV cache):
python kvcache_prefix_bench.py --mode initiator \
--local_server_name <initiator_ip>:12346 \
--target_server_name <target_ip>:12345 \
--pool_size_gb 100 --protocol efa \
--prefix_tokens 4096,8192,16384,32768
Requires: mooncake Python package (pip install -e mooncake-wheel)
Branch: feat/efa-auto-split-mr (per-NIC partition auto-split)
"""
import argparse
import ctypes
import ctypes.util
import json
import os
import signal
import statistics
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
def parse_args():
parser = argparse.ArgumentParser(
description="KV Cache Prefix Transfer Benchmark"
)
parser.add_argument(
"--mode",
choices=["target", "initiator"],
required=True,
help="Run as target (KV cache holder) or initiator (puller)",
)
parser.add_argument(
"--local_server_name",
required=True,
help="Local address, e.g. 172.31.6.162:12345",
)
parser.add_argument(
"--target_server_name",
default="",
help="Target address (initiator mode only)",
)
parser.add_argument(
"--metadata_server",
default="P2PHANDSHAKE",
help="Metadata server address (default: P2PHANDSHAKE)",
)
parser.add_argument(
"--protocol", default="efa", help="Transport protocol (default: efa)"
)
parser.add_argument(
"--pool_size_gb",
type=float,
default=10.0,
help="KV cache pool size in GB to register (default: 10)",
)
parser.add_argument(
"--prefix_tokens",
default="4096,8192,16384,32768",
help="Comma-separated list of prefix token counts to test",
)
parser.add_argument(
"--kv_bytes_per_token",
type=int,
default=89856,
help="KV cache bytes per token. "
"Default: 89856 for GLM-5.1 (754B MoE, MLA attention): "
"(kv_lora_rank=512 + qk_rope_head_dim=64) * 2 bytes * 78 layers. "
"For GLM-4-9B (standard MHA): use 40960",
)
parser.add_argument(
"--iterations",
type=int,
default=50,
help="Number of transfer iterations per test (default: 50)",
)
parser.add_argument(
"--warmup",
type=int,
default=5,
help="Number of warmup iterations (default: 5)",
)
parser.add_argument(
"--use_gpu",
action="store_true",
help="Use GPU memory instead of CPU memory",
)
parser.add_argument(
"--gpu_id", type=int, default=0, help="GPU device ID (default: 0)"
)
parser.add_argument(
"--threads",
type=int,
default=1,
help="Number of threads for concurrent transfer (default: 1). "
"Each thread transfers a chunk of the prefix in parallel.",
)
return parser.parse_args()
def allocate_cpu_memory(size_bytes):
"""Allocate page-aligned CPU memory using mmap."""
libc_name = ctypes.util.find_library("c")
libc = ctypes.CDLL(libc_name, use_errno=True)
# mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
PROT_READ = 0x1
PROT_WRITE = 0x2
MAP_PRIVATE = 0x02
MAP_ANONYMOUS = 0x20
MAP_HUGETLB = 0x40000
MAP_FAILED = ctypes.c_void_p(-1).value
libc.mmap.restype = ctypes.c_void_p
libc.mmap.argtypes = [
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_long,
]
flags = MAP_PRIVATE | MAP_ANONYMOUS
# Try hugepages first (recommended for large EFA registrations)
ptr = libc.mmap(None, size_bytes, PROT_READ | PROT_WRITE,
flags | MAP_HUGETLB, -1, 0)
if ptr and ptr != MAP_FAILED:
print(f" Allocated {size_bytes / 1e9:.1f} GB with 2MB hugepages")
return ptr, size_bytes, True
# Fall back to regular pages
ptr = libc.mmap(None, size_bytes, PROT_READ | PROT_WRITE, flags, -1, 0)
if not ptr or ptr == MAP_FAILED:
raise RuntimeError(
f"mmap failed for {size_bytes} bytes: "
f"errno={ctypes.get_errno()}"
)
print(
f" Allocated {size_bytes / 1e9:.1f} GB with 4KB pages "
f"(hugepages unavailable — configure vm.nr_hugepages for better EFA performance)"
)
return ptr, size_bytes, False
def allocate_gpu_memory(size_bytes, gpu_id):
"""Allocate GPU memory using PyTorch."""
import torch
torch.cuda.set_device(gpu_id)
# Allocate as uint8 tensor
tensor = torch.zeros(size_bytes, dtype=torch.uint8, device=f"cuda:{gpu_id}")
ptr = tensor.data_ptr()
print(f" Allocated {size_bytes / 1e9:.1f} GB on GPU {gpu_id}")
return ptr, tensor # keep tensor alive
def run_target(args):
"""Run as target node: allocate KV cache pool and wait."""
from mooncake.engine import TransferEngine
print(f"=== Target Node ===")
print(f"Pool size: {args.pool_size_gb} GB")
print(f"Protocol: {args.protocol}")
engine = TransferEngine()
ret = engine.initialize(
args.local_server_name, args.metadata_server, args.protocol, ""
)
if ret != 0:
raise RuntimeError(f"Engine initialization failed: {ret}")
if args.metadata_server == "P2PHANDSHAKE":
host = args.local_server_name.rpartition(":")[0]
rpc_port = engine.get_rpc_port()
actual_name = f"{host}:{rpc_port}"
print(f"Actual server name: {actual_name}")
pool_bytes = int(args.pool_size_gb * 1024 * 1024 * 1024)
print(f"Allocating {args.pool_size_gb} GB KV cache pool...")
if args.use_gpu:
pool_addr, _tensor = allocate_gpu_memory(pool_bytes, args.gpu_id)
else:
pool_addr, _, _ = allocate_cpu_memory(pool_bytes)
print(f"Registering memory with transfer engine...")
t0 = time.time()
ret = engine.register_memory(pool_addr, pool_bytes)
reg_time = time.time() - t0
if ret != 0:
raise RuntimeError(f"Memory registration failed: {ret}")
print(f" Registration took {reg_time:.1f}s")
print(f"\nTarget ready. Pool addr: 0x{pool_addr:x}, size: {pool_bytes}")
print("Waiting for initiator (Ctrl+C to stop)...")
# Write pool info for initiator to read
info = {"pool_addr": pool_addr, "pool_bytes": pool_bytes}
print(f"TARGET_INFO:{json.dumps(info)}")
try:
signal.pause()
except KeyboardInterrupt:
print("\nShutting down target.")
def run_initiator(args):
"""Run as initiator node: pull prefix KV cache and measure performance."""
from mooncake.engine import TransferEngine
print(f"=== Initiator Node ===")
print(f"Target: {args.target_server_name}")
print(f"Pool size: {args.pool_size_gb} GB")
print(f"Protocol: {args.protocol}")
print(f"KV bytes/token: {args.kv_bytes_per_token}")
print(f"Threads: {args.threads}")
if not args.target_server_name:
raise RuntimeError("--target_server_name required in initiator mode")
prefix_tokens_list = [int(x) for x in args.prefix_tokens.split(",")]
transfer_sizes = [
tokens * args.kv_bytes_per_token for tokens in prefix_tokens_list
]
print(f"\nTest matrix:")
for tokens, size in zip(prefix_tokens_list, transfer_sizes):
print(f" {tokens:>6} tokens -> {size / 1e6:.1f} MB transfer")
# Initialize engine
engine = TransferEngine()
ret = engine.initialize(
args.local_server_name, args.metadata_server, args.protocol, ""
)
if ret != 0:
raise RuntimeError(f"Engine initialization failed: {ret}")
if args.metadata_server == "P2PHANDSHAKE":
host = args.local_server_name.rpartition(":")[0]
rpc_port = engine.get_rpc_port()
actual_name = f"{host}:{rpc_port}"
print(f"Actual server name: {actual_name}")
# Allocate local receive buffer (large enough for the biggest transfer)
max_transfer = max(transfer_sizes)
recv_bytes = max_transfer
print(f"\nAllocating {recv_bytes / 1e6:.1f} MB local receive buffer...")
if args.use_gpu:
recv_addr, _tensor = allocate_gpu_memory(recv_bytes, args.gpu_id)
else:
recv_addr, _, _ = allocate_cpu_memory(recv_bytes)
ret = engine.register_memory(recv_addr, recv_bytes)
if ret != 0:
raise RuntimeError(f"Local memory registration failed: {ret}")
# Get target's buffer address
print(f"Connecting to target {args.target_server_name}...")
remote_addr = engine.get_first_buffer_address(args.target_server_name)
if remote_addr == 0:
raise RuntimeError(
"Cannot get target buffer address. "
"Is the target running and registered?"
)
print(f" Remote buffer at 0x{remote_addr:x}")
# Connection warmup: a small transfer to establish the EFA connection
# (openSegment, endpoint creation, etc.) so it doesn't skew the first
# prefix size's measurements.
print("Warming up connection...")
warmup_size = min(transfer_sizes[0], recv_bytes)
for _ in range(3):
engine.transfer_sync_read(
args.target_server_name, recv_addr, remote_addr, warmup_size
)
print(" Connection ready.")
num_threads = args.threads
def do_transfer(local_addr, remote_addr_with_offset, size):
"""Single transfer_sync_read call, suitable for thread pool."""
return engine.transfer_sync_read(
args.target_server_name, local_addr,
remote_addr_with_offset, size
)
def threaded_transfer(local_base, remote_base, total_size, pool):
"""Split transfer across threads; return max of 0 (ok) or error code."""
if num_threads <= 1:
return engine.transfer_sync_read(
args.target_server_name, local_base, remote_base, total_size
)
chunk = total_size // num_threads
# Align chunk to 4KB
chunk = chunk & ~0xFFF
futures = []
for t in range(num_threads):
off = t * chunk
sz = chunk if t < num_threads - 1 else (total_size - off)
futures.append(
pool.submit(do_transfer, local_base + off,
remote_base + off, sz)
)
return max(f.result() for f in futures)
# Run benchmarks
print(f"\n{'='*72}")
print(
f"{'Prefix':>8} {'Size':>10} {'Latency(ms)':>12} "
f"{'p50(ms)':>10} {'p99(ms)':>10} {'Tput(GB/s)':>12}"
)
print(f"{'='*72}")
results = []
with ThreadPoolExecutor(max_workers=num_threads) as pool:
for tokens, transfer_size in zip(prefix_tokens_list, transfer_sizes):
if transfer_size > recv_bytes:
print(f" SKIP {tokens} tokens: transfer {transfer_size} > recv buffer")
continue
# Warmup: use the same offset pattern as the benchmark to pre-warm
# remote memory pages and DMA paths at each offset.
pool_bytes = int(args.pool_size_gb * 1024 * 1024 * 1024)
max_offset = pool_bytes - transfer_size
for w in range(args.warmup):
for i in range(args.iterations):
if max_offset > 0:
offset = ((i * transfer_size) % max_offset) & ~0xFFF
else:
offset = 0
ret = threaded_transfer(
recv_addr, remote_addr + offset, transfer_size, pool
)
if ret != 0:
print(f" WARNING: warmup transfer failed: {ret}")
break
# Benchmark
latencies = []
errors = 0
for i in range(args.iterations):
# Use different offset within remote pool for each iteration
# to simulate accessing different prefix locations
if max_offset > 0:
# Align to 4KB boundary
offset = ((i * transfer_size) % max_offset) & ~0xFFF
else:
offset = 0
t0 = time.perf_counter()
ret = threaded_transfer(
recv_addr, remote_addr + offset, transfer_size, pool
)
elapsed = time.perf_counter() - t0
if ret != 0:
errors += 1
if errors <= 3:
print(f" ERROR: transfer failed at iter {i}: {ret}")
continue
latencies.append(elapsed * 1000) # ms
if not latencies:
print(f" {tokens:>6}k ALL FAILED ({errors} errors)")
continue
latencies.sort()
avg_ms = statistics.mean(latencies)
p50_ms = latencies[len(latencies) // 2]
p99_ms = latencies[int(len(latencies) * 0.99)]
throughput_gbs = (transfer_size / 1e9) / (p50_ms / 1000)
print(
f" {tokens:>6} {transfer_size/1e6:>8.1f}MB "
f"{avg_ms:>11.2f} {p50_ms:>10.2f} {p99_ms:>10.2f} "
f"{throughput_gbs:>11.2f}"
)
results.append(
{
"prefix_tokens": tokens,
"transfer_bytes": transfer_size,
"transfer_mb": transfer_size / 1e6,
"pool_size_gb": args.pool_size_gb,
"iterations": len(latencies),
"errors": errors,
"avg_latency_ms": round(avg_ms, 3),
"p50_latency_ms": round(p50_ms, 3),
"p99_latency_ms": round(p99_ms, 3),
"throughput_gbs": round(throughput_gbs, 3),
"threads": num_threads,
}
)
print(f"{'='*72}")
# Summary
if results:
print(f"\n=== Summary (pool_size={args.pool_size_gb}GB, threads={num_threads}) ===")
print(json.dumps(results, indent=2))
# Save results
thread_tag = f"_t{num_threads}" if num_threads > 1 else ""
out_file = (
f"kvcache_bench_pool{args.pool_size_gb}gb"
f"{'_gpu' if args.use_gpu else '_cpu'}{thread_tag}.json"
)
with open(out_file, "w") as f:
json.dump(
{
"pool_size_gb": args.pool_size_gb,
"protocol": args.protocol,
"use_gpu": args.use_gpu,
"kv_bytes_per_token": args.kv_bytes_per_token,
"threads": num_threads,
"iterations": args.iterations,
"results": results,
},
f,
indent=2,
)
print(f"Results saved to {out_file}")
def main():
args = parse_args()
if args.mode == "target":
run_target(args)
else:
run_initiator(args)
if __name__ == "__main__":
main()

View File

@ -113,10 +113,6 @@ static inline int64_t getCurrentTimeInNano() {
return (int64_t{ts.tv_sec} * kNanosPerSecond + int64_t{ts.tv_nsec});
}
static inline int64_t getCurrentTimeInMilli() {
return getCurrentTimeInNano() / 1000 / 1000;
}
static inline std::string getCurrentDateTime() {
auto now = std::chrono::system_clock::now();
auto time_t_now = std::chrono::system_clock::to_time_t(now);
@ -331,13 +327,9 @@ static inline ssize_t writeFully(int fd, const void *buf, size_t len) {
}
static inline ssize_t readFully(int fd, void *buf, size_t len) {
// Set a timeout for read to avoid hanging forever.
constexpr std::chrono::seconds kReadTimeout = std::chrono::seconds(300);
const std::chrono::steady_clock::time_point deadline =
std::chrono::steady_clock::now() + kReadTimeout;
char *pos = (char *)buf;
size_t nbytes = len;
while (nbytes && std::chrono::steady_clock::now() < deadline) {
while (nbytes) {
ssize_t rc = read(fd, pos, nbytes);
if (rc < 0 && (errno == EAGAIN || errno == EINTR))
continue;
@ -352,14 +344,7 @@ static inline ssize_t readFully(int fd, void *buf, size_t len) {
pos += rc;
nbytes -= rc;
}
if (nbytes != 0) {
LOG(WARNING) << "Socket read timed out, timeout: "
<< kReadTimeout.count()
<< ", deadline: " << deadline.time_since_epoch().count()
<< ", read " << len - nbytes << " out of " << len
<< " bytes";
}
return len - nbytes;
return len;
}
static inline int writeString(int fd, const HandShakeRequestType type,

View File

@ -55,14 +55,6 @@ class MultiTransport {
Transport *getTransport(const std::string &proto);
/**
* @brief Check if TCP is the only installed transport.
*
* When only TCP transport is available (no RDMA, NVLink, etc.),
* local memcpy is preferred over TCP loopback for same-host transfers.
*/
bool isTcpOnly() const;
std::vector<Transport *> listTransports();
void *getBaseAddr();

View File

@ -150,14 +150,6 @@ class TransferEngine {
Transport* getTransport(const std::string& proto);
/**
* @brief Check if TCP is the only installed transport.
*
* When only TCP transport is available (no RDMA, NVLink, etc.),
* local memcpy is preferred over TCP loopback for same-host transfers.
*/
bool isTcpOnly() const;
int syncSegmentCache(const std::string& segment_name = "");
std::shared_ptr<TransferMetadata> getMetadata();

View File

@ -105,8 +105,6 @@ transfer_engine_t createTransferEngine(const char *metadata_conn_string,
const char *ip_or_host_name,
uint64_t rpc_port, int auto_discover);
int discoverTopology(transfer_engine_t engine);
int getLocalIpAndPort(transfer_engine_t engine, char *buf_out, size_t buf_len);
transport_t installTransport(transfer_engine_t engine, const char *proto,
@ -121,11 +119,6 @@ segment_id_t openSegmentNoCache(transfer_engine_t engine,
int closeSegment(transfer_engine_t engine, segment_id_t segment_id);
// Eagerly pre-connect all EFA endpoints to `segment_name`. Eliminates the
// first-batch fi_av_insert stall (observed ~6 s for 16 local NICs × N peer
// NICs). No-op on non-EFA installs. Idempotent. Returns 0 on success.
int warmupEfaSegment(transfer_engine_t engine, const char *segment_name);
int removeLocalSegment(transfer_engine_t engine, const char *segment_name);
void destroyTransferEngine(transfer_engine_t engine);

View File

@ -334,8 +334,6 @@ class TransferEngineImpl {
return multi_transports_->getTransport(proto);
}
bool isTcpOnly() const { return multi_transports_->isTcpOnly(); }
int syncSegmentCache(const std::string& segment_name = "") {
return metadata_->syncSegmentCache(segment_name);
}

View File

@ -31,7 +31,6 @@
#include <memory>
#include <string>
#include <thread>
#include <map>
#include <unordered_map>
#include "common.h"
@ -207,7 +206,7 @@ class EfaContext {
std::atomic<int> next_cq_index_{0};
RWSpinlock mr_lock_;
std::map<uint64_t, EfaMemoryRegionMeta> mr_map_;
std::unordered_map<uint64_t, EfaMemoryRegionMeta> mr_map_;
};
} // namespace mooncake

View File

@ -52,96 +52,82 @@ class EfaTransport : public Transport {
~EfaTransport();
int install(std::string& local_server_name,
int install(std::string &local_server_name,
std::shared_ptr<TransferMetadata> meta,
std::shared_ptr<Topology> topo) override;
const char* getName() const override { return "efa"; }
const char *getName() const override { return "efa"; }
int registerLocalMemory(void* addr, size_t length,
const std::string& location, bool remote_accessible,
int registerLocalMemory(void *addr, size_t length,
const std::string &location, bool remote_accessible,
bool update_metadata) override;
int unregisterLocalMemory(void* addr, bool update_metadata = true) override;
int unregisterLocalMemory(void *addr, bool update_metadata = true) override;
int registerLocalMemoryBatch(const std::vector<BufferEntry>& buffer_list,
const std::string& location) override;
int registerLocalMemoryBatch(const std::vector<BufferEntry> &buffer_list,
const std::string &location) override;
int unregisterLocalMemoryBatch(
const std::vector<void*>& addr_list) override;
// Eagerly establish EFA endpoints to every NIC of `segment_name`.
//
// Rationale: libfabric FI_EP_RDM endpoints resolve peer addresses lazily
// via fi_av_insert() on first send. With 16 local NICs × N peer NICs,
// the first submitTransfer() of a batch serializes ~N*16
// handshake+fi_av_insert round-trips, producing a single-digit-second
// stall (observed ~6 s on B300 for the first 100 × 0.5 MB batch). After
// this call returns, every (local_ctx, peer_nic) endpoint is CONNECTED
// and the first real submitTransfer() goes straight to fi_write/fi_read.
//
// Safe to call multiple times (idempotent: endpoint() + setup are both
// idempotent). Re-run after any openSegment() on a new peer.
int warmupSegment(const std::string& segment_name);
const std::vector<void *> &addr_list) override;
private:
// Internal version with force_sequential option to avoid nested parallelism
int registerLocalMemoryInternal(void* addr, size_t length,
const std::string& location,
int registerLocalMemoryInternal(void *addr, size_t length,
const std::string &location,
bool remote_accessible,
bool update_metadata,
bool force_sequential);
int unregisterLocalMemoryInternal(void* addr, bool update_metadata,
int unregisterLocalMemoryInternal(void *addr, bool update_metadata,
bool force_sequential);
// TRANSFER
Status submitTransfer(BatchID batch_id,
const std::vector<TransferRequest>& entries) override;
const std::vector<TransferRequest> &entries) override;
Status submitTransferTask(
const std::vector<TransferTask*>& task_list) override;
const std::vector<TransferTask *> &task_list) override;
Status getTransferStatus(BatchID batch_id,
std::vector<TransferStatus>& status);
std::vector<TransferStatus> &status);
Status getTransferStatus(BatchID batch_id, size_t task_id,
TransferStatus& status) override;
TransferStatus &status) override;
SegmentID getSegmentID(const std::string& segment_name);
SegmentID getSegmentID(const std::string &segment_name);
private:
int allocateLocalSegmentID();
int preTouchMemory(void* addr, size_t length);
int preTouchMemory(void *addr, size_t length);
public:
int onSetupEfaConnections(const HandShakeDesc& peer_desc,
HandShakeDesc& local_desc);
int onSetupEfaConnections(const HandShakeDesc &peer_desc,
HandShakeDesc &local_desc);
int sendHandshake(const std::string& peer_server_name,
const HandShakeDesc& local_desc,
HandShakeDesc& peer_desc) {
int sendHandshake(const std::string &peer_server_name,
const HandShakeDesc &local_desc,
HandShakeDesc &peer_desc) {
return metadata_->sendHandshake(peer_server_name, local_desc,
peer_desc);
}
const std::string& local_server_name() const { return local_server_name_; }
const std::string &local_server_name() const { return local_server_name_; }
std::shared_ptr<TransferMetadata> meta() { return metadata_; }
private:
int initializeEfaResources();
int startHandshakeDaemon(std::string& local_server_name);
int startHandshakeDaemon(std::string &local_server_name);
public:
static int selectDevice(SegmentDesc* desc, uint64_t offset, size_t length,
int& buffer_id, int& device_id, int retry_cnt = 0);
static int selectDevice(SegmentDesc* desc, uint64_t offset, size_t length,
std::string_view hint, int& buffer_id,
int& device_id, int retry_cnt = 0);
static int selectDevice(SegmentDesc *desc, uint64_t offset, size_t length,
int &buffer_id, int &device_id, int retry_cnt = 0);
static int selectDevice(SegmentDesc *desc, uint64_t offset, size_t length,
std::string_view hint, int &buffer_id,
int &device_id, int retry_cnt = 0);
private:
// Start/stop CQ polling worker threads
@ -153,16 +139,6 @@ class EfaTransport : public Transport {
std::vector<std::shared_ptr<EfaContext>> context_list_;
std::shared_ptr<Topology> local_topology_;
// Track chunked MR registrations for per-NIC partitioned buffers.
// When a buffer exceeds max_mr_size, it is split into chunks, each
// registered on a disjoint subset of NICs (per-NIC partition).
struct ChunkRegistration {
uint64_t addr;
std::vector<size_t> nic_indices;
};
std::mutex chunk_map_mutex_;
std::unordered_map<uint64_t, std::vector<ChunkRegistration>> chunk_map_;
// CQ polling worker threads
std::atomic<bool> worker_running_{false};
std::vector<std::thread> worker_threads_;

View File

@ -38,8 +38,6 @@ class EndpointStore {
public:
virtual std::shared_ptr<RdmaEndPoint> getEndpoint(
const std::string &peer_nic_path) = 0;
virtual std::shared_ptr<RdmaEndPoint> getEndpointByPtr(
const RdmaEndPoint *endpoint_ptr) = 0;
virtual std::shared_ptr<RdmaEndPoint> insertEndpoint(
const std::string &peer_nic_path, RdmaContext *context) = 0;
virtual int deleteEndpoint(const std::string &peer_nic_path) = 0;
@ -60,8 +58,6 @@ class FIFOEndpointStore : public EndpointStore {
FIFOEndpointStore(size_t max_size) : max_size_(max_size) {}
std::shared_ptr<RdmaEndPoint> getEndpoint(
const std::string &peer_nic_path) override;
std::shared_ptr<RdmaEndPoint> getEndpointByPtr(
const RdmaEndPoint *endpoint_ptr) override;
std::shared_ptr<RdmaEndPoint> insertEndpoint(
const std::string &peer_nic_path, RdmaContext *context) override;
int deleteEndpoint(const std::string &peer_nic_path) override;
@ -93,8 +89,6 @@ class SIEVEEndpointStore : public EndpointStore {
: waiting_list_len_(0), max_size_(max_size) {}
std::shared_ptr<RdmaEndPoint> getEndpoint(
const std::string &peer_nic_path) override;
std::shared_ptr<RdmaEndPoint> getEndpointByPtr(
const RdmaEndPoint *endpoint_ptr) override;
std::shared_ptr<RdmaEndPoint> insertEndpoint(
const std::string &peer_nic_path, RdmaContext *context) override;
int deleteEndpoint(const std::string &peer_nic_path) override;

View File

@ -99,9 +99,6 @@ class RdmaContext {
// EndPoint Management
std::shared_ptr<RdmaEndPoint> endpoint(const std::string &peer_nic_path);
std::shared_ptr<RdmaEndPoint> getEndpointByPtr(
const RdmaEndPoint *endpoint_ptr);
int deleteEndpoint(const std::string &peer_nic_path);
int disconnectAllEndpoints();

View File

@ -1 +0,0 @@
src/customer_pattern.rs

View File

@ -17,84 +17,16 @@ use std::path::PathBuf;
fn main() {
println!("cargo:rustc-link-search=native=../build/src");
println!("cargo:rustc-link-search=native=../../build/mooncake-transfer-engine/src");
println!("cargo:rustc-link-lib=static=transfer_engine");
// libbase.a holds mooncake::Status, which libtransfer_engine.a references.
println!("cargo:rustc-link-search=native=../build/src/common/base");
println!("cargo:rustc-link-search=native=../../build/mooncake-transfer-engine/src/common/base");
println!("cargo:rustc-link-lib=static=base");
// The transfer_engine build uses ASIO_SEPARATE_COMPILATION + ASIO_DYN_LINK,
// so the asio symbols live in mooncake-asio/libasio.so. Link it whenever
// we can find it (standalone cmake build places it alongside src/).
println!("cargo:rustc-link-search=native=../build/mooncake-asio");
println!("cargo:rustc-link-search=native=../../build/mooncake-asio");
println!("cargo:rustc-link-lib=asio");
// EFA on AWS installs libfabric under /opt/amazon/efa/lib.
if std::path::Path::new("/opt/amazon/efa/lib").exists() {
println!("cargo:rustc-link-search=native=/opt/amazon/efa/lib");
}
println!("cargo:rustc-link-lib=stdc++");
println!("cargo:rustc-link-lib=ibverbs");
// libfabric (fi_*): only needed for EFA transport, but harmless when
// the system has it installed; required on AWS EFA hosts. Opt-out by
// setting MOONCAKE_WITHOUT_LIBFABRIC=1 if building on a box without it.
if env::var("MOONCAKE_WITHOUT_LIBFABRIC")
.map(|v| v == "1" || v.eq_ignore_ascii_case("on") || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
{
// skip
} else {
println!("cargo:rustc-link-lib=fabric");
}
println!("cargo:rustc-link-lib=glog");
println!("cargo:rustc-link-lib=gflags");
println!("cargo:rustc-link-lib=pthread");
println!("cargo:rustc-link-lib=jsoncpp");
println!("cargo:rustc-link-lib=numa");
println!("cargo:rustc-link-lib=curl");
let flag_on = |name: &str| {
env::var(name)
.map(|v| v == "1" || v.eq_ignore_ascii_case("on") || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
};
// etcd-cpp-api: only needed when transfer_engine was built with
// USE_ETCD=ON. Opt-in via MOONCAKE_WITH_ETCD=1 to keep non-etcd builds
// (e.g. EFA-only on AWS) linkable.
if flag_on("MOONCAKE_WITH_ETCD") {
println!("cargo:rustc-link-lib=etcd-cpp-api");
}
// CUDA runtime: libtransfer_engine.a built with USE_CUDA=ON pulls in
// cudaMemcpy/cudaStream* symbols. The Rust demos themselves don't call
// CUDA — this is purely a transitive archive dep. Enable with
// MOONCAKE_WITH_CUDA=1 and optional CUDA_HOME override for lib path.
if flag_on("MOONCAKE_WITH_CUDA") {
// Accept either a CUDA_HOME (append lib64/lib) or an explicit
// CUDART_LIB_DIR that already points at the directory containing
// libcudart.so. This covers both /usr/local/cuda installs and
// pip-wheel layouts like .../nvidia/cu13/lib.
if let Ok(dir) = env::var("CUDART_LIB_DIR") {
println!("cargo:rustc-link-search=native={}", dir);
} else if let Ok(cuda_home) = env::var("CUDA_HOME") {
let lib64 = PathBuf::from(&cuda_home).join("lib64");
let lib = PathBuf::from(&cuda_home).join("lib");
if lib64.exists() {
println!("cargo:rustc-link-search=native={}", lib64.display());
}
if lib.exists() {
println!("cargo:rustc-link-search=native={}", lib.display());
}
} else {
println!("cargo:rustc-link-search=native=/usr/local/cuda/lib64");
}
println!("cargo:rustc-link-lib=cudart");
}
println!("cargo:rustc-link-lib=etcd-cpp-api");
let bindings = bindgen::builder()
.header("../include/transfer_engine_c.h")

View File

@ -91,27 +91,6 @@ impl TransferEngine {
Ok(Self { engine })
}
pub fn discover_topology(&self) -> Result<()> {
let ret = unsafe { bindings::discoverTopology(self.engine) };
if ret != 0 {
bail!("Failed to discover topology")
} else {
Ok(())
}
}
pub fn install_transport(&self, proto: &str) -> Result<()> {
let proto_c = CString::new(proto).map_err(|_| anyhow!("CString::new failed"))?;
let ret = unsafe {
bindings::installTransport(self.engine, proto_c.as_ptr(), std::ptr::null_mut())
};
if ret.is_null() {
bail!("Failed to install transport '{}'", proto)
} else {
Ok(())
}
}
pub fn close(&mut self) -> Result<()> {
unsafe {
bindings::destroyTransferEngine(self.engine);
@ -263,20 +242,6 @@ impl TransferEngine {
}
}
/// Eagerly establish EFA endpoints to `segment_name` so the first
/// `submit_transfer` doesn't pay the serial fi_av_insert cost. No-op on
/// non-EFA transports. Call after `open_segment` (and after the metadata
/// has the peer's NIC list published).
pub fn warmup_efa_segment(&self, name: &str) -> Result<()> {
let name_c = CString::new(name).map_err(|_| anyhow!("CString::new failed"))?;
let ret = unsafe { bindings::warmupEfaSegment(self.engine, name_c.as_ptr()) };
if ret < 0 {
bail!("warmupEfaSegment failed for {}: {}", name, ret)
} else {
Ok(())
}
}
pub fn sync_segment_cache(&self) -> Result<()> {
let ret = unsafe { bindings::syncSegmentCache(self.engine) };
if ret < 0 {

View File

@ -461,10 +461,6 @@ Transport* MultiTransport::getTransport(const std::string& proto) {
return transport_map_[proto].get();
}
bool MultiTransport::isTcpOnly() const {
return transport_map_.size() == 1 && transport_map_.count("tcp") == 1;
}
std::vector<Transport*> MultiTransport::listTransports() {
std::vector<Transport*> transport_list;
for (auto& entry : transport_map_)

View File

@ -179,8 +179,6 @@ Transport* TransferEngine::getTransport(const std::string& proto) {
return impl_->getTransport(proto);
}
bool TransferEngine::isTcpOnly() const { return impl_->isTcpOnly(); }
int TransferEngine::syncSegmentCache(const std::string& segment_name) {
return impl_->syncSegmentCache(segment_name);
}
@ -580,15 +578,6 @@ Transport* TransferEngine::getTransport(const std::string& proto) {
return impl_->getTransport(proto);
}
bool TransferEngine::isTcpOnly() const {
if (use_tent_)
// TENT already rejects TCP loopback transfers when MC_STORE_MEMCPY
// is disabled, so auto-enabling memcpy is unnecessary in TENT mode.
return false;
else
return impl_->isTcpOnly();
}
int TransferEngine::syncSegmentCache(const std::string& segment_name) {
if (use_tent_)
return 0;

View File

@ -19,9 +19,6 @@
#include "transfer_engine.h"
#include "transport/transport.h"
#ifdef USE_EFA
#include "transport/efa_transport/efa_transport.h"
#endif
using namespace mooncake;
@ -39,11 +36,6 @@ transfer_engine_t createTransferEngine(const char *metadata_conn_string,
return (transfer_engine_t)native;
}
int discoverTopology(transfer_engine_t engine) {
TransferEngine *native = (TransferEngine *)engine;
return native->getLocalTopology()->discover({});
}
int getLocalIpAndPort(transfer_engine_t engine, char *buf_out, size_t buf_len) {
TransferEngine *native = (TransferEngine *)engine;
auto str = native->getLocalIpAndPort();
@ -85,21 +77,6 @@ int closeSegment(transfer_engine_t engine, segment_id_t segment_id) {
return native->closeSegment(segment_id);
}
int warmupEfaSegment(transfer_engine_t engine, const char *segment_name) {
#ifdef USE_EFA
TransferEngine *native = (TransferEngine *)engine;
auto *t = native->getTransport("efa");
if (!t) return 0; // Non-EFA build or EFA not installed; nothing to do.
auto *efa = dynamic_cast<EfaTransport *>(t);
if (!efa) return 0;
return efa->warmupSegment(segment_name ? segment_name : "");
#else
(void)engine;
(void)segment_name;
return 0;
#endif
}
int removeLocalSegment(transfer_engine_t engine, const char *segment_name) {
TransferEngine *native = (TransferEngine *)engine;
return native->removeLocalSegment(segment_name);

View File

@ -29,9 +29,7 @@
#include "transfer_metadata_plugin.h"
#include "transport/transport.h"
#ifdef USE_BAREX
#include "transport/barex_transport/barex_transport.h"
#endif
namespace mooncake {

View File

@ -164,15 +164,6 @@ int EfaContext::construct(size_t num_cq_list, size_t num_comp_channels,
int max_endpoints) {
endpoint_store_ = std::make_shared<EfaEndpointStore>(max_endpoints);
#if !defined(USE_CUDA) && !defined(USE_HIP)
// When built without GPU support, prevent libfabric's EFA provider from
// dlopen-ing libcudart/libcuda at fi_getinfo/fi_domain time. That
// initialization creates a CUDA primary context on GPU 0 and leaks
// ~616 MiB of device memory even when no GPU memory is ever registered.
// Only set if the user hasn't explicitly configured FI_HMEM.
setenv("FI_HMEM", "system", 0);
#endif
// Setup hints for EFA provider
hints_ = fi_allocinfo();
if (!hints_) {
@ -190,11 +181,8 @@ int EfaContext::construct(size_t num_cq_list, size_t num_comp_channels,
std::string domain_name = device_name_ + "-rdm";
hints_->domain_attr->name = strdup(domain_name.c_str());
hints_->domain_attr->mr_mode = FI_MR_LOCAL | FI_MR_VIRT_ADDR |
FI_MR_ALLOCATED | FI_MR_PROV_KEY
#if defined(USE_CUDA) || defined(USE_HIP)
| FI_MR_HMEM
#endif
;
FI_MR_ALLOCATED | FI_MR_PROV_KEY |
FI_MR_HMEM;
hints_->domain_attr->threading = FI_THREAD_SAFE;
// Get fabric info
@ -441,35 +429,31 @@ int EfaContext::preTouchMemory(void* addr, size_t length) {
uint64_t EfaContext::rkey(void* addr) {
RWSpinlock::ReadGuard guard(mr_lock_);
auto it = mr_map_.upper_bound((uint64_t)addr);
if (it != mr_map_.begin()) {
--it;
if ((uint64_t)addr < it->first + it->second.length && it->second.mr) {
return it->second.key;
}
auto it = mr_map_.find((uint64_t)addr);
if (it != mr_map_.end() && it->second.mr) {
return it->second.key;
}
return 0;
}
uint64_t EfaContext::lkey(void* addr) {
RWSpinlock::ReadGuard guard(mr_lock_);
auto it = mr_map_.upper_bound((uint64_t)addr);
if (it != mr_map_.begin()) {
--it;
if ((uint64_t)addr < it->first + it->second.length && it->second.mr) {
return fi_mr_key(it->second.mr);
}
auto it = mr_map_.find((uint64_t)addr);
if (it != mr_map_.end() && it->second.mr) {
return fi_mr_key(it->second.mr);
}
return 0;
}
void* EfaContext::mrDesc(void* addr) {
RWSpinlock::ReadGuard guard(mr_lock_);
auto it = mr_map_.upper_bound((uint64_t)addr);
if (it != mr_map_.begin()) {
--it;
if ((uint64_t)addr < it->first + it->second.length && it->second.mr) {
return fi_mr_desc(it->second.mr);
// Find the MR that contains this address
for (auto& entry : mr_map_) {
if ((uint64_t)addr >= entry.first &&
(uint64_t)addr < entry.first + entry.second.length) {
if (entry.second.mr) {
return fi_mr_desc(entry.second.mr);
}
}
}
return nullptr;

View File

@ -17,13 +17,10 @@
#include <glog/logging.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <unistd.h>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <fstream>
#include <future>
#include <set>
#include <thread>
@ -39,57 +36,6 @@
namespace mooncake {
// Default PTE (page table entry) limit per EFA NIC. EFA hardware supports
// roughly 24 million PTEs per NIC, but we use 22M as a conservative default.
// With 4KB pages: 22M × 4KB ≈ 88GB per NIC.
// With 2MB hugepages: 22M × 2MB ≈ 44TB per NIC (effectively unlimited).
// Override via MC_EFA_MAX_PTE_ENTRIES environment variable.
static constexpr size_t kDefaultMaxPteEntries = 22ULL * 1024 * 1024; // 22M
// Detect the kernel page size backing the memory at `addr` by reading
// /proc/self/smaps. Falls back to sysconf(_SC_PAGESIZE) on any failure.
static size_t detectBufferPageSize(void* addr) {
size_t fallback = static_cast<size_t>(sysconf(_SC_PAGESIZE));
std::ifstream smaps("/proc/self/smaps");
if (!smaps.is_open()) return fallback;
uintptr_t target = reinterpret_cast<uintptr_t>(addr);
std::string line;
bool in_range = false;
while (std::getline(smaps, line)) {
// VMA header: "start-end perms offset dev inode [pathname]"
if (!line.empty() && std::isxdigit(line[0])) {
unsigned long start = 0, end = 0;
if (sscanf(line.c_str(), "%lx-%lx", &start, &end) == 2) {
in_range = (target >= start && target < end);
}
} else if (in_range && line.compare(0, 15, "KernelPageSize:") == 0) {
unsigned long kb = 0;
if (sscanf(line.c_str(), "KernelPageSize: %lu kB", &kb) == 1 &&
kb > 0) {
return kb * 1024;
}
}
}
return fallback;
}
static size_t getMaxPteEntries() {
static size_t cached = []() {
const char* env = std::getenv("MC_EFA_MAX_PTE_ENTRIES");
if (env) {
size_t val = std::stoull(env);
if (val > 0) {
LOG(INFO) << "MC_EFA_MAX_PTE_ENTRIES override: " << val;
return val;
}
}
return kDefaultMaxPteEntries;
}();
return cached;
}
EfaTransport::EfaTransport() {
LOG(INFO) << "[EFA] AWS Elastic Fabric Adapter transport initialized";
}
@ -211,6 +157,9 @@ int EfaTransport::preTouchMemory(void* addr, size_t length) {
auto hwc = std::thread::hardware_concurrency();
auto num_threads = hwc > 64 ? 16 : std::min(hwc, 8u);
if (length > (size_t)globalConfig().max_mr_size) {
length = (size_t)globalConfig().max_mr_size;
}
size_t block_size = length / num_threads;
if (block_size == 0) {
return 0;
@ -256,266 +205,105 @@ int EfaTransport::registerLocalMemoryInternal(void* addr, size_t length,
bool update_metadata,
bool force_sequential) {
(void)remote_accessible;
BufferDesc buffer_desc;
const int kBaseAccessRights = IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ;
int access_rights = kBaseAccessRights;
size_t max_mr = (size_t)globalConfig().max_mr_size;
// Compute chunk limit based on EFA PTE (page table entry) constraints.
// Each EFA NIC has a hardware limit on PTE entries (~24M). The effective
// per-NIC MR size limit depends on the backing page size:
// 4KB pages → 22M × 4KB ≈ 88GB (smaller than device max_mr_size)
// 2MB hugepg → 22M × 2MB ≈ 44TB (device max_mr_size is the limit)
// We detect the actual page size of the buffer and compute accordingly,
// so hugepage-backed memory avoids unnecessary splitting.
size_t page_size = detectBufferPageSize(addr);
size_t pte_limit = getMaxPteEntries() * page_size;
// When max_mr_size is not configured, fall back to pte_limit so that
// PTE-aware splitting still kicks in for large buffers on 4KB pages.
size_t chunk_limit = (max_mr > 0) ? std::min(max_mr, pte_limit) : pte_limit;
LOG(INFO) << "Auto-split params: page_size=" << page_size
<< ", max_pte_entries=" << getMaxPteEntries()
<< ", pte_limit=" << pte_limit << ", max_mr_size=" << max_mr
<< ", chunk_limit=" << chunk_limit;
// Determine chunk boundaries
std::vector<std::pair<void*, size_t>> chunks;
if (length > chunk_limit) {
size_t offset = 0;
while (offset < length) {
size_t chunk_len = std::min(chunk_limit, length - offset);
chunks.emplace_back(static_cast<char*>(addr) + offset, chunk_len);
offset += chunk_len;
bool do_pre_touch = context_list_.size() > 0 &&
std::thread::hardware_concurrency() >= 4 &&
length >= (size_t)4 * 1024 * 1024 * 1024;
if (do_pre_touch) {
int ret = preTouchMemory(addr, length);
if (ret != 0) {
return ret;
}
LOG(WARNING) << "Auto-splitting buffer " << addr << " (" << length
<< " bytes) into " << chunks.size()
<< " chunks of <= " << chunk_limit << " bytes each";
} else {
chunks.emplace_back(addr, length);
}
// Resolve location name once (based on original buffer)
std::string resolved_name;
int use_parallel_reg = 0;
if (!force_sequential) {
use_parallel_reg = globalConfig().parallel_reg_mr;
if (use_parallel_reg == -1) {
use_parallel_reg = context_list_.size() > 1 && do_pre_touch;
}
}
auto reg_start = std::chrono::steady_clock::now();
if (use_parallel_reg) {
std::vector<std::thread> reg_threads;
reg_threads.reserve(context_list_.size());
std::vector<int> ret_codes(context_list_.size(), 0);
const int ar = access_rights;
for (size_t i = 0; i < context_list_.size(); ++i) {
reg_threads.emplace_back([this, &ret_codes, i, addr, length, ar]() {
ret_codes[i] =
context_list_[i]->registerMemoryRegion(addr, length, ar);
});
}
for (auto& thread : reg_threads) {
thread.join();
}
for (size_t i = 0; i < ret_codes.size(); ++i) {
if (ret_codes[i] != 0) {
LOG(ERROR)
<< "Failed to register memory region with EFA context "
<< i;
return ret_codes[i];
}
}
} else {
for (size_t i = 0; i < context_list_.size(); ++i) {
int ret = context_list_[i]->registerMemoryRegion(addr, length,
access_rights);
if (ret) {
LOG(ERROR)
<< "Failed to register memory region with EFA context "
<< i;
return ret;
}
}
}
auto reg_end = std::chrono::steady_clock::now();
auto reg_duration_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(reg_end -
reg_start)
.count();
if (globalConfig().trace) {
LOG(INFO) << "EFA registerMemoryRegion: addr=" << addr
<< ", length=" << length
<< ", contexts=" << context_list_.size()
<< ", parallel=" << (use_parallel_reg ? "true" : "false")
<< ", duration=" << reg_duration_ms << "ms";
}
// Collect keys from all contexts
for (auto& context : context_list_) {
buffer_desc.lkey.push_back(context->lkey(addr));
buffer_desc.rkey.push_back(context->rkey(addr));
}
if (name == kWildcardLocation) {
bool only_first_page = true;
const std::vector<MemoryLocationEntry> entries =
getMemoryLocation(addr, length, only_first_page);
if (entries.empty()) return -1;
resolved_name = entries[0].location;
buffer_desc.name = entries[0].location;
} else {
resolved_name = name;
}
// Pre-compute NIC assignments for each chunk.
// Strategy: if total PTE usage per NIC fits within the PTE budget,
// register ALL chunks on ALL NICs (full coverage → max throughput).
// Otherwise fall back to disjoint per-NIC partition.
size_t num_nics = context_list_.size();
size_t num_chunks = chunks.size();
size_t total_pages_per_nic = length / page_size;
bool use_full_coverage = (total_pages_per_nic <= getMaxPteEntries());
std::vector<std::vector<size_t>> nic_assignments(num_chunks);
if (chunks.size() <= 1) {
// Single chunk: all NICs
for (size_t n = 0; n < num_nics; ++n) {
nic_assignments[0].push_back(n);
}
} else if (use_full_coverage) {
// Multi-chunk, PTE budget OK: every chunk on every NIC
LOG(WARNING) << "Full NIC coverage: " << num_chunks << " chunks × "
<< num_nics
<< " NICs (total PTE/NIC=" << total_pages_per_nic
<< ", budget=" << getMaxPteEntries() << ")";
for (size_t ci = 0; ci < num_chunks; ++ci) {
for (size_t n = 0; n < num_nics; ++n) {
nic_assignments[ci].push_back(n);
}
}
} else if (num_chunks <= num_nics) {
// Multi-chunk, PTE exceeded, more NICs than chunks: disjoint partition
for (size_t ci = 0; ci < num_chunks; ++ci) {
size_t nics_per = num_nics / num_chunks;
size_t extra = num_nics % num_chunks;
size_t start = ci * nics_per + std::min(ci, extra);
size_t count = nics_per + (ci < extra ? 1 : 0);
for (size_t n = start; n < start + count; ++n) {
nic_assignments[ci].push_back(n);
}
}
LOG(WARNING) << "Disjoint NIC partition: PTE/NIC="
<< total_pages_per_nic
<< " exceeds budget=" << getMaxPteEntries();
for (size_t ci = 0; ci < num_chunks; ++ci) {
std::string nic_list;
for (size_t j = 0; j < nic_assignments[ci].size(); ++j) {
if (j > 0) nic_list += ",";
nic_list += std::to_string(nic_assignments[ci][j]);
}
LOG(WARNING) << " chunk " << ci << " -> NICs [" << nic_list << "]";
}
} else {
// Multi-chunk, PTE exceeded, more chunks than NICs: round-robin
// Each NIC gets multiple chunks; verify per-NIC PTE stays in budget.
size_t pte_budget = getMaxPteEntries();
std::vector<size_t> pages_per_nic(num_nics, 0);
for (size_t ci = 0; ci < num_chunks; ++ci) {
size_t nic = ci % num_nics;
size_t chunk_pages = chunks[ci].second / page_size;
pages_per_nic[nic] += chunk_pages;
nic_assignments[ci].push_back(nic);
}
bool pte_ok = true;
for (size_t n = 0; n < num_nics; ++n) {
if (pages_per_nic[n] > pte_budget) {
pte_ok = false;
break;
}
}
if (!pte_ok) {
LOG(ERROR) << "Buffer requires " << num_chunks << " chunks ("
<< length << " bytes) but per-NIC PTE budget ("
<< pte_budget << " entries, page_size=" << page_size
<< ") is exceeded even with round-robin across "
<< num_nics << " NICs";
return ERR_INVALID_ARGUMENT;
}
LOG(WARNING) << "Round-robin NIC assignment: " << num_chunks
<< " chunks across " << num_nics << " NICs";
for (size_t ci = 0; ci < num_chunks; ++ci) {
LOG(WARNING) << " chunk " << ci << " ("
<< chunks[ci].second / (1024 * 1024) << " MB) -> NIC "
<< nic_assignments[ci][0];
}
}
auto rollbackChunks = [&](size_t up_to_ci) {
for (size_t ri = 0; ri <= up_to_ci; ++ri) {
for (size_t nic_idx : nic_assignments[ri]) {
context_list_[nic_idx]->unregisterMemoryRegion(
chunks[ri].first);
}
}
};
// Register each chunk on its assigned NICs
for (size_t ci = 0; ci < chunks.size(); ++ci) {
void* chunk_addr = chunks[ci].first;
size_t chunk_len = chunks[ci].second;
const auto& assigned_nics = nic_assignments[ci];
bool do_pre_touch = context_list_.size() > 0 &&
std::thread::hardware_concurrency() >= 4 &&
chunk_len >= (size_t)4 * 1024 * 1024 * 1024;
if (do_pre_touch) {
int ret = preTouchMemory(chunk_addr, chunk_len);
if (ret != 0) {
if (ci > 0) rollbackChunks(ci - 1);
return ret;
}
}
int use_parallel_reg = 0;
if (!force_sequential) {
use_parallel_reg = globalConfig().parallel_reg_mr;
if (use_parallel_reg == -1) {
use_parallel_reg = assigned_nics.size() > 1 && do_pre_touch;
}
}
auto reg_start = std::chrono::steady_clock::now();
if (use_parallel_reg) {
std::vector<std::thread> reg_threads;
reg_threads.reserve(assigned_nics.size());
std::vector<int> ret_codes(assigned_nics.size(), 0);
const int ar = access_rights;
for (size_t j = 0; j < assigned_nics.size(); ++j) {
size_t nic_idx = assigned_nics[j];
reg_threads.emplace_back([this, &ret_codes, j, nic_idx,
chunk_addr, chunk_len, ar]() {
ret_codes[j] = context_list_[nic_idx]->registerMemoryRegion(
chunk_addr, chunk_len, ar);
});
}
for (auto& thread : reg_threads) {
thread.join();
}
for (size_t j = 0; j < ret_codes.size(); ++j) {
if (ret_codes[j] != 0) {
LOG(ERROR)
<< "Failed to register memory region chunk " << ci
<< " with EFA context " << assigned_nics[j];
rollbackChunks(ci);
return ret_codes[j];
}
}
} else {
for (size_t nic_idx : assigned_nics) {
int ret = context_list_[nic_idx]->registerMemoryRegion(
chunk_addr, chunk_len, access_rights);
if (ret) {
LOG(ERROR) << "Failed to register memory region chunk "
<< ci << " with EFA context " << nic_idx;
rollbackChunks(ci);
return ret;
}
}
}
auto reg_end = std::chrono::steady_clock::now();
auto reg_duration_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(reg_end -
reg_start)
.count();
if (globalConfig().trace) {
LOG(INFO) << "EFA registerMemoryRegion: chunk " << ci
<< ", addr=" << chunk_addr << ", length=" << chunk_len
<< ", nics=" << assigned_nics.size() << "/"
<< context_list_.size()
<< ", parallel=" << (use_parallel_reg ? "true" : "false")
<< ", duration=" << reg_duration_ms << "ms";
}
LOG(WARNING) << "Chunk " << ci << "/" << chunks.size()
<< " registered on " << assigned_nics.size() << " NICs"
<< ", addr=" << chunk_addr << ", length=" << chunk_len
<< ", duration=" << reg_duration_ms << "ms";
// Collect keys: assigned NICs have valid keys, others get 0
BufferDesc buffer_desc;
for (auto& context : context_list_) {
buffer_desc.lkey.push_back(context->lkey(chunk_addr));
buffer_desc.rkey.push_back(context->rkey(chunk_addr));
}
buffer_desc.name = resolved_name;
buffer_desc.addr = (uint64_t)chunk_addr;
buffer_desc.length = chunk_len;
int rc = metadata_->addLocalMemoryBuffer(buffer_desc, update_metadata);
if (rc) {
rollbackChunks(ci);
return rc;
}
}
// Track chunks and NIC assignments for unregistration
if (chunks.size() > 1) {
std::lock_guard<std::mutex> lock(chunk_map_mutex_);
std::vector<ChunkRegistration> regs;
regs.reserve(chunks.size());
for (size_t ci = 0; ci < chunks.size(); ++ci) {
regs.push_back({(uint64_t)chunks[ci].first, nic_assignments[ci]});
}
chunk_map_[(uint64_t)addr] = std::move(regs);
buffer_desc.name = name;
}
buffer_desc.addr = (uint64_t)addr;
buffer_desc.length = length;
int rc = metadata_->addLocalMemoryBuffer(buffer_desc, update_metadata);
if (rc) return rc;
return 0;
}
@ -526,40 +314,6 @@ int EfaTransport::unregisterLocalMemory(void* addr, bool update_metadata) {
int EfaTransport::unregisterLocalMemoryInternal(void* addr,
bool update_metadata,
bool force_sequential) {
// Check if this buffer was split into chunks (per-NIC partition)
std::vector<ChunkRegistration> chunk_regs;
{
std::lock_guard<std::mutex> lock(chunk_map_mutex_);
auto it = chunk_map_.find((uint64_t)addr);
if (it != chunk_map_.end()) {
chunk_regs = std::move(it->second);
chunk_map_.erase(it);
}
}
if (!chunk_regs.empty()) {
// Unregister each chunk from its assigned NICs only
for (auto& reg : chunk_regs) {
void* ca = (void*)reg.addr;
int rc = metadata_->removeLocalMemoryBuffer(ca, update_metadata);
if (rc) {
LOG(ERROR) << "Failed to remove chunk metadata at " << ca;
return rc;
}
for (size_t nic_idx : reg.nic_indices) {
int ret = context_list_[nic_idx]->unregisterMemoryRegion(ca);
if (ret) {
LOG(ERROR) << "Failed to unregister chunk " << ca
<< " with EFA context " << nic_idx;
return ret;
}
}
}
return 0;
}
// Non-chunked buffer: original path
int rc = metadata_->removeLocalMemoryBuffer(addr, update_metadata);
if (rc) return rc;
@ -669,79 +423,6 @@ int EfaTransport::unregisterLocalMemoryBatch(
return metadata_->updateLocalSegmentDesc();
}
int EfaTransport::warmupSegment(const std::string& segment_name) {
if (!metadata_) {
LOG(ERROR) << "EfaTransport::warmupSegment: metadata_ is null";
return ERR_INVALID_ARGUMENT;
}
if (segment_name.empty() || segment_name == local_server_name_) {
// Loopback / empty name — nothing to pre-connect.
return 0;
}
auto desc = metadata_->getSegmentDescByName(segment_name);
if (!desc) {
LOG(ERROR) << "EfaTransport::warmupSegment: segment '" << segment_name
<< "' not found in metadata (did you openSegment() first?)";
return ERR_INVALID_ARGUMENT;
}
if (desc->devices.empty()) {
LOG(WARNING) << "EfaTransport::warmupSegment: segment '" << segment_name
<< "' has no devices";
return 0;
}
// Build peer_nic_path list: "<segment_name>@<device_name>" for each NIC.
std::vector<std::string> peer_paths;
peer_paths.reserve(desc->devices.size());
for (const auto& dev : desc->devices) {
peer_paths.emplace_back(segment_name + "@" + dev.name);
}
// Warm up every (local_ctx, peer_nic) pair concurrently. Each
// EfaContext::endpoint() + setupConnectionsByActive() is idempotent and
// takes its own lock, so parallel calls across distinct peer_nic_paths
// (and distinct contexts) are safe. We use std::async to get roughly
// per-pair parallelism — the critical path is now max(handshake RTT) not
// sum(handshake RTT).
auto t0 = std::chrono::steady_clock::now();
size_t n_pairs = context_list_.size() * peer_paths.size();
std::vector<std::future<int>> futs;
futs.reserve(n_pairs);
for (auto& ctx : context_list_) {
for (const auto& path : peer_paths) {
futs.emplace_back(
std::async(std::launch::async, [ctx, path]() -> int {
auto ep = ctx->endpoint(path);
if (!ep) {
LOG(WARNING) << "warmupSegment: endpoint() returned "
"null for "
<< path;
return -1;
}
if (ep->connected()) return 0;
return ep->setupConnectionsByActive();
}));
}
}
int ok = 0, fail = 0;
for (auto& f : futs) {
int rc = f.get();
if (rc == 0)
++ok;
else
++fail;
}
auto elapsed =
std::chrono::duration<double>(std::chrono::steady_clock::now() - t0)
.count();
LOG(INFO) << "EfaTransport::warmupSegment('" << segment_name << "'): " << ok
<< "/" << n_pairs << " endpoints connected (" << fail
<< " failed) in " << elapsed << "s (" << context_list_.size()
<< " local NICs × " << peer_paths.size() << " peer NICs)";
return fail == 0 ? 0 : ERR_ENDPOINT;
}
Status EfaTransport::submitTransfer(
BatchID batch_id, const std::vector<TransferRequest>& entries) {
auto& batch_desc = *((BatchDesc*)(batch_id));
@ -806,9 +487,7 @@ Status EfaTransport::submitTransferTask(
if (static_cast<size_t>(request_buffer_id) <
local_segment_desc->buffers.size() &&
local_segment_desc->buffers[request_buffer_id].lkey.size() >
static_cast<size_t>(d) &&
local_segment_desc->buffers[request_buffer_id].lkey[d] !=
0) {
static_cast<size_t>(d)) {
active_devs.push_back({d, ctx});
}
}
@ -1109,39 +788,6 @@ int EfaTransport::initializeEfaResources() {
LOG(ERROR) << "EfaTransport: No available EFA devices";
return ERR_DEVICE_NOT_FOUND;
}
// Query EFA device max_mr_size via ibverbs and clamp globalConfig.
// libfabric does not expose max_mr_size, so we go through the ibverbs
// layer.
{
int num_devices = 0;
struct ibv_device** dev_list = ibv_get_device_list(&num_devices);
if (dev_list) {
const std::string& first_efa = efa_devices[0];
for (int i = 0; i < num_devices; ++i) {
if (first_efa == ibv_get_device_name(dev_list[i])) {
struct ibv_context* ctx = ibv_open_device(dev_list[i]);
if (ctx) {
struct ibv_device_attr attr;
if (ibv_query_device(ctx, &attr) == 0) {
auto& config = globalConfig();
if (config.max_mr_size >
(uint64_t)attr.max_mr_size) {
config.max_mr_size = attr.max_mr_size;
LOG(INFO) << "EfaTransport: Clamped "
"max_mr_size to device limit: "
<< config.max_mr_size;
}
}
ibv_close_device(ctx);
}
break;
}
}
ibv_free_device_list(dev_list);
}
}
return 0;
}
@ -1167,31 +813,16 @@ int EfaTransport::selectDevice(SegmentDesc* desc, uint64_t offset,
continue;
}
// Try multiple attempts to find a device with valid MR registration.
// With per-NIC partition, not all devices have all buffers registered,
// so rkey[device_id] may be 0 for unassigned NICs.
int num_devices = static_cast<int>(desc->devices.size());
for (int attempt = 0; attempt < num_devices; ++attempt) {
int try_count = retry_count + attempt;
device_id =
hint.empty()
? desc->topology.selectDevice(buffer.name, try_count)
: desc->topology.selectDevice(buffer.name, hint, try_count);
if (device_id >= 0 &&
static_cast<size_t>(device_id) < buffer.rkey.size() &&
buffer.rkey[device_id] != 0) {
return 0;
}
device_id = hint.empty() ? desc->topology.selectDevice(
kWildcardLocation, try_count)
: desc->topology.selectDevice(
kWildcardLocation, hint, try_count);
if (device_id >= 0 &&
static_cast<size_t>(device_id) < buffer.rkey.size() &&
buffer.rkey[device_id] != 0) {
return 0;
}
}
device_id =
hint.empty()
? desc->topology.selectDevice(buffer.name, retry_count)
: desc->topology.selectDevice(buffer.name, hint, retry_count);
if (device_id >= 0) return 0;
device_id = hint.empty() ? desc->topology.selectDevice(
kWildcardLocation, retry_count)
: desc->topology.selectDevice(
kWildcardLocation, hint, retry_count);
if (device_id >= 0) return 0;
}
return ERR_ADDRESS_NOT_REGISTERED;
}

View File

@ -35,17 +35,6 @@ std::shared_ptr<RdmaEndPoint> FIFOEndpointStore::getEndpoint(
return nullptr;
}
std::shared_ptr<RdmaEndPoint> FIFOEndpointStore::getEndpointByPtr(
const RdmaEndPoint *endpoint_ptr) {
RWSpinlock::ReadGuard guard(endpoint_map_lock_);
for (auto &kv : endpoint_map_) {
if (kv.second.get() == endpoint_ptr) return kv.second;
}
for (auto &endpoint : waiting_list_)
if (endpoint.get() == endpoint_ptr) return endpoint;
return nullptr;
}
std::shared_ptr<RdmaEndPoint> FIFOEndpointStore::insertEndpoint(
const std::string &peer_nic_path, RdmaContext *context) {
RWSpinlock::WriteGuard guard(endpoint_map_lock_);
@ -150,17 +139,6 @@ std::shared_ptr<RdmaEndPoint> SIEVEEndpointStore::getEndpoint(
return nullptr;
}
std::shared_ptr<RdmaEndPoint> SIEVEEndpointStore::getEndpointByPtr(
const RdmaEndPoint *endpoint_ptr) {
RWSpinlock::ReadGuard guard(endpoint_map_lock_);
for (auto &kv : endpoint_map_) {
if (kv.second.first.get() == endpoint_ptr) return kv.second.first;
}
for (auto &endpoint : waiting_list_)
if (endpoint.get() == endpoint_ptr) return endpoint;
return nullptr;
}
std::shared_ptr<RdmaEndPoint> SIEVEEndpointStore::insertEndpoint(
const std::string &peer_nic_path, RdmaContext *context) {
RWSpinlock::WriteGuard guard(endpoint_map_lock_);

View File

@ -357,11 +357,6 @@ std::shared_ptr<RdmaEndPoint> RdmaContext::endpoint(
return endpoint;
}
std::shared_ptr<RdmaEndPoint> RdmaContext::getEndpointByPtr(
const RdmaEndPoint *endpoint_ptr) {
return endpoint_store_->getEndpointByPtr(endpoint_ptr);
}
int RdmaContext::disconnectAllEndpoints() {
return endpoint_store_->disconnectQPs();
}

View File

@ -161,8 +161,9 @@ int WorkerPool::submitPostSend(
slice_queue_lock_[shard_id].unlock();
}
submitted_slice_count_.fetch_add(submitted_slice_count);
if (suspended_flag_.load()) {
submitted_slice_count_.fetch_add(submitted_slice_count,
std::memory_order_relaxed);
if (suspended_flag_.load(std::memory_order_relaxed)) {
std::lock_guard<std::mutex> lock(cond_mutex_);
cond_var_.notify_all();
}
@ -397,7 +398,7 @@ void WorkerPool::transferWorker(int thread_id) {
// Double-check condition after acquiring lock to avoid lost
// wakeup
if (processed_slice_count_.load(std::memory_order_relaxed) ==
submitted_slice_count_.load()) {
submitted_slice_count_.load(std::memory_order_relaxed)) {
cond_var_.wait_for(lock, std::chrono::seconds(1));
}
suspended_flag_.fetch_sub(1);
@ -414,62 +415,19 @@ void WorkerPool::transferWorker(int thread_id) {
int WorkerPool::doProcessContextEvents() {
ibv_async_event event;
bool event_acked = false;
if (ibv_get_async_event(context_.context(), &event) < 0) return ERR_CONTEXT;
LOG(WARNING) << "Worker: Received context async event "
<< ibv_event_type_str(event.event_type) << " for context "
<< context_.deviceName();
if (event.event_type == IBV_EVENT_QP_FATAL) {
auto endpoint_ptr = (RdmaEndPoint *)event.element.qp->qp_context;
/**
* There might be a deadlock if we call endpoint->set_active(false)
* before ack the event:
*
* Thread A:
* Holding endpoint->lock_ and calling ibv_destroy_qp (if using
* eRDMA), ibv_destroy_qp will block until the event is acked.
*
* Thread B (this thread):
* Calling endpoint->set_active(false), which blocks as
* endpoint->lock_ is held by Thread A.
*/
ibv_ack_async_event(&event);
event_acked = true;
/**
* After ack the event, the endpoint might be destroyed if it happened
* to be destroying event.element.qp. Therefore, we cannot just
* dereference endpoint_ptr. Instead, we need to get the shared_ptr of
* the endpoint from context_ and use that shared_ptr to access the
* endpoint.
*/
auto endpoint = context_.getEndpointByPtr(endpoint_ptr);
if (endpoint) {
endpoint->set_active(false);
}
auto endpoint = (RdmaEndPoint *)event.element.qp->qp_context;
endpoint->set_active(false);
} else if (event.event_type == IBV_EVENT_DEVICE_FATAL ||
event.event_type == IBV_EVENT_CQ_ERR ||
event.event_type == IBV_EVENT_WQ_FATAL ||
event.event_type == IBV_EVENT_PORT_ERR ||
event.event_type == IBV_EVENT_LID_CHANGE) {
context_.set_active(false);
/**
* Similar deadlock might happen if we call
* context_.disconnectAllEndpoints() before ack the event:
*
* Thread A:
* Holding endpoint->lock_ and calling ibv_destroy_qp (if using
* eRDMA), ibv_destroy_qp will block until the event is acked.
*
* Thread B (this thread):
* Calling endpoint->disconnect(), which blocks as endpoint->lock_
* is held by Thread A.
*/
ibv_ack_async_event(&event);
event_acked = true;
context_.disconnectAllEndpoints();
LOG(INFO) << "Worker: Context " << context_.deviceName()
<< " is now inactive";
@ -478,11 +436,7 @@ int WorkerPool::doProcessContextEvents() {
LOG(INFO) << "Worker: Context " << context_.deviceName()
<< " is now active";
}
if (!event_acked) {
ibv_ack_async_event(&event);
}
ibv_ack_async_event(&event);
return 0;
}

View File

@ -1,213 +0,0 @@
// Copyright 2026 KVCache.AI
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef TENT_FAULT_PROXY_TRANSPORT_H
#define TENT_FAULT_PROXY_TRANSPORT_H
#include <atomic>
#include <cassert>
#include <chrono>
#include <memory>
#include <random>
#include <thread>
#include "tent/common/status.h"
#include "tent/runtime/transport.h"
namespace mooncake {
namespace tent {
// Configurable fault injection policy. All fields default to "no fault".
struct FaultPolicy {
// Probability [0.0, 1.0] that submitTransferTasks() returns an error.
double submit_fail_rate = 0.0;
// Probability [0.0, 1.0] that getTransferStatus() flips COMPLETED→FAILED.
double status_corrupt_rate = 0.0;
// Artificial latency (microseconds) added before each submit.
uint64_t submit_delay_us = 0;
// Deterministic mode: succeed for the first N submits, then always fail.
// -1 disables this mode (rate-based mode used instead).
int fail_after_n_submits = -1;
// If true, install() returns an error immediately.
bool fail_install = false;
};
// Decorator that wraps any Transport and injects faults according to a
// FaultPolicy. The engine sees a normal Transport; failures trigger the
// real failover / retry machinery without requiring hardware.
class FaultProxyTransport : public Transport {
public:
FaultProxyTransport(std::shared_ptr<Transport> real, FaultPolicy policy)
: real_(std::move(real)), policy_(policy), submit_count_(0) {
assert(real_ && "FaultProxyTransport: real transport must not be null");
}
// -- Lifecycle -----------------------------------------------------------
Status install(std::string& local_segment_name,
std::shared_ptr<ControlService> metadata,
std::shared_ptr<Topology> local_topology,
std::shared_ptr<Config> conf = nullptr) override {
if (policy_.fail_install) {
return Status::InternalError(
"fault injected: install failure" LOC_MARK);
}
return real_->install(local_segment_name, metadata, local_topology,
conf);
}
Status uninstall() override { return real_->uninstall(); }
// -- Capabilities --------------------------------------------------------
const Capabilities capabilities() const override {
return real_->capabilities();
}
// -- Batch management ----------------------------------------------------
Status allocateSubBatch(SubBatchRef& batch, size_t max_size) override {
return real_->allocateSubBatch(batch, max_size);
}
Status freeSubBatch(SubBatchRef& batch) override {
return real_->freeSubBatch(batch);
}
// -- Transfer (primary injection points) ---------------------------------
Status submitTransferTasks(
SubBatchRef batch, const std::vector<Request>& request_list) override {
// Artificial delay
if (policy_.submit_delay_us > 0) {
std::this_thread::sleep_for(
std::chrono::microseconds(policy_.submit_delay_us));
}
int count = submit_count_.fetch_add(1, std::memory_order_relaxed);
// Deterministic count-based failure (takes precedence over rate-based)
if (policy_.fail_after_n_submits >= 0 &&
count >= policy_.fail_after_n_submits) {
return Status::InternalError(
"fault injected: submit failure (count)" LOC_MARK);
}
// Rate-based probabilistic failure
if (policy_.submit_fail_rate > 0.0 &&
randomDouble() < policy_.submit_fail_rate) {
return Status::InternalError(
"fault injected: submit failure (rate)" LOC_MARK);
}
return real_->submitTransferTasks(batch, request_list);
}
Status getTransferStatus(SubBatchRef batch, int task_id,
TransferStatus& status) override {
auto s = real_->getTransferStatus(batch, task_id, status);
if (!s.ok()) return s;
// Status corruption: flip COMPLETED → FAILED
if (status.s == TransferStatusEnum::COMPLETED &&
policy_.status_corrupt_rate > 0.0 &&
randomDouble() < policy_.status_corrupt_rate) {
status.s = TransferStatusEnum::FAILED;
}
return s;
}
// -- Memory management (pass-through) ------------------------------------
Status addMemoryBuffer(BufferDesc& desc,
const MemoryOptions& options) override {
return real_->addMemoryBuffer(desc, options);
}
Status addMemoryBuffer(std::vector<BufferDesc>& desc_list,
const MemoryOptions& options) override {
return real_->addMemoryBuffer(desc_list, options);
}
Status removeMemoryBuffer(BufferDesc& desc) override {
return real_->removeMemoryBuffer(desc);
}
Status allocateLocalMemory(void** addr, size_t size,
MemoryOptions& options) override {
return real_->allocateLocalMemory(addr, size, options);
}
Status freeLocalMemory(void* addr, size_t size) override {
return real_->freeLocalMemory(addr, size);
}
bool warmupMemory(void* addr, size_t length) override {
return real_->warmupMemory(addr, length);
}
// -- Notifications (pass-through) ----------------------------------------
bool supportNotification() const override {
return real_->supportNotification();
}
Status sendNotification(SegmentID target_id,
const Notification& notify) override {
return real_->sendNotification(target_id, notify);
}
Status receiveNotification(
std::vector<Notification>& notify_list) override {
return real_->receiveNotification(notify_list);
}
// -- Identity ------------------------------------------------------------
const char* getName() const override { return "<fault-proxy>"; }
// -- Test helpers --------------------------------------------------------
// Note: resetPolicy() is intended for single-threaded test scenarios.
// It is NOT safe to call concurrently with submitTransferTasks().
void resetPolicy(FaultPolicy new_policy) {
policy_ = new_policy;
submit_count_.store(0, std::memory_order_relaxed);
}
int submitCount() const {
return submit_count_.load(std::memory_order_relaxed);
}
private:
// Thread-safe random double in [0.0, 1.0).
static double randomDouble() {
thread_local std::mt19937 rng(std::random_device{}());
thread_local std::uniform_real_distribution<double> dist(0.0, 1.0);
return dist(rng);
}
std::shared_ptr<Transport> real_;
FaultPolicy policy_;
std::atomic<int> submit_count_;
};
} // namespace tent
} // namespace mooncake
#endif // TENT_FAULT_PROXY_TRANSPORT_H

View File

@ -37,7 +37,6 @@ struct MnnvlTask {
volatile size_t transferred_bytes;
uint64_t target_addr = 0;
int cuda_id = 0;
cudaEvent_t completion_event = nullptr;
};
struct MnnvlSubBatch : public Transport::SubBatch {
@ -84,7 +83,7 @@ class MnnvlTransport : public Transport {
virtual Status freeLocalMemory(void *addr, size_t size);
private:
void startTransfer(std::vector<MnnvlTask *> &tasks, MnnvlSubBatch *batch);
void startTransfer(MnnvlTask *task, MnnvlSubBatch *batch);
void *createSharedMemory(const std::string &path, size_t size);

View File

@ -40,7 +40,6 @@ struct NVLinkTask {
uint64_t target_addr = 0;
bool is_cuda_ipc;
int cuda_id = 0;
cudaEvent_t completion_event = nullptr;
};
struct NVLinkSubBatch : public Transport::SubBatch {
@ -82,7 +81,7 @@ class NVLinkTransport : public Transport {
virtual const char *getName() const { return "nvlink"; }
private:
void startTransfer(std::vector<NVLinkTask *> &tasks, NVLinkSubBatch *batch);
void startTransfer(NVLinkTask *task, NVLinkSubBatch *batch);
void *createSharedMemory(const std::string &path, size_t size);

View File

@ -7,7 +7,7 @@ add_library(tent_metrics STATIC ${TENT_METRICS_SOURCES})
# ODR safety: yalantinglibs bundles ASIO headers but does NOT compile ASIO inline because
# ASIO_SEPARATE_COMPILATION is set globally. All ASIO symbols live exclusively in asio_shared.so,
# so there is no risk of duplicate symbols between yalantinglibs and the rest of TE.
# (See mooncake-common/src/CMakeLists.txt: "Build asio as a shared library to avoid ODR violations")
# (See mooncake-asio/CMakeLists.txt: "Build asio as a shared library to avoid ODR violations")
target_link_libraries(tent_metrics PUBLIC tent_common tent_interface yalantinglibs::yalantinglibs glog pthread)
# Pass compile definition based on option
@ -17,4 +17,4 @@ if(TENT_METRICS_ENABLED)
else()
target_compile_definitions(tent_metrics PUBLIC TENT_METRICS_ENABLED=0)
message(STATUS "TENT metrics: DISABLED (zero overhead)")
endif()
endif()

View File

@ -168,7 +168,6 @@ Status MnnvlTransport::submitTransferTasks(
if (request_list.size() + mnnvl_batch->task_list.size() >
mnnvl_batch->max_size)
return Status::TooManyRequests("Exceed batch capacity" LOC_MARK);
std::vector<MnnvlTask *> new_tasks;
for (auto &request : request_list) {
mnnvl_batch->task_list.push_back(MnnvlTask{});
auto &task = mnnvl_batch->task_list[mnnvl_batch->task_list.size() - 1];
@ -185,84 +184,72 @@ Status MnnvlTransport::submitTransferTasks(
task.target_addr = target_addr;
task.request = request;
task.status_word = TransferStatusEnum::PENDING;
new_tasks.push_back(&task);
startTransfer(&task, mnnvl_batch);
}
startTransfer(new_tasks, mnnvl_batch);
return Status::OK();
}
void MnnvlTransport::startTransfer(std::vector<MnnvlTask *> &tasks,
MnnvlSubBatch *batch) {
if (tasks.empty()) return;
std::vector<void *> srcs;
std::vector<void *> dsts;
std::vector<size_t> sizes;
for (auto *task : tasks) {
void *src = nullptr, *dst = nullptr;
if (task->request.opcode == Request::READ) {
dst = task->request.source; // read into source buffer
src = (void *)task->target_addr; // from remote
} else {
src = task->request.source; // write from source buffer
dst = (void *)task->target_addr; // to remote
}
srcs.push_back(src);
dsts.push_back(dst);
sizes.push_back(task->request.length);
}
void MnnvlTransport::startTransfer(MnnvlTask *task, MnnvlSubBatch *batch) {
cudaError_t err;
void *src = nullptr, *dst = nullptr;
#if CUDART_VERSION >= 13000
cudaMemcpyAttributes attr{};
attr.srcAccessOrder = cudaMemcpySrcAccessOrderStream;
size_t attrs_idx = 0;
err = cudaMemcpyBatchAsync(const_cast<const void **>(dsts.data()),
const_cast<const void **>(srcs.data()),
sizes.data(), srcs.size(), &attr, &attrs_idx, 1,
batch->async_stream.get());
#elif CUDART_VERSION >= 12080
cudaMemcpyAttributes attr{};
attr.srcAccessOrder = cudaMemcpySrcAccessOrderStream;
size_t attrs_idx = 0;
size_t fail_idx = tasks.size();
err = cudaMemcpyBatchAsync(dsts.data(), srcs.data(), sizes.data(),
srcs.size(), &attr, &attrs_idx, 1, &fail_idx,
batch->async_stream.get());
if (err != cudaSuccess && fail_idx < tasks.size()) {
LOG(ERROR) << "MnnvlTransport::startTransfer internal error: "
<< "cudaMemcpyBatchAsync failed at task index " << fail_idx
<< " (src=" << srcs[fail_idx] << ", dst=" << dsts[fail_idx]
<< ", size=" << sizes[fail_idx]
<< "): " << cudaGetErrorString(err);
tasks[fail_idx]->status_word = TransferStatusEnum::FAILED;
// Determine direction and addresses
if (task->request.opcode == Request::READ) {
dst = task->request.source; // read into source buffer
src = (void *)task->target_addr; // from remote
} else {
src = task->request.source; // write from source buffer
dst = (void *)task->target_addr; // to remote
}
#else
err = cudaSuccess;
for (size_t i = 0; i < tasks.size(); ++i) {
auto single_err =
cudaMemcpyAsync(dsts[i], srcs[i], sizes[i], cudaMemcpyDefault,
batch->async_stream.get());
if (single_err != cudaSuccess) {
tasks[i]->status_word = TransferStatusEnum::FAILED;
err = single_err;
}
}
#endif
if (err != cudaSuccess) {
for (auto *task : tasks) {
if (task->status_word == TransferStatusEnum::PENDING)
task->status_word = TransferStatusEnum::FAILED;
bool is_async = (task->request.length >= async_memcpy_threshold_);
cudaPointerAttributes src_attr_info, dst_attr_info;
cudaMemoryType src_type = cudaMemoryTypeHost, dst_type = cudaMemoryTypeHost;
if (cudaPointerGetAttributes(&src_attr_info, src) == cudaSuccess) {
src_type = src_attr_info.type;
}
if (cudaPointerGetAttributes(&dst_attr_info, dst) == cudaSuccess) {
dst_type = dst_attr_info.type;
}
cudaMemcpyKind kind = cudaMemcpyDefault;
if (src_type == cudaMemoryTypeDevice && dst_type == cudaMemoryTypeHost) {
kind = cudaMemcpyDeviceToHost;
} else if (src_type == cudaMemoryTypeHost &&
dst_type == cudaMemoryTypeDevice) {
kind = cudaMemcpyHostToDevice;
} else if (src_type == cudaMemoryTypeDevice &&
dst_type == cudaMemoryTypeDevice) {
kind = cudaMemcpyDeviceToDevice;
} else if (src_type == cudaMemoryTypeHost &&
dst_type == cudaMemoryTypeHost) {
kind = cudaMemcpyHostToHost;
}
if (!is_async) {
err = cudaMemcpyAsync(dst, src, task->request.length, kind,
batch->sync_stream.get());
if (err != cudaSuccess) {
task->status_word = TransferStatusEnum::FAILED;
return;
}
err = cudaStreamSynchronize(batch->sync_stream.get());
if (err != cudaSuccess) {
task->status_word = TransferStatusEnum::FAILED;
return;
}
task->transferred_bytes = task->request.length;
task->status_word = TransferStatusEnum::COMPLETED;
return;
}
cudaEvent_t event;
cudaEventCreateWithFlags(&event, cudaEventDisableTiming);
cudaEventRecord(event, batch->async_stream.get());
for (auto *task : tasks) task->completion_event = event;
err = cudaMemcpyAsync(dst, src, task->request.length, kind,
batch->async_stream.get());
if (err != cudaSuccess) task->status_word = TransferStatusEnum::FAILED;
}
Status MnnvlTransport::getTransferStatus(SubBatchRef batch, int task_id,
@ -274,8 +261,9 @@ Status MnnvlTransport::getTransferStatus(SubBatchRef batch, int task_id,
auto &task = mnnvl_batch->task_list[task_id];
status = TransferStatus{task.status_word, task.transferred_bytes};
if (task.status_word == TransferStatusEnum::PENDING) {
auto err = cudaEventQuery(task.completion_event);
auto err = cudaStreamQuery(mnnvl_batch->async_stream.get());
if (err == cudaSuccess) {
cudaStreamSynchronize(mnnvl_batch->async_stream.get());
task.transferred_bytes = task.request.length;
task.status_word = TransferStatusEnum::COMPLETED;
} else if (err != cudaErrorNotReady) {

View File

@ -106,7 +106,6 @@ Status NVLinkTransport::submitTransferTasks(
return Status::InvalidArgument("Invalid NVLink sub-batch" LOC_MARK);
if (request_list.size() + shm_batch->task_list.size() > shm_batch->max_size)
return Status::TooManyRequests("Exceed batch capacity" LOC_MARK);
std::vector<NVLinkTask*> new_tasks;
for (auto& request : request_list) {
shm_batch->task_list.push_back(NVLinkTask{});
auto& task = shm_batch->task_list[shm_batch->task_list.size() - 1];
@ -119,83 +118,72 @@ Status NVLinkTransport::submitTransferTasks(
task.target_addr = target_addr;
task.request = request;
task.status_word = TransferStatusEnum::PENDING;
new_tasks.push_back(&task);
startTransfer(&task, shm_batch);
}
startTransfer(new_tasks, shm_batch);
return Status::OK();
}
void NVLinkTransport::startTransfer(std::vector<NVLinkTask*>& tasks,
NVLinkSubBatch* batch) {
if (tasks.empty()) return;
std::vector<void*> srcs;
std::vector<void*> dsts;
std::vector<size_t> sizes;
for (auto* task : tasks) {
void *src = nullptr, *dst = nullptr;
if (task->request.opcode == Request::READ) {
dst = task->request.source; // read into source buffer
src = (void*)task->target_addr; // from remote
} else {
src = task->request.source; // write from source buffer
dst = (void*)task->target_addr; // to remote
}
srcs.push_back(src);
dsts.push_back(dst);
sizes.push_back(task->request.length);
}
void NVLinkTransport::startTransfer(NVLinkTask* task, NVLinkSubBatch* batch) {
cudaError_t err;
void *src = nullptr, *dst = nullptr;
#if CUDART_VERSION >= 13000
cudaMemcpyAttributes attr{};
attr.srcAccessOrder = cudaMemcpySrcAccessOrderStream;
size_t attrs_idx = 0;
err = cudaMemcpyBatchAsync(const_cast<const void**>(dsts.data()),
const_cast<const void**>(srcs.data()),
sizes.data(), srcs.size(), &attr, &attrs_idx, 1,
batch->async_stream.get());
#elif CUDART_VERSION >= 12080
cudaMemcpyAttributes attr{};
attr.srcAccessOrder = cudaMemcpySrcAccessOrderStream;
size_t attrs_idx = 0;
size_t fail_idx = tasks.size();
err = cudaMemcpyBatchAsync(dsts.data(), srcs.data(), sizes.data(),
srcs.size(), &attr, &attrs_idx, 1, &fail_idx,
batch->async_stream.get());
if (err != cudaSuccess && fail_idx < tasks.size()) {
LOG(ERROR) << "NVLinkTransport::startTransfer internal error: "
<< "cudaMemcpyBatchAsync failed at task index " << fail_idx
<< " (src=" << srcs[fail_idx] << ", dst=" << dsts[fail_idx]
<< ", size=" << sizes[fail_idx]
<< "): " << cudaGetErrorString(err);
tasks[fail_idx]->status_word = TransferStatusEnum::FAILED;
// Determine direction and addresses
if (task->request.opcode == Request::READ) {
dst = task->request.source; // read into source buffer
src = (void*)task->target_addr; // from remote
} else {
src = task->request.source; // write from source buffer
dst = (void*)task->target_addr; // to remote
}
#else
err = cudaSuccess;
for (size_t i = 0; i < tasks.size(); ++i) {
auto single_err =
cudaMemcpyAsync(dsts[i], srcs[i], sizes[i], cudaMemcpyDefault,
batch->async_stream.get());
if (single_err != cudaSuccess) {
tasks[i]->status_word = TransferStatusEnum::FAILED;
err = single_err;
bool is_async = (task->request.length >= async_memcpy_threshold_);
cudaPointerAttributes src_attr_info, dst_attr_info;
cudaMemoryType src_type = cudaMemoryTypeHost, dst_type = cudaMemoryTypeHost;
if (cudaPointerGetAttributes(&src_attr_info, src) == cudaSuccess) {
src_type = src_attr_info.type;
}
if (cudaPointerGetAttributes(&dst_attr_info, dst) == cudaSuccess) {
dst_type = dst_attr_info.type;
}
cudaMemcpyKind kind = cudaMemcpyDefault;
if (src_type == cudaMemoryTypeDevice && dst_type == cudaMemoryTypeHost) {
kind = cudaMemcpyDeviceToHost;
} else if (src_type == cudaMemoryTypeHost &&
dst_type == cudaMemoryTypeDevice) {
kind = cudaMemcpyHostToDevice;
} else if (src_type == cudaMemoryTypeDevice &&
dst_type == cudaMemoryTypeDevice) {
kind = cudaMemcpyDeviceToDevice;
} else if (src_type == cudaMemoryTypeHost &&
dst_type == cudaMemoryTypeHost) {
kind = cudaMemcpyHostToHost;
}
if (!is_async) {
err = cudaMemcpyAsync(dst, src, task->request.length, kind,
batch->sync_stream.get());
if (err != cudaSuccess) {
task->status_word = TransferStatusEnum::FAILED;
return;
}
}
#endif
if (err != cudaSuccess) {
for (auto* task : tasks) {
if (task->status_word == TransferStatusEnum::PENDING)
task->status_word = TransferStatusEnum::FAILED;
err = cudaStreamSynchronize(batch->sync_stream.get());
if (err != cudaSuccess) {
task->status_word = TransferStatusEnum::FAILED;
return;
}
task->transferred_bytes = task->request.length;
task->status_word = TransferStatusEnum::COMPLETED;
return;
}
cudaEvent_t event;
cudaEventCreateWithFlags(&event, cudaEventDisableTiming);
cudaEventRecord(event, batch->async_stream.get());
for (auto* task : tasks) task->completion_event = event;
err = cudaMemcpyAsync(dst, src, task->request.length, kind,
batch->async_stream.get());
if (err != cudaSuccess) task->status_word = TransferStatusEnum::FAILED;
}
Status NVLinkTransport::getTransferStatus(SubBatchRef batch, int task_id,
@ -207,8 +195,9 @@ Status NVLinkTransport::getTransferStatus(SubBatchRef batch, int task_id,
auto& task = shm_batch->task_list[task_id];
status = TransferStatus{task.status_word, task.transferred_bytes};
if (task.status_word == TransferStatusEnum::PENDING) {
auto err = cudaEventQuery(task.completion_event);
auto err = cudaStreamQuery(shm_batch->async_stream.get());
if (err == cudaSuccess) {
cudaStreamSynchronize(shm_batch->async_stream.get());
task.transferred_bytes = task.request.length;
task.status_word = TransferStatusEnum::COMPLETED;
} else if (err != cudaErrorNotReady) {

View File

@ -57,10 +57,3 @@ add_test(NAME tent_failover_test COMMAND tent_failover_test)
add_executable(tent_endpoint_lifecycle_test endpoint_lifecycle_test.cpp)
target_link_libraries(tent_endpoint_lifecycle_test PRIVATE gtest gtest_main)
add_test(NAME tent_endpoint_lifecycle_test COMMAND tent_endpoint_lifecycle_test)
add_executable(tent_fault_proxy_test fault_proxy_test.cpp)
target_link_libraries(tent_fault_proxy_test PRIVATE gtest gtest_main
tent_link_group)
target_include_directories(tent_fault_proxy_test
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
add_test(NAME tent_fault_proxy_test COMMAND tent_fault_proxy_test)

View File

@ -1,365 +0,0 @@
// Copyright 2026 KVCache.AI
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include "tent/common/types.h"
#include "tent/runtime/transfer_engine_impl.h"
#include "tent/transport/fault_proxy/fault_proxy_transport.h"
namespace mooncake {
namespace tent {
namespace {
// ---------------------------------------------------------------------------
// Minimal FakeTransport: always succeeds, tracks call counts.
// ---------------------------------------------------------------------------
class FakeSubBatch : public Transport::SubBatch {
public:
size_t size() const override { return task_count; }
size_t task_count = 0;
// Store status per task for getTransferStatus
std::vector<TransferStatus> statuses;
};
class FakeTransport : public Transport {
public:
int install_calls = 0;
int submit_calls = 0;
int status_calls = 0;
Status install(std::string& /*local_segment_name*/,
std::shared_ptr<ControlService> /*metadata*/,
std::shared_ptr<Topology> /*local_topology*/,
std::shared_ptr<Config> /*conf*/ = nullptr) override {
++install_calls;
return Status::OK();
}
Status allocateSubBatch(SubBatchRef& batch, size_t /*max_size*/) override {
batch = new FakeSubBatch();
return Status::OK();
}
Status freeSubBatch(SubBatchRef& batch) override {
delete batch;
batch = nullptr;
return Status::OK();
}
Status submitTransferTasks(
SubBatchRef batch, const std::vector<Request>& request_list) override {
++submit_calls;
auto* fb = static_cast<FakeSubBatch*>(batch);
for (size_t i = 0; i < request_list.size(); ++i) {
fb->statuses.push_back(
{TransferStatusEnum::COMPLETED, request_list[i].length});
fb->task_count++;
}
return Status::OK();
}
Status getTransferStatus(SubBatchRef batch, int task_id,
TransferStatus& status) override {
++status_calls;
auto* fb = static_cast<FakeSubBatch*>(batch);
if (task_id < 0 || task_id >= (int)fb->statuses.size()) {
return Status::InvalidArgument("bad task_id" LOC_MARK);
}
status = fb->statuses[task_id];
return Status::OK();
}
const char* getName() const override { return "<fake>"; }
};
// ---------------------------------------------------------------------------
// Helper: create a Request for testing (no real memory needed)
// ---------------------------------------------------------------------------
static Request makeRequest(size_t length = 4096) {
Request req;
req.opcode = Request::WRITE;
req.source = nullptr;
req.target_id = 1;
req.target_offset = 0;
req.length = length;
return req;
}
// ===========================================================================
// Group 1: Unit — FaultPolicy and FaultProxyTransport basics
// ===========================================================================
TEST(FaultPolicyTest, Defaults) {
FaultPolicy policy;
EXPECT_DOUBLE_EQ(policy.submit_fail_rate, 0.0);
EXPECT_DOUBLE_EQ(policy.status_corrupt_rate, 0.0);
EXPECT_EQ(policy.submit_delay_us, 0u);
EXPECT_EQ(policy.fail_after_n_submits, -1);
EXPECT_FALSE(policy.fail_install);
}
TEST(FaultProxyTest, DelegatesToReal) {
auto fake = std::make_shared<FakeTransport>();
FaultPolicy policy; // all defaults — no faults
auto proxy = std::make_shared<FaultProxyTransport>(fake, policy);
// install
std::string seg_name = "test";
ASSERT_TRUE(proxy->install(seg_name, nullptr, nullptr).ok());
EXPECT_EQ(fake->install_calls, 1);
// allocateSubBatch + submitTransferTasks
Transport::SubBatchRef batch = nullptr;
ASSERT_TRUE(proxy->allocateSubBatch(batch, 16).ok());
ASSERT_NE(batch, nullptr);
auto req = makeRequest();
ASSERT_TRUE(proxy->submitTransferTasks(batch, {req}).ok());
EXPECT_EQ(fake->submit_calls, 1);
EXPECT_EQ(proxy->submitCount(), 1);
// getTransferStatus
TransferStatus ts{};
ASSERT_TRUE(proxy->getTransferStatus(batch, 0, ts).ok());
EXPECT_EQ(ts.s, TransferStatusEnum::COMPLETED);
EXPECT_EQ(ts.transferred_bytes, 4096u);
EXPECT_EQ(fake->status_calls, 1);
proxy->freeSubBatch(batch);
}
TEST(FaultProxyTest, FailsInstall) {
auto fake = std::make_shared<FakeTransport>();
FaultPolicy policy;
policy.fail_install = true;
auto proxy = std::make_shared<FaultProxyTransport>(fake, policy);
std::string seg_name = "test";
auto status = proxy->install(seg_name, nullptr, nullptr);
EXPECT_FALSE(status.ok());
EXPECT_EQ(fake->install_calls, 0); // never reached the real transport
}
TEST(FaultProxyTest, FailsSubmitDeterministic) {
auto fake = std::make_shared<FakeTransport>();
FaultPolicy policy;
policy.fail_after_n_submits = 2; // succeed twice, then fail
auto proxy = std::make_shared<FaultProxyTransport>(fake, policy);
Transport::SubBatchRef batch = nullptr;
ASSERT_TRUE(proxy->allocateSubBatch(batch, 16).ok());
auto req = makeRequest();
// First two submits succeed
EXPECT_TRUE(proxy->submitTransferTasks(batch, {req}).ok());
EXPECT_TRUE(proxy->submitTransferTasks(batch, {req}).ok());
EXPECT_EQ(fake->submit_calls, 2);
// Third submit fails
auto s = proxy->submitTransferTasks(batch, {req});
EXPECT_FALSE(s.ok());
EXPECT_EQ(fake->submit_calls, 2); // real transport not called
// Fourth also fails
EXPECT_FALSE(proxy->submitTransferTasks(batch, {req}).ok());
proxy->freeSubBatch(batch);
}
TEST(FaultProxyTest, CorruptsStatus) {
auto fake = std::make_shared<FakeTransport>();
FaultPolicy policy;
policy.status_corrupt_rate = 1.0; // always corrupt
auto proxy = std::make_shared<FaultProxyTransport>(fake, policy);
Transport::SubBatchRef batch = nullptr;
ASSERT_TRUE(proxy->allocateSubBatch(batch, 16).ok());
auto req = makeRequest();
ASSERT_TRUE(proxy->submitTransferTasks(batch, {req}).ok());
TransferStatus ts{};
ASSERT_TRUE(proxy->getTransferStatus(batch, 0, ts).ok());
// Real transport returned COMPLETED, but proxy flipped it to FAILED
EXPECT_EQ(ts.s, TransferStatusEnum::FAILED);
proxy->freeSubBatch(batch);
}
// ===========================================================================
// Group 2: Integration — Failover state machine driven by proxy
// ===========================================================================
TEST(FaultProxyFailoverTest, FailoverFromProxyToReal) {
// Simulate: RDMA (proxied, always fails) → TCP (real, always succeeds)
auto fake_rdma = std::make_shared<FakeTransport>();
auto fake_tcp = std::make_shared<FakeTransport>();
FaultPolicy rdma_policy;
rdma_policy.submit_fail_rate = 1.0; // RDMA always fails
auto proxied_rdma =
std::make_shared<FaultProxyTransport>(fake_rdma, rdma_policy);
// Allocate sub-batches for both transports
Transport::SubBatchRef rdma_batch = nullptr;
Transport::SubBatchRef tcp_batch = nullptr;
ASSERT_TRUE(proxied_rdma->allocateSubBatch(rdma_batch, 16).ok());
ASSERT_TRUE(fake_tcp->allocateSubBatch(tcp_batch, 16).ok());
// Simulate TransferEngineImpl::submitTransfer + resubmitTransferTask
constexpr int kMaxAttempts = 3;
TaskInfo task;
task.type = RDMA;
task.xport_priority = 0;
task.status = TransferStatusEnum::PENDING;
task.failover_count = 0;
auto req = makeRequest();
// Step 1: Submit on "RDMA" — should fail (proxy injects fault)
auto s = proxied_rdma->submitTransferTasks(rdma_batch, {req});
EXPECT_FALSE(s.ok());
// Step 2: Failover logic (mirrors resubmitTransferTask)
task.status = TransferStatusEnum::FAILED;
++task.failover_count;
EXPECT_LE(task.failover_count, kMaxAttempts);
task.xport_priority++;
task.type = TCP;
task.status = TransferStatusEnum::PENDING;
// Step 3: Submit on "TCP" — should succeed
s = fake_tcp->submitTransferTasks(tcp_batch, {req});
EXPECT_TRUE(s.ok());
// Step 4: Verify completion
TransferStatus ts{};
ASSERT_TRUE(fake_tcp->getTransferStatus(tcp_batch, 0, ts).ok());
EXPECT_EQ(ts.s, TransferStatusEnum::COMPLETED);
task.status = TransferStatusEnum::COMPLETED;
// Assertions
EXPECT_EQ(task.type, TCP);
EXPECT_EQ(task.failover_count, 1);
EXPECT_EQ(task.status, TransferStatusEnum::COMPLETED);
EXPECT_EQ(fake_rdma->submit_calls, 0); // proxy intercepted, never hit real
EXPECT_EQ(fake_tcp->submit_calls, 1);
proxied_rdma->freeSubBatch(rdma_batch);
fake_tcp->freeSubBatch(tcp_batch);
}
TEST(FaultProxyFailoverTest, ExhaustAllTransports) {
// Both RDMA and TCP are proxied with 100% failure
auto fake_rdma = std::make_shared<FakeTransport>();
auto fake_tcp = std::make_shared<FakeTransport>();
FaultPolicy always_fail;
always_fail.submit_fail_rate = 1.0;
auto proxy_rdma =
std::make_shared<FaultProxyTransport>(fake_rdma, always_fail);
auto proxy_tcp =
std::make_shared<FaultProxyTransport>(fake_tcp, always_fail);
Transport::SubBatchRef rdma_batch = nullptr;
Transport::SubBatchRef tcp_batch = nullptr;
ASSERT_TRUE(proxy_rdma->allocateSubBatch(rdma_batch, 16).ok());
ASSERT_TRUE(proxy_tcp->allocateSubBatch(tcp_batch, 16).ok());
constexpr int kMaxAttempts = 3;
TaskInfo task;
task.type = RDMA;
task.xport_priority = 0;
task.status = TransferStatusEnum::PENDING;
task.failover_count = 0;
auto req = makeRequest();
// Attempt on RDMA — fails
EXPECT_FALSE(proxy_rdma->submitTransferTasks(rdma_batch, {req}).ok());
task.status = TransferStatusEnum::FAILED;
++task.failover_count;
task.xport_priority++;
task.type = TCP;
task.status = TransferStatusEnum::PENDING;
// Attempt on TCP — also fails
EXPECT_FALSE(proxy_tcp->submitTransferTasks(tcp_batch, {req}).ok());
task.status = TransferStatusEnum::FAILED;
++task.failover_count;
task.xport_priority++;
task.status = TransferStatusEnum::PENDING;
// Third attempt — fails again
EXPECT_FALSE(proxy_rdma->submitTransferTasks(rdma_batch, {req}).ok());
task.status = TransferStatusEnum::FAILED;
++task.failover_count;
EXPECT_LE(task.failover_count, kMaxAttempts); // Still within limit
task.xport_priority++;
task.status = TransferStatusEnum::PENDING;
// Fourth attempt — exceeds limit
EXPECT_FALSE(proxy_tcp->submitTransferTasks(tcp_batch, {req}).ok());
task.status = TransferStatusEnum::FAILED;
++task.failover_count;
EXPECT_GT(task.failover_count, kMaxAttempts);
// Task stays FAILED — no more failover
EXPECT_EQ(task.status, TransferStatusEnum::FAILED);
EXPECT_EQ(task.failover_count, kMaxAttempts + 1);
proxy_rdma->freeSubBatch(rdma_batch);
proxy_tcp->freeSubBatch(tcp_batch);
}
// ===========================================================================
// Group 3: Policy mutation
// ===========================================================================
TEST(FaultProxyTest, ResetPolicyMidRun) {
auto fake = std::make_shared<FakeTransport>();
FaultPolicy fail_policy;
fail_policy.submit_fail_rate = 1.0;
auto proxy = std::make_shared<FaultProxyTransport>(fake, fail_policy);
Transport::SubBatchRef batch = nullptr;
ASSERT_TRUE(proxy->allocateSubBatch(batch, 16).ok());
auto req = makeRequest();
// Should fail
EXPECT_FALSE(proxy->submitTransferTasks(batch, {req}).ok());
EXPECT_EQ(fake->submit_calls, 0);
// Reset to clean policy
FaultPolicy clean_policy;
proxy->resetPolicy(clean_policy);
// Should succeed now
EXPECT_TRUE(proxy->submitTransferTasks(batch, {req}).ok());
EXPECT_EQ(fake->submit_calls, 1);
EXPECT_EQ(proxy->submitCount(), 1); // counter was reset too
proxy->freeSubBatch(batch);
}
} // namespace
} // namespace tent
} // namespace mooncake

View File

@ -63,16 +63,6 @@ if (USE_EFA)
add_executable(efa_transport_test ${WORKSPACE}/efa_transport_test.cpp)
target_link_libraries(efa_transport_test PUBLIC transfer_engine gtest gtest_main)
add_test(NAME efa_transport_test COMMAND efa_transport_test)
add_executable(efa_c_api_test ${WORKSPACE}/efa_c_api_test.cpp)
target_link_libraries(efa_c_api_test PUBLIC transfer_engine gtest gtest_main)
add_test(NAME efa_c_api_test COMMAND efa_c_api_test)
add_executable(efa_single_nic_large_mr_test ${WORKSPACE}/efa_single_nic_large_mr_test.cpp)
target_link_libraries(efa_single_nic_large_mr_test PUBLIC transfer_engine gflags::gflags glog::glog)
add_executable(efa_transfer_test ${WORKSPACE}/efa_transfer_test.cpp)
target_link_libraries(efa_transfer_test PUBLIC transfer_engine gflags::gflags glog::glog)
endif()
# UB transport test with URMA endpoint and mock support

View File

@ -1,104 +0,0 @@
// Copyright 2024 KVCache.AI
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <gtest/gtest.h>
#include <numa.h>
#include <cstring>
#include "transfer_engine_c.h"
// Test: discoverTopology + installTransport("efa") via pure C API
TEST(EfaCApiTest, DiscoverTopologyAndInstallEfa) {
transfer_engine_t engine = createTransferEngine(
"P2PHANDSHAKE", "127.0.0.1:12345", "127.0.0.1", 12345, 0);
ASSERT_NE(engine, nullptr) << "createTransferEngine failed";
int ret = discoverTopology(engine);
ASSERT_EQ(ret, 0) << "discoverTopology failed with code " << ret;
transport_t xport = installTransport(engine, "efa", NULL);
ASSERT_NE(xport, nullptr) << "installTransport(\"efa\") failed";
destroyTransferEngine(engine);
}
// Test: registerLocalMemory works after C API EFA setup
TEST(EfaCApiTest, RegisterMemoryAfterCApiSetup) {
transfer_engine_t engine = createTransferEngine(
"P2PHANDSHAKE", "127.0.0.1:22345", "127.0.0.1", 22345, 0);
ASSERT_NE(engine, nullptr);
ASSERT_EQ(discoverTopology(engine), 0);
ASSERT_NE(installTransport(engine, "efa", NULL), nullptr);
size_t buf_size = 1 << 20; // 1 MB
void* buf = numa_alloc_onnode(buf_size, 0);
ASSERT_NE(buf, nullptr);
memset(buf, 0xAB, buf_size);
int ret = registerLocalMemory(engine, buf, buf_size, "cpu:0", 1);
EXPECT_EQ(ret, 0) << "registerLocalMemory failed with code " << ret;
unregisterLocalMemory(engine, buf);
numa_free(buf, buf_size);
destroyTransferEngine(engine);
}
// Test: registerLocalMemoryBatch works after C API EFA setup
TEST(EfaCApiTest, RegisterMemoryBatchAfterCApiSetup) {
transfer_engine_t engine = createTransferEngine(
"P2PHANDSHAKE", "127.0.0.1:32345", "127.0.0.1", 32345, 0);
ASSERT_NE(engine, nullptr);
ASSERT_EQ(discoverTopology(engine), 0);
ASSERT_NE(installTransport(engine, "efa", NULL), nullptr);
const int num_bufs = 4;
size_t buf_size = 1 << 20;
buffer_entry_t entries[4];
for (int i = 0; i < num_bufs; i++) {
entries[i].addr = numa_alloc_onnode(buf_size, 0);
ASSERT_NE(entries[i].addr, nullptr);
entries[i].length = buf_size;
memset(entries[i].addr, 0xCD, buf_size);
}
int ret = registerLocalMemoryBatch(engine, entries, num_bufs, "cpu:0");
EXPECT_EQ(ret, 0) << "registerLocalMemoryBatch failed with code " << ret;
void* addrs[4];
for (int i = 0; i < num_bufs; i++) addrs[i] = entries[i].addr;
unregisterLocalMemoryBatch(engine, addrs, num_bufs);
for (int i = 0; i < num_bufs; i++) numa_free(entries[i].addr, buf_size);
destroyTransferEngine(engine);
}
// Negative test: without discoverTopology, installTransport should fail
TEST(EfaCApiTest, InstallEfaWithoutDiscoverFails) {
transfer_engine_t engine = createTransferEngine(
"P2PHANDSHAKE", "127.0.0.1:42345", "127.0.0.1", 42345, 0);
ASSERT_NE(engine, nullptr);
transport_t xport = installTransport(engine, "efa", NULL);
EXPECT_EQ(xport, nullptr)
<< "installTransport should fail without discover";
destroyTransferEngine(engine);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -1,178 +0,0 @@
// Copyright 2024 KVCache.AI
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Reproduce: single-NIC + 200GB 4KB-page buffer registration.
// Expected: auto-split into chunks and register all on the one NIC.
// Current bug: "Buffer requires N chunks but only 1 NICs available."
//
// Usage:
// ./efa_single_nic_large_mr_test [--nic rdmap85s0] [--size_gb 200]
//
// The buffer is allocated via mmap (no MAP_HUGETLB) to get 4KB pages,
// matching the customer's /dev/shm mmap pattern.
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <sys/mman.h>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include "transfer_engine_c.h"
DEFINE_string(nic, "",
"EFA device name (e.g. rdmap85s0). "
"If empty, auto-detects the first rdmap* device.");
DEFINE_double(size_gb, 200.0, "Total memory in GB to register");
DEFINE_double(chunk_gb, 0, "Per-buffer chunk size in GB (0 = single buffer)");
DEFINE_string(server, "127.0.0.1:12345", "Local server name");
static std::string detectFirstEfaDevice() {
FILE* fp = popen(
"ls /sys/class/infiniband/ 2>/dev/null | grep rdmap | head -1", "r");
if (!fp) return "";
char buf[256] = {};
if (fgets(buf, sizeof(buf), fp)) {
size_t len = strlen(buf);
if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
}
pclose(fp);
return std::string(buf);
}
int main(int argc, char** argv) {
google::InitGoogleLogging(argv[0]);
gflags::ParseCommandLineFlags(&argc, &argv, true);
FLAGS_logtostderr = 1;
std::string nic = FLAGS_nic;
size_t size_bytes = static_cast<size_t>(FLAGS_size_gb * 1024 * 1024 * 1024);
LOG(INFO) << "Buffer size: " << FLAGS_size_gb << " GB (" << size_bytes
<< " bytes)";
// Create engine
transfer_engine_t engine = createTransferEngine(
"P2PHANDSHAKE", FLAGS_server.c_str(), "127.0.0.1", 12345, 0);
if (!engine) {
LOG(ERROR) << "createTransferEngine failed";
return 1;
}
// discoverTopology to populate device list
int ret = discoverTopology(engine);
if (ret != 0) {
LOG(ERROR) << "discoverTopology failed: " << ret;
destroyTransferEngine(engine);
return 1;
}
// Install EFA transport — optionally restrict to single NIC
transport_t xport = nullptr;
if (!nic.empty()) {
std::string matrix = "{\"cpu:0\": [[\"" + nic + "\"], []]}";
LOG(INFO) << "nic_priority_matrix: " << matrix;
char* matrix_cstr = const_cast<char*>(matrix.c_str());
void* args[] = {matrix_cstr};
xport = installTransport(engine, "efa", args);
} else {
LOG(INFO) << "Using all available NICs (no nic restriction)";
xport = installTransport(engine, "efa", nullptr);
}
if (!xport) {
LOG(ERROR) << "installTransport(efa) failed";
destroyTransferEngine(engine);
return 1;
}
// Determine buffer count and per-buffer size
size_t chunk_bytes = 0;
int num_bufs = 1;
if (FLAGS_chunk_gb > 0) {
chunk_bytes = static_cast<size_t>(FLAGS_chunk_gb * 1024 * 1024 * 1024);
num_bufs = (size_bytes + chunk_bytes - 1) / chunk_bytes;
} else {
chunk_bytes = size_bytes;
}
LOG(INFO) << "Plan: " << num_bufs << " buffers × "
<< chunk_bytes / (1024 * 1024)
<< " MB = " << (num_bufs * chunk_bytes) / (1024ULL * 1024 * 1024)
<< " GB";
// Allocate all buffers with hugepages
std::vector<void*> bufs;
bufs.reserve(num_bufs);
LOG(INFO) << "Allocating " << num_bufs << " buffers...";
for (int i = 0; i < num_bufs; ++i) {
void* buf = mmap(nullptr, chunk_bytes, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
if (buf == MAP_FAILED) {
LOG(ERROR) << "mmap failed at buffer " << i << "/" << num_bufs
<< ": " << strerror(errno);
for (auto* p : bufs) munmap(p, chunk_bytes);
destroyTransferEngine(engine);
return 1;
}
bufs.push_back(buf);
if ((i + 1) % 50 == 0 || i == num_bufs - 1) {
LOG(INFO) << " allocated " << (i + 1) << "/" << num_bufs;
}
}
LOG(INFO) << "All buffers allocated";
// Register each buffer
LOG(INFO) << "Registering " << num_bufs << " × "
<< chunk_bytes / (1024 * 1024) << " MB on single NIC...";
auto t0 = std::chrono::steady_clock::now();
int failures = 0;
for (int i = 0; i < num_bufs; ++i) {
ret = registerLocalMemory(engine, bufs[i], chunk_bytes, "cpu:0", 1);
if (ret != 0) {
LOG(ERROR) << "FAILED at buffer " << i << "/" << num_bufs
<< " (addr=" << bufs[i] << "): ret=" << ret;
failures++;
break;
}
if ((i + 1) % 50 == 0 || i == num_bufs - 1) {
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - t0).count();
LOG(INFO) << " registered " << (i + 1) << "/" << num_bufs << " ("
<< elapsed << "s)";
}
}
auto t1 = std::chrono::steady_clock::now();
double total_time = std::chrono::duration<double>(t1 - t0).count();
if (failures == 0) {
LOG(INFO) << "SUCCESS: registered " << num_bufs << " × "
<< chunk_bytes / (1024 * 1024) << " MB ("
<< (num_bufs * chunk_bytes) / (1024ULL * 1024 * 1024)
<< " GB) in " << total_time << "s";
} else {
LOG(ERROR) << "FAILED after " << total_time << "s";
}
// Cleanup
for (auto* p : bufs) {
unregisterLocalMemory(engine, p);
munmap(p, chunk_bytes);
}
destroyTransferEngine(engine);
return failures == 0 ? 0 : 1;
}

View File

@ -1,446 +0,0 @@
// Copyright 2024 KVCache.AI
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// EFA multi-NIC transfer test: register 200x2GB on all NICs, then transfer.
//
// Target node: allocate 200x2GB buffers, register on all NICs, wait.
// Initiator node: allocate receive buffer, register, pull from target.
//
// Usage:
// # Target (holds KV cache):
// ./efa_transfer_test --mode target --server <target_ip>:12345 \
// --num_bufs 200 --buf_size_gb 2
//
// # Initiator (pulls data):
// ./efa_transfer_test --mode initiator --server <initiator_ip>:12346 \
// --target <target_ip>:12345 --num_bufs 200 --buf_size_gb 2 \
// --transfer_mb 368
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <signal.h>
#include <sys/mman.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <numeric>
#include <string>
#include <thread>
#include <vector>
#include "transfer_engine.h"
using namespace mooncake;
DEFINE_string(mode, "target", "Running mode: target or initiator");
DEFINE_string(server, "", "Local server name, e.g. 172.31.6.162:12345");
DEFINE_string(target, "", "Target server name (initiator mode only)");
DEFINE_string(metadata, "P2PHANDSHAKE", "Metadata server");
DEFINE_int32(num_bufs, 200, "Number of buffers to allocate and register");
DEFINE_double(buf_size_gb, 2.0, "Size of each buffer in GB");
DEFINE_double(transfer_mb, 368.0,
"Transfer size per iteration in MB (initiator)");
DEFINE_int32(iterations, 50, "Number of benchmark iterations");
DEFINE_int32(warmup, 5, "Number of warmup iterations");
DEFINE_int32(batch_size, 1, "Batch size for each transfer submission");
DEFINE_int32(threads, 1, "Number of initiator worker threads");
DEFINE_uint64(block_size, 65536, "Block size for transfer requests (64KB)");
static std::atomic<bool> g_running(true);
static void signalHandler(int) {
g_running.store(false, std::memory_order_relaxed);
}
static void setupSignalHandler() {
struct sigaction sa;
sa.sa_handler = signalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
}
static void* allocateHugepage(size_t size) {
void* buf = mmap(nullptr, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
if (buf == MAP_FAILED) {
LOG(WARNING) << "Hugepage mmap failed (" << strerror(errno)
<< "), falling back to regular pages";
buf = mmap(nullptr, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (buf == MAP_FAILED) return nullptr;
}
return buf;
}
static int runTarget(TransferEngine* engine) {
size_t buf_bytes =
static_cast<size_t>(FLAGS_buf_size_gb * 1024 * 1024 * 1024);
int num_bufs = FLAGS_num_bufs;
LOG(INFO) << "=== Target Node ===";
LOG(INFO) << "Registering " << num_bufs << " x " << FLAGS_buf_size_gb
<< " GB = " << num_bufs * FLAGS_buf_size_gb << " GB";
// Allocate buffers
std::vector<void*> bufs;
bufs.reserve(num_bufs);
LOG(INFO) << "Allocating " << num_bufs << " buffers...";
for (int i = 0; i < num_bufs; ++i) {
void* buf = allocateHugepage(buf_bytes);
if (!buf) {
LOG(ERROR) << "Allocation failed at buffer " << i;
for (auto* p : bufs) munmap(p, buf_bytes);
return 1;
}
bufs.push_back(buf);
if ((i + 1) % 50 == 0 || i == num_bufs - 1)
LOG(INFO) << " allocated " << (i + 1) << "/" << num_bufs;
}
// Register all buffers
LOG(INFO) << "Registering " << num_bufs << " buffers on all NICs...";
auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < num_bufs; ++i) {
int ret = engine->registerLocalMemory(bufs[i], buf_bytes, "*", true);
if (ret != 0) {
LOG(ERROR) << "registerLocalMemory failed at buffer " << i
<< ": ret=" << ret;
for (int j = 0; j < i; ++j) engine->unregisterLocalMemory(bufs[j]);
for (auto* p : bufs) munmap(p, buf_bytes);
return 1;
}
if ((i + 1) % 50 == 0 || i == num_bufs - 1) {
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - t0).count();
LOG(INFO) << " registered " << (i + 1) << "/" << num_bufs << " ("
<< elapsed << "s)";
}
}
auto t1 = std::chrono::steady_clock::now();
LOG(INFO) << "Registration complete: "
<< std::chrono::duration<double>(t1 - t0).count() << "s";
LOG(INFO) << "Target ready. First buffer at " << bufs[0]
<< ". Waiting for initiator (Ctrl+C to stop)...";
while (g_running) sleep(1);
LOG(INFO) << "Shutting down target...";
for (auto* p : bufs) {
engine->unregisterLocalMemory(p);
munmap(p, buf_bytes);
}
return 0;
}
struct LatencyStats {
double avg_ms;
double p50_ms;
double p99_ms;
double throughput_gbs;
};
static LatencyStats computeStats(std::vector<double>& latencies_ms,
size_t transfer_bytes) {
std::sort(latencies_ms.begin(), latencies_ms.end());
double sum = std::accumulate(latencies_ms.begin(), latencies_ms.end(), 0.0);
size_t n = latencies_ms.size();
LatencyStats stats;
stats.avg_ms = sum / n;
stats.p50_ms = latencies_ms[n / 2];
stats.p99_ms = latencies_ms[std::min(n - 1, (size_t)(n * 0.99))];
stats.throughput_gbs = (transfer_bytes / 1e9) / (stats.p50_ms / 1000.0);
return stats;
}
static int runInitiator(TransferEngine* engine) {
size_t transfer_bytes =
static_cast<size_t>(FLAGS_transfer_mb * 1024 * 1024);
LOG(INFO) << "=== Initiator Node ===";
LOG(INFO) << "Target: " << FLAGS_target;
LOG(INFO) << "Transfer size: " << FLAGS_transfer_mb << " MB";
LOG(INFO) << "Threads: " << FLAGS_threads;
if (FLAGS_target.empty()) {
LOG(ERROR) << "--target required in initiator mode";
return 1;
}
// Allocate local receive buffer (one per thread)
size_t recv_bytes = transfer_bytes;
std::vector<void*> recv_bufs(FLAGS_threads);
for (int t = 0; t < FLAGS_threads; ++t) {
recv_bufs[t] = allocateHugepage(recv_bytes);
if (!recv_bufs[t]) {
LOG(ERROR) << "Failed to allocate receive buffer for thread " << t;
return 1;
}
int ret = engine->registerLocalMemory(recv_bufs[t], recv_bytes, "cpu:0",
true);
if (ret != 0) {
LOG(ERROR) << "Failed to register receive buffer: " << ret;
return 1;
}
}
LOG(INFO) << "Allocated and registered " << FLAGS_threads
<< " receive buffers of " << recv_bytes / 1e6 << " MB each";
// Open remote segment
auto segment_id = engine->openSegment(FLAGS_target);
if (segment_id < 0) {
LOG(ERROR) << "openSegment failed for " << FLAGS_target;
return 1;
}
// Get remote buffer info
auto segment_desc = engine->getMetadata()->getSegmentDescByID(segment_id);
if (!segment_desc || segment_desc->buffers.empty()) {
LOG(ERROR) << "No remote buffers found";
return 1;
}
size_t num_remote_bufs = segment_desc->buffers.size();
LOG(INFO) << "Remote has " << num_remote_bufs << " buffers";
LOG(INFO) << "First buffer: addr=0x" << std::hex
<< segment_desc->buffers[0].addr << std::dec
<< " size=" << segment_desc->buffers[0].length;
// Connection warmup: small transfer to establish endpoints
LOG(INFO) << "Warming up connection...";
{
size_t warmup_size = std::min(transfer_bytes, (size_t)(64 * 1024));
auto batch_id = engine->allocateBatchID(1);
TransferRequest req;
req.opcode = TransferRequest::READ;
req.source = (uint8_t*)recv_bufs[0];
req.target_id = segment_id;
req.target_offset = segment_desc->buffers[0].addr;
req.length = warmup_size;
auto s = engine->submitTransfer(batch_id, {req});
if (!s.ok()) {
LOG(ERROR) << "Warmup transfer failed: " << s.ToString();
return 1;
}
while (true) {
TransferStatus status;
engine->getTransferStatus(batch_id, 0, status);
if (status.s == TransferStatusEnum::COMPLETED) break;
if (status.s == TransferStatusEnum::FAILED) {
LOG(ERROR) << "Warmup transfer FAILED";
return 1;
}
}
engine->freeBatchID(batch_id);
}
LOG(INFO) << "Connection ready.";
// Refresh segment desc after connection warmup (endpoints are now up)
engine->syncSegmentCache(FLAGS_target);
segment_desc = engine->getMetadata()->getSegmentDescByID(segment_id);
// Worker function: each thread runs its own transfer loop
struct ThreadResult {
std::vector<double> latencies;
int errors = 0;
};
auto workerFn = [&](int tid, int warmup_iters, int bench_iters,
ThreadResult* result) {
void* my_recv = recv_bufs[tid];
for (int w = 0; w < warmup_iters; ++w) {
size_t buf_idx = (tid + w * FLAGS_threads) % num_remote_bufs;
uint64_t raddr = segment_desc->buffers[buf_idx].addr;
size_t rlen = segment_desc->buffers[buf_idx].length;
size_t xfer = std::min(transfer_bytes, rlen);
auto bid = engine->allocateBatchID(1);
TransferRequest req;
req.opcode = TransferRequest::READ;
req.source = (uint8_t*)my_recv;
req.target_id = segment_id;
req.target_offset = raddr;
req.length = xfer;
engine->submitTransfer(bid, {req});
while (true) {
TransferStatus st;
engine->getTransferStatus(bid, 0, st);
if (st.s == TransferStatusEnum::COMPLETED ||
st.s == TransferStatusEnum::FAILED)
break;
}
engine->freeBatchID(bid);
}
for (int i = 0; i < bench_iters; ++i) {
size_t buf_idx = (tid + i * FLAGS_threads) % num_remote_bufs;
uint64_t raddr = segment_desc->buffers[buf_idx].addr;
size_t rlen = segment_desc->buffers[buf_idx].length;
size_t xfer = std::min(transfer_bytes, rlen);
auto t0 = std::chrono::steady_clock::now();
auto bid = engine->allocateBatchID(1);
TransferRequest req;
req.opcode = TransferRequest::READ;
req.source = (uint8_t*)my_recv;
req.target_id = segment_id;
req.target_offset = raddr;
req.length = xfer;
auto s = engine->submitTransfer(bid, {req});
if (!s.ok()) {
result->errors++;
engine->freeBatchID(bid);
continue;
}
bool ok = false;
while (true) {
TransferStatus st;
engine->getTransferStatus(bid, 0, st);
if (st.s == TransferStatusEnum::COMPLETED) {
ok = true;
break;
}
if (st.s == TransferStatusEnum::FAILED) {
result->errors++;
break;
}
}
engine->freeBatchID(bid);
if (ok) {
auto t1 = std::chrono::steady_clock::now();
result->latencies.push_back(
std::chrono::duration<double, std::milli>(t1 - t0).count());
}
}
};
// Run warmup + benchmark with threads
int num_threads = FLAGS_threads;
LOG(INFO) << "Running with " << num_threads << " threads, " << FLAGS_warmup
<< " warmup + " << FLAGS_iterations
<< " bench iterations per thread...";
std::vector<ThreadResult> results(num_threads);
std::vector<std::thread> threads;
auto wall_t0 = std::chrono::steady_clock::now();
for (int t = 0; t < num_threads; ++t) {
threads.emplace_back(workerFn, t, FLAGS_warmup * FLAGS_iterations,
FLAGS_iterations, &results[t]);
}
for (auto& th : threads) th.join();
auto wall_t1 = std::chrono::steady_clock::now();
double wall_ms =
std::chrono::duration<double, std::milli>(wall_t1 - wall_t0).count();
// Aggregate results
std::vector<double> all_latencies;
int total_errors = 0;
for (auto& r : results) {
all_latencies.insert(all_latencies.end(), r.latencies.begin(),
r.latencies.end());
total_errors += r.errors;
}
if (all_latencies.empty()) {
LOG(ERROR) << "All transfers failed";
return 1;
}
auto stats = computeStats(all_latencies, transfer_bytes);
size_t total_xfers = all_latencies.size();
double total_bytes = (double)total_xfers * transfer_bytes;
double agg_throughput = total_bytes / 1e9 / (wall_ms / 1000.0);
LOG(INFO) << "=== Results (" << num_threads << " threads) ===";
LOG(INFO) << "Transfer: " << FLAGS_transfer_mb << " MB x " << total_xfers
<< " transfers";
LOG(INFO) << "Wall time: " << wall_ms << " ms";
LOG(INFO) << "Per-transfer p50: " << stats.p50_ms << " ms"
<< " p99: " << stats.p99_ms << " ms";
LOG(INFO) << "Per-transfer throughput: " << stats.throughput_gbs << " GB/s";
LOG(INFO) << "Aggregate throughput: " << agg_throughput << " GB/s";
LOG(INFO) << "Errors: " << total_errors;
// Per-thread stats
for (int t = 0; t < num_threads; ++t) {
if (results[t].latencies.empty()) continue;
auto ts = computeStats(results[t].latencies, transfer_bytes);
LOG(INFO) << " Thread " << t << ": p50=" << ts.p50_ms
<< "ms tput=" << ts.throughput_gbs << " GB/s"
<< " iters=" << results[t].latencies.size()
<< " errors=" << results[t].errors;
}
// Cleanup
for (int t = 0; t < FLAGS_threads; ++t) {
engine->unregisterLocalMemory(recv_bufs[t]);
munmap(recv_bufs[t], recv_bytes);
}
return total_errors > 0 ? 1 : 0;
}
int main(int argc, char** argv) {
google::InitGoogleLogging(argv[0]);
gflags::ParseCommandLineFlags(&argc, &argv, true);
FLAGS_logtostderr = 1;
setupSignalHandler();
if (FLAGS_server.empty()) {
LOG(ERROR) << "--server is required";
return 1;
}
// Parse host:port
auto colon = FLAGS_server.rfind(':');
std::string host = FLAGS_server.substr(0, colon);
uint64_t port = 12345;
if (colon != std::string::npos)
port = std::stoull(FLAGS_server.substr(colon + 1));
auto engine = std::make_unique<TransferEngine>(false);
int ret = engine->init(FLAGS_metadata, FLAGS_server, host, port);
if (ret != 0) {
LOG(ERROR) << "Engine init failed: " << ret;
return 1;
}
// Discover topology and install EFA transport (all NICs)
engine->getLocalTopology()->discover({});
auto* xport = engine->installTransport("efa", nullptr);
if (!xport) {
LOG(ERROR) << "installTransport(efa) failed";
return 1;
}
std::string actual_server = engine->getLocalIpAndPort();
LOG(INFO) << "Actual server name (use this for --target): "
<< actual_server;
if (FLAGS_mode == "target") {
ret = runTarget(engine.get());
} else if (FLAGS_mode == "initiator") {
ret = runInitiator(engine.get());
} else {
LOG(ERROR) << "Unknown mode: " << FLAGS_mode;
ret = 1;
}
return ret;
}

View File

@ -28,19 +28,6 @@ from vllm.attention.selector import get_attn_backend
from vllm.config import VllmConfig
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
KVConnectorBase_V1, KVConnectorMetadata, KVConnectorRole)
# SupportsHMA was introduced in vllm-project/vllm PR #25712 and is enforced
# for KV connectors by PR #27592. It is the marker the Hybrid Memory
# Allocator uses to allow PD-disaggregation with hybrid (e.g. attention +
# Mamba2) models. We import it conditionally so this connector keeps
# working on older vLLM releases that pre-date the interface; on those
# releases the marker is a no-op object base and the
# `request_finished_all_groups` shim below is dead code.
try:
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
SupportsHMA)
except ImportError: # pragma: no cover - older vLLM
SupportsHMA = object # type: ignore[assignment, misc]
from vllm.distributed.parallel_state import (get_tensor_model_parallel_rank,
get_tp_group)
from vllm.forward_context import ForwardContext
@ -117,12 +104,7 @@ class MooncakeConnectorMetadata(KVConnectorMetadata):
self.reqs_to_send[request_id] = local_block_ids
class MooncakeConnector(KVConnectorBase_V1, SupportsHMA):
# Subclassing SupportsHMA gates this connector through vLLM's Hybrid
# Memory Allocator path, which is required to PD-disaggregate hybrid
# attention + Mamba2 models (e.g. nvidia/NVIDIA-Nemotron-Nano-9B-v2).
# On vLLM versions that pre-date PR #25712 the marker resolves to
# `object` and this is identical to the previous single-base class.
class MooncakeConnector(KVConnectorBase_V1):
def __init__(self, vllm_config: VllmConfig, role: KVConnectorRole):
assert vllm_config.kv_transfer_config is not None
@ -172,28 +154,6 @@ class MooncakeConnector(KVConnectorBase_V1, SupportsHMA):
assert self.connector_scheduler is not None
return self.connector_scheduler.request_finished(request, block_ids)
def request_finished_all_groups(
self,
request: "Request",
block_ids: tuple[list[int], ...],
) -> tuple[bool, Optional[dict[str, Any]]]:
"""SupportsHMA hook for hybrid (multi-group) KV cache layouts.
Hybrid models (e.g. attention + Mamba2) expose one block-id list
per KV cache group instead of a single flat list. The Mooncake
transport itself does not yet distinguish groups on the wire, so
we flatten the per-group lists and delegate to the existing
single-group `request_finished`. This is the minimum-viable shim
that satisfies the `SupportsHMA` contract and unblocks the
Hybrid Memory Allocator gate so the engine can start up; cross-
node fidelity for non-attention SSM/Mamba state is not asserted
by this method and remains a follow-up (the Mamba2 backend in
vLLM still raises NotImplementedError from
`get_kv_cache_shape()`).
"""
flat: list[int] = [b for group in block_ids for b in group]
return self.request_finished(request, flat)
############################################################
# Worker Side Methods
############################################################

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "mooncake-transfer-engine"
version = "0.3.10.post2"
version = "0.3.10.post1"
description = "Python binding of a Mooncake library using pybind11"
authors = [
{ name = "Mooncake Authors" }

Some files were not shown because too many files have changed in this diff Show More