You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Mooncake Store's read/write paths have three systemic issues:
GPU pointer unsafety: 8 std::memcpy call sites may receive GPU device pointers, causing crashes or silent data corruption.
Redundant staging copies: The write path always stages user data into an intermediate buffer — 2 copies instead of 1.
Slow GPU→CPU bandwidth: cudaMemcpy to pageable (non-CUDA-pinned) memory is throttled to ~3-6 GB/s by internal driver staging. Pinned memory achieves ~12+ GB/s.
Root Cause
All six *_internal write functions unconditionally stage in RealClient:
user buffer → ① std::memcpy → staging buffer → ② CopyAuto → target segment
Copy ① is GPU unsafe (std::memcpy cannot operate on device memory)
Copy ① is redundant — staging can be skipped if we push the decision downstream
Both staging buffer and segment are pageable (not CUDA-pinned), so even cudaMemcpy via CopyAuto only achieves ~3-6 GB/s
Current vs Target Copy Counts
Scenario
Current
Target
GPU → local segment
2 copies, ~3-6 GB/s, GPU unsafe
1 copy, ~12+ GB/s
CPU → local segment
2 copies
1 copy
Registered buf → remote (RDMA)
1+RDMA, GPU unsafe
0+RDMA
Unregistered buf → remote (RDMA)
1+RDMA, GPU unsafe
1+RDMA, GPU safe
get_buffer → buffer_to_tensor
2 copies
1 copy
Core Design
Two orthogonal changes that compose into the solution:
cudaHostRegister segment and staging memory — enables full-bandwidth GPU DMA
Push staging decision to TransferSubmitter — eliminates the redundant copy
Current Architecture
flowchart TD
A["RealClient *_internal\nallocate staging\nstd::memcpy(staging, user_data) ⚠️ GPU UNSAFE"] --> B["client_->Put(key, slices)"]
B --> C{"TransferSubmitter\nselectStrategy"}
C -->|local| D["LOCAL_MEMCPY\nCopyAuto → segment (pageable)\n⚠️ ~3-6 GB/s"]
C -->|remote| E["TRANSFER_ENGINE\nRDMA from staging"]
style A fill:#f66,color:#fff
style D fill:#f96,color:#fff
Loading
Proposed Architecture
flowchart TD
A["RealClient *_internal\nbuild slices from user pointer\nNO staging, NO memcpy"] --> B["client_->Put(key, slices)"]
B --> C{"TransferSubmitter\nselectStrategy"}
C -->|local| D["LOCAL_MEMCPY\nCopyAuto → segment (CUDA-pinned)\n✅ ~12+ GB/s, 1 copy"]
C -->|remote| F{"isRegistered?"}
F -->|yes| G["RDMA direct\n0 local copy"]
F -->|no| H["ensureRegisteredForRDMA\nstage + RDMA"]
style D fill:#4a4,color:#fff
style G fill:#4a4,color:#fff
Loading
Key Design Details
cudaHostRegister segment and staging memory
At mount time (MountSegmentAndGetId), after RDMA registration (ibv_reg_mr), call cudaHostRegister on the segment memory to make it CUDA-pinned. Same for the staging buffer at setup_internal.
#ifdef USE_CUDA
auto ret = cudaHostRegister(buffer, size, cudaHostRegisterDefault);
if (ret != cudaSuccess) {
LOG(WARNING) << "cudaHostRegister failed: " << cudaGetErrorString(ret);
// Non-fatal: falls back to pageable-speed cudaMemcpy
}
#endif
Why this works:
Segment memory is mmap-allocated (hugepage, 2MB-aligned), long-lived, never reallocated — ideal for cudaHostRegister
ibv_reg_mr (RDMA pin) and cudaHostRegister (CUDA pin) coexist — different subsystems pinning the same pages
Precedent: Mooncake's tent NVLink transport already uses cudaHostRegister on CPU memory in production (nvlink_transport.cpp:276)
cudaPointerGetAttributes in IsDevicePointer (×2 per call)
~1-5 us/call
Negligible
Bandwidth (with cudaHostRegister'd destination):
GPU→pinned host: ~12-25 GB/s (PCIe Gen4/5 DMA, full line rate)
CPU→CPU fallback: std::memcpy at ~20+ GB/s
This is sufficient for Mooncake Store's primary workload (MB+ tensor writes). The overhead is amortized over large transfers.
Future optimization: Replace cudaMemcpy with cudaMemcpyAsync on a persistent per-worker CUDA stream. Eliminates the per-call stream creation overhead and allows CPU-side pipelining. Not required for Phase 1 — the bandwidth bottleneck is PCIe, not API overhead.
ensureRegisteredForRDMA: on-demand staging for TRANSFER_ENGINE
Only allocates staging when the RDMA path is selected AND a slice is not RDMA-registered:
auto [prepared_slices, staging_handles] = ensureRegisteredForRDMA(slices);
// registered → original ptr; unregistered → staging copy
Staging removal from *_internal
Six write functions change from allocate → memcpy → split_into_slices(staging) to split_into_slices(user_ptr). Follows the existing zero-copy pattern of put_from_internal.
Lifetime safety: All Put/Upsert calls are synchronous — user buffer on caller's stack stays alive throughout the transfer.
Read Path
get_into / get_into_ranges: already optimal for memory replicas (1 copy). No change.
get_buffer → buffer_to_tensor: reads into staging, then new char[] + memcpy. Fix: transfer BufferHandle ownership to the tensor capsule (1 copy instead of 2).
execute_tensor_into_plan_transfers: std::memcpy(metadata → gpu_buffer) is GPU unsafe. Fix: replace with MemcpySafe.
cudaHostRegister fails (no GPU, insufficient pinnable memory)
GPU→segment copies remain at pageable speed (~3-6 GB/s)
Non-fatal: log WARNING, system works correctly at reduced GPU bandwidth. CPU→CPU path unaffected.
cudaHostRegister on hugepage memory
Potential compatibility issues on some kernels
Validated: tent NVLink transport uses same pattern in production. cudaHostRegister supports hugepages since CUDA 11.
Large segment registration overhead
One-time latency at startup (ms-level for GB-scale memory)
Amortized over process lifetime. Only done once at mount time.
ibv_reg_mr + cudaHostRegister coexistence
Theoretical double-pinning conflict
Both pin OS pages — independent subsystems (NIC DMA vs GPU DMA). No known conflict. RDMA registration uses IBV_ACCESS_LOCAL_WRITE; CUDA registration uses page table entries.
Removing staging breaks *_internal user buffer lifetime
Data corruption if user buffer freed during transfer
All Put/Upsert/BatchPut are synchronous — user buffer guaranteed alive. std::span<const char> references caller stack data.
System pinned memory limit (vm.max_map_count, RLIMIT_MEMLOCK)
Registration silently fails or OOM-kills
Check ulimit -l at startup; document minimum requirements. Fallback is transparent (pageable speed).
Error Handling
flowchart LR
A["GPU/RDMA failure"] --> B["CopyAuto or MemcpySafe returns false"]
B --> C["TransferFuture → TRANSFER_FAIL"]
C --> D["Client::Put → PutRevoke\nrolls back master metadata"]
D --> E["error returned to caller"]
Background and Motivation
Mooncake Store's read/write paths have three systemic issues:
std::memcpycall sites may receive GPU device pointers, causing crashes or silent data corruption.cudaMemcpyto pageable (non-CUDA-pinned) memory is throttled to ~3-6 GB/s by internal driver staging. Pinned memory achieves ~12+ GB/s.Root Cause
All six
*_internalwrite functions unconditionally stage in RealClient:std::memcpycannot operate on device memory)cudaMemcpyviaCopyAutoonly achieves ~3-6 GB/sCurrent vs Target Copy Counts
get_buffer→buffer_to_tensorCore Design
Two orthogonal changes that compose into the solution:
cudaHostRegistersegment and staging memory — enables full-bandwidth GPU DMACurrent Architecture
flowchart TD A["RealClient *_internal\nallocate staging\nstd::memcpy(staging, user_data) ⚠️ GPU UNSAFE"] --> B["client_->Put(key, slices)"] B --> C{"TransferSubmitter\nselectStrategy"} C -->|local| D["LOCAL_MEMCPY\nCopyAuto → segment (pageable)\n⚠️ ~3-6 GB/s"] C -->|remote| E["TRANSFER_ENGINE\nRDMA from staging"] style A fill:#f66,color:#fff style D fill:#f96,color:#fffProposed Architecture
flowchart TD A["RealClient *_internal\nbuild slices from user pointer\nNO staging, NO memcpy"] --> B["client_->Put(key, slices)"] B --> C{"TransferSubmitter\nselectStrategy"} C -->|local| D["LOCAL_MEMCPY\nCopyAuto → segment (CUDA-pinned)\n✅ ~12+ GB/s, 1 copy"] C -->|remote| F{"isRegistered?"} F -->|yes| G["RDMA direct\n0 local copy"] F -->|no| H["ensureRegisteredForRDMA\nstage + RDMA"] style D fill:#4a4,color:#fff style G fill:#4a4,color:#fffKey Design Details
cudaHostRegistersegment and staging memoryAt mount time (
MountSegmentAndGetId), after RDMA registration (ibv_reg_mr), callcudaHostRegisteron the segment memory to make it CUDA-pinned. Same for the staging buffer atsetup_internal.Why this works:
mmap-allocated (hugepage, 2MB-aligned), long-lived, never reallocated — ideal forcudaHostRegisteribv_reg_mr(RDMA pin) andcudaHostRegister(CUDA pin) coexist — different subsystems pinning the same pagescudaHostRegisteron CPU memory in production (nvlink_transport.cpp:276)MemcpySafe: GPU-safe generic memcpyCopy efficiency:
CopyAutoandMemcpySafeCopyAutowrapscudaMemcpy(dst, src, size, cudaMemcpyDefault)— a synchronous, blocking copy. Two overhead sources:cudaMemcpysync overhead (implicit stream create + wait)cudaPointerGetAttributesinIsDevicePointer(×2 per call)Bandwidth (with
cudaHostRegister'd destination):std::memcpyat ~20+ GB/sThis is sufficient for Mooncake Store's primary workload (MB+ tensor writes). The overhead is amortized over large transfers.
Future optimization: Replace
cudaMemcpywithcudaMemcpyAsyncon a persistent per-worker CUDA stream. Eliminates the per-call stream creation overhead and allows CPU-side pipelining. Not required for Phase 1 — the bandwidth bottleneck is PCIe, not API overhead.ensureRegisteredForRDMA: on-demand staging for TRANSFER_ENGINEOnly allocates staging when the RDMA path is selected AND a slice is not RDMA-registered:
Staging removal from
*_internalSix write functions change from
allocate → memcpy → split_into_slices(staging)tosplit_into_slices(user_ptr). Follows the existing zero-copy pattern ofput_from_internal.Lifetime safety: All
Put/Upsertcalls are synchronous — user buffer on caller's stack stays alive throughout the transfer.Read Path
get_into/get_into_ranges: already optimal for memory replicas (1 copy). No change.get_buffer→buffer_to_tensor: reads into staging, thennew char[] + memcpy. Fix: transferBufferHandleownership to the tensor capsule (1 copy instead of 2).execute_tensor_into_plan_transfers:std::memcpy(metadata → gpu_buffer)is GPU unsafe. Fix: replace withMemcpySafe.GPU-Unsafe Call Sites
put_internalstaging memcpyreal_client.cpp:1551put_batch_internalstaging memcpyreal_client.cpp:1629put_parts_internalloop memcpyreal_client.cpp:1733upsert_internalstaging memcpyreal_client.cpp:3504upsert_parts_internalloop memcpyreal_client.cpp:3750upsert_batch_internalstaging memcpyreal_client.cpp:3834batch_write_tensor_impldata memcpystore_py_parallel_write.h:55-57execute_tensor_into_plan_transfersmetadata memcpystore_py_parallel_read.h:1074Risks and Mitigations
cudaHostRegisterfails (no GPU, insufficient pinnable memory)cudaHostRegisteron hugepage memorycudaHostRegistersupports hugepages since CUDA 11.ibv_reg_mr+cudaHostRegistercoexistenceIBV_ACCESS_LOCAL_WRITE; CUDA registration uses page table entries.*_internaluser buffer lifetimestd::span<const char>references caller stack data.vm.max_map_count, RLIMIT_MEMLOCK)ulimit -lat startup; document minimum requirements. Fallback is transparent (pageable speed).Error Handling
flowchart LR A["GPU/RDMA failure"] --> B["CopyAuto or MemcpySafe returns false"] B --> C["TransferFuture → TRANSFER_FAIL"] C --> D["Client::Put → PutRevoke\nrolls back master metadata"] D --> E["error returned to caller"]Scope and Phasing
cudaHostRegistersegment/staginggpu_staging_utils.h,real_client.cpp,client_service.cpp,store_py_parallel_*.h*_internal+ensureRegisteredForRDMAreal_client.cpp,transfer_task.h/.cpp,transfer_engine*.h/.cpp,client_service.cppbuffer_to_tensorownership transferstore_py.cppPhase 1 is low-risk (additive changes, non-fatal fallback). Deploy and validate first.
Phase 2 restructures the write path. Depends on Phase 1 for GPU safety. Higher blast radius but all changes are API-compatible.
Phase 3 is isolated to Python binding layer.
Compatibility
cudaHostRegisterfailure is non-fatal. System operates correctly without GPU, just at pageable speed for GPU transfers.register_buffer(). Unregistered buffers handled transparently viaensureRegisteredForRDMA.