Repository navigation
Conversation
The CUDA staging copies in the TCP transport ran a synchronous cudaMemcpy on the legacy default stream, so any device-memory transfer waited behind every kernel the process already had queued, with the calling thread sitting inside cudaMemcpy for the whole wait. The MACA build already went through a private non-blocking stream via copyTcpCudaMemory; the CUDA build did not. Give copyTcpCudaMemory to every GPU build and route all four staging sites (server/client x read/write body) through it. The trailing stream-sync keeps the copy's completion contract identical for callers. Fixes kvcache-ai#4323
|
Heads-up for reviewers: the reporter ran a wider repro matrix on this change over in #4323 and found one regression case (8 B reads while the sender is busy on a non-blocking stream: ~0.35 -> ~1.75 ms, pageable async D2H waiting behind an in-flight kernel). Analysis and the plan are in the issue thread: the trade still favors this fix (bounded ~1.4 ms vs an unbounded stall), and the residual is exactly what #4233's pinned staging closes, so whichever of the two lands second owns the re-measure. |
… waits on it Test contributed by @Gaurav-Shah05 on kvcache-ai#4323: cudaLaunchHostFunc keeps the legacy stream blocked for as long as the copy takes, no kernel and no nvcc needed. On main both transfers time out; with the non-blocking staging copies they complete (5/5 on his CUDA build). Compiles in GPU CI; skipped without a device, so CPU-only runs are unaffected. Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
|
Merged current main (b6d5ba8) so the CUDA legs rerun on the fixed pipeline (#4513 restored the build, #4440 pinned the wheel-test torch). Merge was clean. Rebuilt in the local docker toolchain: the staged-copy change compiles into the TCP transport, and tcp_transport_test passes against a live metadata server. The CUDA-gated staging test itself runs in the CI CUDA legs, which were previously red for the repo-wide Environ break, not this change. |
|
Open since 09-25 with a green matrix and no review yet, so a look from a transfer-engine owner when time allows: @alogfans @doujiang24 @chestnut-Q @staryxchen. The TCP transport staged CUDA buffers through a synchronous cudaMemcpy on the legacy default stream, so GPU transfers queued behind every kernel the process already had in flight; this routes all four staging sites through copyTcpCudaMemory (private stream, cudaMemcpyAsync) like the MACA build already did, with the same completion contract for callers. |
alogfans
left a comment
There was a problem hiding this comment.
Sorry for delayed review :( However, The MLU backend does not provide the APIs required by the helper introduced in this PR.
copyTcpCudaMemory() was expanded from USE_MACA to all CUDA-like backends, but it calls cudaStreamCreateWithFlags(..., cudaStreamNonBlocking). The MLU shim only provides cudaStreamCreate, cudaMemcpyAsync, and cudaStreamSynchronize; it does not map cudaStreamCreateWithFlags or cudaStreamNonBlocking. As a result, builds with USE_MLU enabled will fail to compile.
…LU shim Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
|
Thanks for the catch — you're right, the MLU shim only provides Fixed in e30f420: the MLU path now uses plain Verification, stated plainly: I don't have MLU headers here, so I can't run a real |
alogfans
left a comment
There was a problem hiding this comment.
cudaStreamSynchronize(stream) waits for the copy, but does not ensure that the producer has finished writing src on another stream. Switching to cudaStreamNonBlocking may therefore expose a data race.
Please preserve producer-to-copy ordering, following [TENT NVLink’s synchronization](https://github.com/kvcache-ai/Mooncake/blob/main/mooncake-transfer-engine/tent/src/transport/nvlink/nvlink_transport.cpp#L98) (#3570). For TCP, establish this dependency at submission; synchronizing cudaStreamPerThread in the I/O thread would target the wrong stream.
|
Addressed in MLU now stays on its original synchronous Added Validation: the three-file pre-commit run passed, including clang-format and codespell. I did not run a local build or CUDA test because available disk dropped below the 15 GiB local build gate, and I did not run a real |
|
Follow-up to the verification note:
|
|
Hardware validation for this PR, since CI has no CUDA host: on real GPUs the legacy-stream staging does more than add latency. Through Why it deadlocks on the base: Setup: GH200 (aarch64, CUDA 13.2, driver 595), two nodes with one GPU each;
The same hang reproduces on 2× L40S (x86, CUDA 13.3) with the current nightly wheel, for both write and read, at 4 KiB and 64 MiB, with legacy and blocking streams (8/8). Minimal repro (no torch): # Minimal repro: transfer_write_on_cuda over the TCP transport never completes
# when the caller passes the legacy default stream (or a blocking stream).
# python repro_on_cuda_tcp.py legacy -> hangs (watchdog prints HANG)
# python repro_on_cuda_tcp.py nonblocking -> OK
import ctypes, os, sys, threading
os.environ["MC_FORCE_TCP"] = "1"
from mooncake.engine import TransferEngine
kind = sys.argv[1] if len(sys.argv) > 1 else "legacy"
N = 4 << 20
rt = ctypes.CDLL("libcudart.so.13")
assert rt.cudaSetDevice(0) == 0
src, dst, s = ctypes.c_void_p(), ctypes.c_void_p(), ctypes.c_void_p()
assert rt.cudaMalloc(ctypes.byref(src), ctypes.c_size_t(N)) == 0
assert rt.cudaMalloc(ctypes.byref(dst), ctypes.c_size_t(N)) == 0
assert rt.cudaMemset(src, 0x5A, ctypes.c_size_t(N)) == 0
assert rt.cudaMemset(dst, 0, ctypes.c_size_t(N)) == 0
assert rt.cudaDeviceSynchronize() == 0
if kind != "legacy": # cudaStreamNonBlocking = 1, default (blocking) = 0
assert rt.cudaStreamCreateWithFlags(ctypes.byref(s), 1 if kind == "nonblocking" else 0) == 0
eng = TransferEngine()
assert eng.initialize("127.0.0.1", "P2PHANDSHAKE", "tcp", "") == 0
seg = f"127.0.0.1:{eng.get_rpc_port()}"
assert eng.register_memory(src.value, N, "cuda:0") == 0
assert eng.register_memory(dst.value, N, "cuda:0") == 0
threading.Timer(30, lambda: (print(f"{kind}: HANG after 30 s", flush=True), os._exit(3))).start()
eng.transfer_write_on_cuda(seg, src.value, dst.value, N, stream_ptr=s.value or 0)
assert rt.cudaStreamSynchronize(s) == 0
out = (ctypes.c_ubyte * N)()
assert rt.cudaMemcpy(out, dst, ctypes.c_size_t(N), 2) == 0
print(f"{kind}: OK" if bytes(out[:16]) == b"\x5a" * 16 and out[N - 1] == 0x5A else f"{kind}: CORRUPT", flush=True)
os._exit(0)One gap worth closing here or in a follow-up: AI assistance: the investigation and scripts were prepared with Claude; I reviewed them and ran the matrix on the hardware above. |
|
@alogfans The current head keeps MLU on the previous synchronous copy because its shim has neither stream flags nor the event/per-thread API. The other GPU paths synchronize
The hardware evidence is narrower than a full producer-ordering matrix. On GH200, this PR completes the legacy/default cases that hang on its merge-base and current main, including the two-node case, while non-blocking callers remain OK. The separate 2x L40S result reproduces legacy and blocking hangs on the nightly/base build; it is not a validation run of this PR. None of these runs queued producer work on a custom stream, so I am not presenting them as proof of that ordering case. Could you re-review the implementation against these two contracts? |
Description
With the CUDA build, the TCP transport staged device memory through a synchronous
cudaMemcpyon the legacy default stream, so any transfer of GPU buffers waited behind every kernel the process already had queued — the reporter sees streamed tokens reaching the client two at a time. TheUSE_MACAbuild already went throughcopyTcpCudaMemory(a private non-blocking stream,cudaMemcpyAsync, then a sync of that stream); the CUDA build didn't.This lifts
copyTcpCudaMemoryout of theUSE_MACAguard so every GPU build has it, and routes all four staging sites (ServerSession/ClientSessionxreadBody/writeBody) through it. The trailing stream sync keeps the copy's completion contract identical for callers.Neighbor note: #4233 pipelines CUDA TCP staging with pinned buffers but deliberately keeps
cudaStreamDefault(legacy-stream semantics) for its session stream, so the stall this fixes survives that PR. The two are complementary; whichever lands second rebases.Fixes #4323
Module
mooncake-transfer-engine)Type of Change
How Has This Been Tested?
Test commands:
Test results:
Checklist
AI Assistance Disclosure
Prepared with AI assistance (Kimi); the submitter reviewed the diff and the verification results above.