Repository navigation
Conversation
MountSegmentAndGetId registered the buffer with the transfer engine, then returned the Master's MountSegment error without unregistering it. Callers (allocate_and_mount_segment) free the buffer on failure, so the registration outlived its memory: its pages stayed pinned (about 64 GiB per segment on RDMA), and every later buffer at the same address failed to register as an overlapped memory region, so the store could never mount another segment there. Seen on H200s with RDMA after a Master restart during an allocate_and_mount_segment. NonHAReconnectTest.FailedMountLeavesBufferUnregistered mounts a buffer while the Master is down, restarts the Master, and mounts the same buffer again.
he-yufeng
left a comment
There was a problem hiding this comment.
Verified the leak path against current main (245e710) before writing this:
Client::MountSegmentAndGetIdregisters with the transfer engine at client_service.cpp:3901 and then callsmaster_client_.MountSegmentat :3922. On RPC failure it returns without unregistering, and since the segment never enteredmounted_segments_(insertion happens only after a successful master mount),UnmountSegmentcan't find it later (INVALID_PARAMS), so no in-process cleanup path exists.- The failure is permanent from the process's point of view:
registerLocalMemoryrejects any later buffer overlapping that address with ERR_ADDRESS_OVERLAPPED (transfer_engine_impl.cpp:766 and friends), which matches the "store can never grow again" symptom in the description. - The in-repo caller I checked (
Client::MountSegmentat :3816) just propagates the error, so on the RDMA path the pinned pages indeed outlive the freed buffer.
The fix is placed correctly: same mutex scope and same ERR_ADDRESS_NOT_REGISTERED tolerance as the existing unregister in UnmountSegmentImpl, and there is no double-unregister risk because the failed segment never made it into mounted_segments_. The test is the right regression shape too: master down, mount fails, master back, the same buffer must mount, which fails deterministically if the registration leaked.
One note, not a request: if the master actually applied the mount but the response was lost, the client now unregisters and frees while the master keeps the segment record. That ambiguity predates this change (the old code ended in the same state, plus the leak), so it shouldn't block anything here, but it may be worth a follow-up look in the reconnect/remount path.
Scope of this review: code-path read against current main; I did not rebuild locally.
he-yufeng
left a comment
There was a problem hiding this comment.
Read this against current main and traced the engine paths it touches. The bug and the fix both check out.
Mechanism, for the record:
TransferEngineImpl::registerLocalMemoryreserves the region intryReserveMemoryRegionsbefore any transport sees it, and that bookkeeping is protocol-agnostic, so a stale reservation also breaks later TCP mounts, not just RDMA. The overlap error is exactly where a retry at the same address dies.- The client's own overlap guard in
MountSegmentAndGetIdcan't catch this becausemounted_segments_only records successful mounts. After a failed mount the engine reservation is the sole artifact, which is why every later buffer landing at that address gets refused. unregisterLocalMemory(void*, bool update_metadata = true)matches the registration'supdate_metadata=true, and toleratingERR_ADDRESS_NOT_REGISTEREDfollows the same pattern as the other unregister call sites in this file (UnmountSegmentImpl, the hot-cache unregister).
The test is a real red/green: pre-fix the second MountSegment never reaches the master, it dies inside registerLocalMemory with OVERLAPPED, so the retry loop exhausts and the assert fires. CI confirms the binary runs green here (CTest leg, non_ha_reconnect_test 27s pass). The two red legs on this run are the repo-wide wheel-test torch gap that #4440 fixed after this CI ran; a merge with current main should clear them.
Two non-blocking observations:
unregisterLocalMemoryonly erases the engine bookkeeping when every transport returns 0 (first-error early return). A partial transport failure would leave the reservation despite the new cleanup, with only the log line to show for it. Mirrors the existing behavior everywhere else, nothing to do in this PR.- Good call keeping this scoped to the mount-failure path instead of pulling it out of #2336; that one is parked and this stands on its own.
I did not rerun the test locally; verification here is code-level tracing plus the CI run above.
Description
Client::MountSegmentAndGetIdregisters the buffer with the transfer engine, then asks the master toMountSegmentit. When that RPC fails (the master restarting or briefly unreachable), it returned the error without unregistering the buffer. Its callers (allocate_and_mount_segment,MountSegment) treat a failed mount as nothing mounted and free the buffer, so the registration outlives its memory:Transfer Engine does not support overlapped memory region(ERR_ADDRESS_OVERLAPPED), so a store that adds segments at runtime can never grow again until the process restarts.The fix unregisters the buffer on that error path. The unregistration uses the default
update_metadata=true, mirroring the registration just above it andUnmountSegmentImpl.ERR_ADDRESS_NOT_REGISTEREDis tolerated there, as elsewhere in this file.#2336 contains the same rollback, as part of a larger rework of the destructor and unmount paths that is marked "DO NOT Merge Now". This PR is only the mount-failure fix, with a regression test.
Module
mooncake-transfer-engine)mooncake-store)mooncake-conductor)mooncake-reshard)mooncake-ep)mooncake-pg)mooncake-integration)mooncake-p2p-store)mooncake-wheel)mooncake-common)mooncake-rl)Type of Change
How Has This Been Tested?
Unit test. The new
NonHAReconnectTest.FailedMountLeavesBufferUnregisteredstops the in-process master, mounts a buffer (registration succeeds,MountSegmentfails), restarts the master on the same ports, and mounts the same buffer again.Test commands (Ubuntu 22.04 with
dependencies.sh, as in CI):cmake -S . -B build-test -DBUILD_UNIT_TESTS=ON -DUSE_HTTP=ON -DUSE_ETCD=ON -DUSE_CUDA=OFF -DWITH_EP=OFF -DSTORE_USE_ETCD=ON -DCMAKE_BUILD_TYPE=Release cmake --build build-test --target non_ha_reconnect_test ./build-test/mooncake-store/tests/non_ha_reconnect_testTest results: on this branch and on a backport to
v0.3.13.post1:client_service.cpprevertedFailedMountLeavesBufferUnregisteredregister_local_memory_failed ... error=-7("overlapped memory region")NonHAReconnectTesttestsEnd to end on RDMA. Our setup: stores that add and free 64 GiB segments at runtime with
allocate_and_mount_segment/unmount_and_free_segment, on 8×H200 nodes with 8 RDMA NICs each.mooncake-transfer-engine-cuda13for CPython 3.12, built fromv0.3.13.post1plus this commit with the release workflow's cuda13 x86_64 profile inpytorch/manylinux2_28-builder:cuda13.0. It has the same 85 files as the PyPI release.allocate_and_mount_segmentcalls on 3 stores: each mount failed (mount_segment_to_master_failed ... error=RPC_FAIL). Every store kept running.VmPinstayed at exactly the live segments' registrations, with no extra 64 GiB per NIC. After the master returned, all 3 grew from 2 to 8 segments with no failures and no overlapped-region errors.0.3.13.post1wheel and two on the fix. The stock store's pinned memory grew by 64 GiB × 8 NICs (the stranded registration), and it had to restart. The two stores with the fix were unaffected.Checklist
./scripts/code_format.sh(the changed files passclang-format20.1.8, the version the script requires)AI Assistance Disclosure