[None][feat] cache transceiver nixl bounce buffer - #15780
Conversation
5998a62 to
11e9711
Compare
08c3277 to
006072a
Compare
|
/bot run --disable-fail-fast |
|
PR_Github #56919 [ run ] triggered by Bot. Commit: |
|
PR_Github #56919 [ run ] completed with state
|
7e44ece to
1214785
Compare
|
/bot run --disable-fail-fast |
1214785 to
40c5a3b
Compare
fdff3d7 to
b7c648d
Compare
|
/bot run --disable-fail-fast |
|
PR_Github #61555 [ run ] triggered by Bot. Commit: |
|
PR_Github #61555 [ run ] completed with state
|
175552f to
2aef331
Compare
|
/bot run --disable-fail-fast |
|
PR_Github #61779 [ run ] triggered by Bot. Commit: |
|
PR_Github #61779 [ run ] completed with state
|
9eba889 to
5e6080e
Compare
50c5e24 to
136d661
Compare
NIXL bounce-buffer v2 (variable-region arena KV-transfer coalescing) — the single-IO-thread, lock-free building blocks (no GPU/network dependencies except the kernel/arena): - BuddyAllocator: power-of-two buddy allocator (byte offsets, alloc/free/coalesce). - CreditScheduler: per-flow credit window + round-robin fair scheduling over one shared arena (acquireLocal/releaseLocal for the local-gather role; onWant/onScatterDone/ reclaimByPrefix/reclaimFlow for the remote role); embeds the BuddyAllocator. - BounceMessage: little-endian WANT/GRANT/DATA/ACK wire codec (WANT carries per-chunk sizes + the sender's control endpoint). - BounceConfig: env snapshot (TRTLLM_NIXL_BOUNCE_*). - BounceArena: one registered device buffer (fabric on MNNVL, else cudaMalloc). - ExecPool: gather/scatter exec contexts (stream/event/scratch) borrowed per kernel. - GatherScatterKernel: batched vectorized memcpy. - BounceTransferPlan: split (src,dst) descriptors into <=maxChunkBytes chunks. With L0 unit tests (buddy, scheduler, codec, plan, arena, exec, kernel). Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…gine + agent integration The reactor and wiring on top of the core building blocks: - BounceTransport: one IO thread + M scatter workers; decomposed into BounceContext (shared deps + the one CreditScheduler + sendGrants), BounceSender ([S]: submit -> WANT, GRANT -> gather+RDMA-write, ACK -> resolve), BounceReceiver ([R]: WANT -> grant regions, DATA -> scatter, ACK). No notifMsg: getXferStatus==SUCCESS ⇒ landed. - ControlChannel + ZmqControlChannel: ROUTER recv + per-peer DEALER send; non-blocking send (drop on full SNDHWM), routable-IP bind (common::getLocalIp, IPv4/IPv6). - TransferEngine + NixlTransferEngine (production RDMA over the same nixlAgent) + LocalCopyTransferEngine (loopback for tests). - NixlTransferAgent integration: maybeInitBounce (arena+exec+transport, register arena, bind a routable control endpoint), shouldUseBounce routing, AgentDesc carries the bounce endpoint, WANT self-bootstraps the reverse control path, invalidateRemoteAgent -> forgetPeer; opt-in via TRTLLM_NIXL_BOUNCE_ENABLE (disabled path byte-identical). - common::FabricMemory extracted from batch_manager for the fabric-backed arena. With unit + real-NIXL-RDMA e2e tests (zmq, transport, failure, nixl-engine, nixl e2e, agent e2e). Build gated on ENABLE_UCX (-DTLLM_BOUNCE_V2). Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Lean as-built design doc for bounce v2: motivation, module map, control-plane (credit flow + fair scheduling) and data-plane pipeline, single-IO-thread model, sequence + state diagrams (region lifecycle, sender Request state machine), error handling / lifecycle, config env vars, and test coverage. Mermaid validated. Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Resource-cleanup paths discarded their results, hiding leaks/faults. Use the
project's idiomatic warn-only cleanup check (TLLM_CUDA_CHECK_WARN, as in tllmBuffers
/ cudaMemPool / ncclUtils) for the CUDA teardown/thread-pin calls that must not throw,
and explicit status/throw checks for the NIXL ones (no equivalent macro):
- ExecPool::~ExecPool / BounceArena::~BounceArena: cudaSetDevice + cudaFree/cudaFreeHost
/cudaStreamDestroy/cudaEventDestroy were (void)-discarded -> TLLM_CUDA_CHECK_WARN.
- LocalCopyTransferEngine: cudaEventDestroy (dtor / record-fail / release) -> TLLM_CUDA_CHECK_WARN.
- BounceTransport: ioLoop/scatterWorker thread-pin cudaSetDevice + shutdown cudaSetDevice/
cudaDeviceSynchronize (can't throw out of a thread fn / dtor) -> TLLM_CUDA_CHECK_WARN.
- NixlTransferEngine: releaseXferReq (dtor / postWrite-error / release) sat in empty
catch(...){}, and deregisterMem discarded its status -> a shared releaseXferLogged
helper + deregisterMem now check the nixl_status and catch+log throws.
- maybeInitBounce: cudaGetDevice failure silently used device 0 -> warn.
Kept explicit (the cudaError_t drives control flow, not just a warning): the scatter
worker's launch/stream-sync (ok/!ok -> ACK or not) and the gather event-query path.
The three promise->set_value() catch(...) blocks are commented as intentional (throws
only if already satisfied — benign double-resolve). No behavior change on success; all
bounce tests green (incl. real-RDMA e2e).
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Bounce's real dependencies are NIXL (data plane) + zmq (control plane); UCX is orthogonal. It was only compiled under if(ENABLE_UCX) because cppzmq happened to be fetched there (UCX's own bootstrap also uses zmq), so a NIXL-without-UCX build couldn't get bounce even though its deps were satisfiable. - cpp/CMakeLists.txt: FetchContent cppzmq when (NIXL_ROOT OR ENABLE_UCX), not only ENABLE_UCX; ucxx stays UCX-only. - nixl_utils/CMakeLists.txt: gate the bounce sources on zmq availability (pkg_check_modules(ZMQ libzmq), non-REQUIRED -> ZMQ_FOUND) instead of ENABLE_UCX, inside the existing NIXL_ROOT block. Skips cleanly when libzmq is absent. - tests/.../executor/CMakeLists.txt: same — bounce tests gated on ZMQ_FOUND (+ the fetched cppzmq header), not ENABLE_UCX. Validated: reconfigure clean; bounce still wired (TLLM_BOUNCE_V2 + sources) in the default NIXL+UCX build; full bounce build + 84 gtests pass through the new gate. Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
§5 of DESIGN.md gains a plain-language "直观理解" of schedule() (the queue-ticket metaphor for the three constraints: round-robin fairness / per-flow window W / arena backpressure) plus a worked A/B example table showing the strict A,B,A,B grant order and the c3-waits-for-ACK case. schedule() gets a function-level comment spelling out the same three rules and the one-grant-per-sweep + advance-cursor rotation, and inline notes on the while-loop and the break. Comments/doc only — no behavior change. Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…ancelWant)
"empty WANT == cancel" was an implicit convention at the call sites. Make it explicit
without touching the wire: encodeCancel(rid, endpoint) is a thin wrapper over
encodeWant(rid, {}, endpoint), and isCancelWant(chunkBytes) replaces the inline
chunkBytes.empty() check on the receiver. Same bytes (still a WANT), so it reuses
onWant's reclaim/self-bootstrap path. Codec test updated to exercise both helpers
(+ a non-empty WANT is-not-cancel case). Call sites adopt encodeCancel in the next
commit (shared file BounceTransport.cpp).
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…ckends Two off-by-default knobs for the gather/scatter descriptor copy (default = current custom kernel + H2D of the plan arrays, unchanged): - TRTLLM_NIXL_BOUNCE_CUB_COPY: use cub::DeviceMemcpy::Batched (tuned load-balancing across heterogeneous buffer sizes) instead of the custom kernel. ExecPool pre-sizes a per-context cub workspace (batchedCopyCubTempBytes for maxDescs). - TRTLLM_NIXL_BOUNCE_ZEROCOPY_ARGS: kernel reads the [srcs|dsts|sizes] plan arrays straight from mapped pinned host (cudaHostAllocMapped + cudaHostGetDevicePointer), skipping their H2D. Likely a loss for large n (PCIe reads in-kernel) — provided for A/B benchmarking, not as a default. launchPacked composes the two (arg source x copy backend); BounceConfig carries the flags; ExecPool allocates the workspace / mapped host accordingly. Also switches the cancel call sites to encodeCancel (helper added previously). Tested: gatherScatterKernelTest gains byte-exact cub / zero-copy / both round trips; bounceAgentE2ETest passes over real RDMA in all four off/cub/zerocopy/both combos; full 84-test bounce suite green with both knobs OFF (no regression). Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Erasing the LAST flow from the round-robin ring left `mCursor %= mRing.size()` to execute with size()==0 -- modulo by zero (UB; a deterministic SIGFPE in -O0 builds, silently folded away at -O2). Any normal completion or reclaim of the only active flow hits this path. Reset the cursor and return early when the ring empties; the non-empty path is unchanged. Add a regression test that drains a single flow to empty the ring and verifies scheduling still rotates fairly on refill. Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
136d661 to
be9dd02
Compare
|
/bot run --disable-fail-fast |
WalkthroughAdds an opt-in Bounce v2 transport for NIXL GPU transfers. The change defines wire protocols, memory and scheduling components, control channels, asynchronous transfer handling, agent handshake integration, build configuration, and C++, CUDA, NIXL, ZeroMQ, and Python tests. ChangesBounce v2 transport
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant NixlTransferAgent
participant BounceTransport
participant ControlChannel
participant NixlTransferEngine
NixlTransferAgent->>BounceTransport: Submit eligible VRAM descriptors
BounceTransport->>ControlChannel: Exchange WANT and GRANT messages
BounceTransport->>NixlTransferEngine: Post remote writes
NixlTransferEngine-->>BounceTransport: Report write completion
BounceTransport->>ControlChannel: Exchange DATA and ACK messages
BounceTransport-->>NixlTransferAgent: Resolve transfer status
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (14)
tests/unittest/disaggregated/test_cache_transceiver_single_process.py (2)
1564-1567: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the redundant
env.popcall.The loop at lines 1549-1551 already deletes every variable whose name starts with
TRTLLM_NIXL_BOUNCE_.TRTLLM_NIXL_BOUNCE_DISABLE_FABRIC_MEMORYmatches that prefix, so line 1567 never removes anything. Keep the comment, which explains the intent, and drop the call.♻️ Proposed cleanup
# Do not override TRTLLM_NIXL_BOUNCE_DISABLE_FABRIC_MEMORY: the test must # exercise the production default, including automatic cudaMalloc fallback - # on devices without fabric-memory support. - env.pop("TRTLLM_NIXL_BOUNCE_DISABLE_FABRIC_MEMORY", None) + # on devices without fabric-memory support. The prefix loop above already + # cleared it from the inherited environment.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unittest/disaggregated/test_cache_transceiver_single_process.py` around lines 1564 - 1567, Remove the redundant env.pop call for TRTLLM_NIXL_BOUNCE_DISABLE_FABRIC_MEMORY while preserving the existing comment and prefix-based environment cleanup loop.
1501-1582: 📐 Maintainability & Code Quality | 🔵 TrivialTest coverage summary (required for
tests/**).1. Test functions added, modified, or removed
Added, Python:
test_python_nixl_cache_transceiver_uses_cpp_bouncewith four parameters:v2_mha,v2_mla_tp2,v1_dsa_indexer,v2_ctx_tp2_gen_pp2.Modified, Python:
run_transfer_testnow pinskv_cache_bounce_size_mb=0. This is a shared helper, so the change affects every existing caller in the file.Added, C++ gtest (files supplied in this review):
bounceMessageCodecTest.cpp: 16 tests covering WANT, cancel, GRANT, DATA, ACK, empty payloads, >65535 entry counts, short blobs, bad magic, inflated payload length, cross-type decode, and handshake round-trip, garbage, and truncation.bounceTransferPlanTest.cpp: 16 tests covering empty plans, 32-byte alignment, chunk-size and descriptor-count splitting, oversize rejection, device-id mismatch, zero-length descriptors, descriptor merging, and scatter-run coalescing.buddyAllocatorTest.cpp: 12 tests covering rounding, no-overlap, exhaustion, coalescing, recycling, overflow rejection, double free, unknown free, and fragmentation.creditSchedulerTest.cpp: 20 tests covering inflight caps, fairness, reclamation, busy-region deferral, tombstone removal, cursor reset, variable-size packing, and large-chunk starvation.zmqControlChannelTest.cpp: 5 tests covering bidirectional TCP, IPv6, FIFO ordering, idle timeout, endpoint validation, and non-blocking send.Added, C++ gtest targets declared in
bounce/CMakeLists.txtbut not supplied in this review:bounceConfigTest,bounceArenaTest,execPoolTest,gatherScatterKernelTest,bounceTransportTest,bounceTransportFailureTest,bounceNixlE2ETest,bounceAgentE2ETest, and the opt-inbounceTransportTsanTest.2. Test list membership
test_python_nixl_cache_transceiver_uses_cpp_bounceis added totests/integration/test_lists/test-db/l0_h100.ymlat line 101. The entry has no parameter filter, so all four parameters run.- The C++ gtest targets are registered through
add_gtestincpp/tests/unit_tests/executor/bounce/CMakeLists.txtand run through the C++ test harness. They do not belong in the integration test lists.3. Coverage verdict: needs follow-up
Reasons:
- Nine of the declared C++ test files in this layer were not supplied in this prompt. Their assertions and gating are unverified.
- The
v2_mla_tp2andv2_ctx_tp2_gen_pp2parameters need two GPUs. Confirm that thel0_h100tier at line 101 provides them, and confirm the tier's wall-time budget for four nestedpytestsubprocesses.- No test asserts the fallback behavior that the PR objectives describe, where bounce initialization failure falls back to the standard NIXL path. Add a case that forces bounce initialization to fail and confirms the transfer still completes.
- The effect of
kv_cache_bounce_size_mb=0on the pre-existing tests intest_cache_transceiver_single_process.pyis unconfirmed. See the separate comment on lines 998-1000.As per path instructions: "Always produce a test coverage summary, even if no issues are found."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unittest/disaggregated/test_cache_transceiver_single_process.py` around lines 1501 - 1582, Extend test coverage around test_python_nixl_cache_transceiver_uses_cpp_bounce by adding a scenario that forces bounce initialization failure and verifies the standard NIXL path still completes. Validate that the v2_mla_tp2 and v2_ctx_tp2_gen_pp2 parameters are gated to a two-GPU-capable tier and fit its timeout budget, and verify the shared run_transfer_test kv_cache_bounce_size_mb=0 change does not regress existing callers. Supply or explicitly gate the declared bounce C++ test targets so their assertions and execution coverage are verifiable.Source: Path instructions
cpp/tests/unit_tests/executor/bounce/bounceTransferPlanTest.cpp (1)
94-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert
TllmExceptioninstead of accepting any exception.Replace the five
EXPECT_ANY_THROWassertions withEXPECT_THROW(..., tensorrt_llm::common::TllmException). Add a direct include fortensorrt_llm/common/tllmException.h.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tests/unit_tests/executor/bounce/bounceTransferPlanTest.cpp` around lines 94 - 131, In the BounceTransferPlan tests, replace all five EXPECT_ANY_THROW assertions with EXPECT_THROW assertions specifying tensorrt_llm::common::TllmException, including the cases in DescLargerThanChunkThrows, MaxChunkSizeBytesAboveU32Throws, MixedSourceDeviceIdsThrow, and MixedDestinationDeviceIdsThrow. Add a direct include for tensorrt_llm/common/tllmException.h.cpp/tests/unit_tests/executor/bounce/CMakeLists.txt (1)
57-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
PkgConfig::ZMQfor both ZMQ consumers.Change
pkg_check_modules(ZMQ libzmq)topkg_check_modules(ZMQ IMPORTED_TARGET libzmq). LinkPkgConfig::ZMQand remove the separate${ZMQ_INCLUDE_DIRS}and${ZMQ_LIBRARIES}entries. This preserves pkg-config include paths, library paths, and linker flags.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tests/unit_tests/executor/bounce/CMakeLists.txt` around lines 57 - 69, Update the ZMQ discovery in the CMake configuration around pkg_check_modules to request the imported target, then link zmqControlChannelTest against PkgConfig::ZMQ. Remove the separate ${ZMQ_INCLUDE_DIRS} include path and ${ZMQ_LIBRARIES} link entry while retaining the cppzmq include directory.cpp/tests/unit_tests/executor/bounce/bounceTransportTest.cpp (1)
100-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
ConcurrentRequestsToSameReceiverdoes not test concurrency or a shared receiver.The test calls
runTransfertwice in sequence. Each call builds a new agent pair (btConc1A/btConc1B, thenbtConc2A/btConc2B), submits one transfer, waits for it, and shuts both nodes down. No two requests are ever in flight at the same time, and the two transfers do not share a receiver.Concurrent submission is covered by
BounceTransportFailure.ConcurrentMultiThreadedSubmitincpp/tests/unit_tests/executor/bounce/bounceTransportFailureTest.cpp. Either submit two futures on one node pair before waiting, or rename the test to describe the sequential behavior it actually checks.♻️ Sketch of a real concurrent-submit variant
TEST(BounceTransport, ConcurrentRequestsToSameReceiver) { // ... build one node pair A/B, wirePair(*A, *B) ... auto b1 = bounce_test::makeXferBufs(8, 1024, /*seed=*/1); auto b2 = bounce_test::makeXferBufs(8, 1024, /*seed=*/2); auto f1 = A->tx->submit(b1.srcDescs, b1.dstDescs, B->name); // both in flight auto f2 = A->tx->submit(b2.srcDescs, b2.dstDescs, B->name); // ... then wait on f1 and f2 and verify both ... }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tests/unit_tests/executor/bounce/bounceTransportTest.cpp` around lines 100 - 105, Update the ConcurrentRequestsToSameReceiver test to create one shared sender/receiver pair, submit two distinct transfers through the same receiver before waiting on either result, then await and verify both futures before cleanup. Do not use separate sequential runTransfer calls; reuse the existing node-pair setup and transfer verification helpers.cpp/tests/unit_tests/executor/bounce/bounceArenaTest.cpp (2)
43-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBrace the skip guards.
The repository C++ style requires braces on every
ifbody. Lines 43-44 and 59-60 use unbraced bodies. The same pattern exists in other bounce test files.As per coding guidelines: "Use Allman brace style; always brace if/else, loop, and switch bodies".
Also applies to: 59-60
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tests/unit_tests/executor/bounce/bounceArenaTest.cpp` around lines 43 - 44, Add Allman-style braces around the skip guards controlled by hasCuda() at both affected locations, and apply the same bracing style to matching unbraced skip guards in the other bounce test files.Source: Coding guidelines
43-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUnbraced single-statement bodies across the bounce tests. Several new test files write single-statement
ifandforbodies without braces. The repository C++ style requires braces on all such bodies. One consistent edit fixes every site.
cpp/tests/unit_tests/executor/bounce/bounceArenaTest.cpp#L43-L60: brace the twoif (!hasCuda()) GTEST_SKIP();guards at lines 43-44 and 59-60.cpp/tests/unit_tests/executor/bounce/bounceTransportFailureTest.cpp#L107-L112: brace thehasCuda()and node-null guards at lines 107-108, 111-112, 127-128, 154-155, 158-159, 175-176, 179-180, 202-203, 216-217, and theforbodies at lines 242-243 and 293-294.cpp/tests/unit_tests/executor/bounce/execPoolTest.cpp#L41-L42: brace the skip guards at lines 41-42 and 82-83.cpp/tests/unit_tests/executor/bounce/gatherScatterKernelTest.cpp#L180-L182: brace the nestedforbodies at lines 180-182 and 255-257, and the loop andifbodies at lines 262-267.As per coding guidelines: "Use Allman brace style; always brace if/else, loop, and switch bodies".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tests/unit_tests/executor/bounce/bounceArenaTest.cpp` around lines 43 - 60, Brace every single-statement if and for body using Allman style across the bounce tests: in cpp/tests/unit_tests/executor/bounce/bounceArenaTest.cpp lines 43-60, cpp/tests/unit_tests/executor/bounce/bounceTransportFailureTest.cpp lines 107-112, 111-112, 127-128, 154-155, 158-159, 175-176, 179-180, 202-203, 216-217, 242-243, and 293-294, cpp/tests/unit_tests/executor/bounce/execPoolTest.cpp lines 41-42 and 82-83, and cpp/tests/unit_tests/executor/bounce/gatherScatterKernelTest.cpp lines 180-182, 255-257, and 262-267. Preserve the existing guard and loop behavior while adding braces to all listed bodies.Source: Coding guidelines
cpp/tests/unit_tests/executor/bounce/gatherScatterKernelTest.cpp (1)
93-101: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winThe gather step does not compact the buffers.
gatherDst[i]uses the sameoff[i]asgatherSrc[i], so the "packed" region has the identical sparse, 256-aligned layout as the source. The kernel therefore never copies into a compacted destination layout, which is the layout the transfer planner produces for a real chunk.Use a second, tightly-packed offset array for the arena region so the test covers unequal src and dst layouts.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tests/unit_tests/executor/bounce/gatherScatterKernelTest.cpp` around lines 93 - 101, The gather/scatter test currently uses the sparse source offsets for both source and packed-region addresses. Introduce a separate tightly packed offset sequence for the arena, and use it for gatherDst and scatterSrc while retaining off[i] for gatherSrc and scatterDst, so the test exercises unequal source and destination layouts.cpp/tests/unit_tests/executor/bounce/bounceAgentE2ETest.cpp (1)
140-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRestore the bounce environment variables with an RAII guard.
setenvcalls in this file are undone only by the explicitclearBounceEnv()at the end of each test. If the agent construction fails, the test at Line 162 callsGTEST_SKIP()at Line 188 without clearing the variables. The same happens if anyASSERT_*in the test body aborts the test early. The variables then stay set for every later test in the same process, and bounce becomes enabled where the test did not request it.
bounceConfigTest.cppalready defines aScopedEnvclass for this purpose. Extract it into a shared test header and use it here, or add a small local RAII guard whose destructor callsclearBounceEnv().Also note that
setBounceEnv()never setsTRTLLM_NIXL_BOUNCE_USE_NIXL_NOTIFICATIONS, butclearBounceEnv()unsets it. That asymmetry is safe today, but it hides the dependency on the notification test at Line 275.Also applies to: 162-189
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tests/unit_tests/executor/bounce/bounceAgentE2ETest.cpp` around lines 140 - 148, Replace the manual bounce environment cleanup around setBounceEnv and clearBounceEnv with an RAII guard, preferably by reusing a shared ScopedEnv from bounceConfigTest.cpp or adding a local guard whose destructor calls clearBounceEnv. Ensure the guard covers the test setup and body so GTEST_SKIP or ASSERT_* still restores all bounce variables; include TRTLLM_NIXL_BOUNCE_USE_NIXL_NOTIFICATIONS in the environment setup or otherwise keep the managed variable set symmetric.cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ControlChannel.h (1)
72-76: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winMark
addPeeras[[nodiscard]].
addPeerreports a failed endpoint load through its return value. Every other value-returning method in this interface is[[nodiscard]]. If a caller drops the result, all latersendTo(peer, ...)calls are dropped silently and the request stalls untilrequestTimeoutMs. Add the attribute so the compiler catches an ignored failure.♻️ Proposed change
- virtual bool addPeer(std::string const& peer, std::string const& endpoint) = 0; + [[nodiscard]] virtual bool addPeer(std::string const& peer, std::string const& endpoint) = 0;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ControlChannel.h` around lines 72 - 76, Mark the virtual addPeer method in the ControlChannel interface as [[nodiscard]] so callers must handle endpoint-registration failures. Preserve its existing signature, behavior, and documentation.cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceNvtx.h (1)
95-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a printf format attribute to the variadic helpers.
All call sites pass literal format strings, so the static analysis warning about a non-literal format string is a false positive here. The compiler still cannot check the arguments against the format. Add
__attribute__((format(printf, N, N+1)))so a format/argument mismatch becomes a compile-time diagnostic instead of undefined behavior at run time. For the constructor the implicitthisis argument 1, so the format index is 3.♻️ Proposed fix
`#ifndef` NVTX_DISABLE + __attribute__((format(printf, 3, 4))) BounceNvtxScope(std::uint32_t argb, char const* fmt, ...)`#ifndef` NVTX_DISABLE +__attribute__((format(printf, 2, 3))) inline std::uint64_t bounceRangeStart(std::uint32_t argb, char const* fmt, ...)Apply the same attribute to the
NVTX_DISABLEvariants so both builds check identically.Also applies to: 120-128
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceNvtx.h` around lines 95 - 103, Add the printf format attribute to the variadic BounceNvtxScope constructor, using format index 3 and argument index 4 because of the implicit this parameter. Apply the equivalent attribute to the NVTX_DISABLE variant as well, so both enabled and disabled builds validate format strings and arguments consistently.Source: Linters/SAST tools
cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ZmqControlChannel.cpp (1)
158-189: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winHandle a partially read multipart message.
If the second
recvat line 168 returns no value, the function returnsfalseafter it consumed the identity frame. The body frame stays in the socket queue. The nextrecvcall then reads that body frame as an identity frame, which is the desync the comment at line 175 describes. Drain the remaining frames of the current message before returningfalse.♻️ Proposed fix
zmq::message_t bodyFrame; auto r2 = mRouter.recv(bodyFrame, zmq::recv_flags::none); if (!r2.has_value()) { + // The identity frame is already consumed; drop the rest of this message so the next + // recv() starts on a frame boundary. + while (idFrame.more()) + { + zmq::message_t drop; + if (!mRouter.recv(drop, zmq::recv_flags::none).has_value()) + { + break; + } + idFrame.swap(drop); + } return false; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ZmqControlChannel.cpp` around lines 158 - 189, Update the receive path in the surrounding function so that when the second mRouter.recv call for bodyFrame fails, it drains the remaining multipart frames of the current message before returning false. Reuse the existing trailing-frame drain pattern based on the message’s more flag, while preserving the current successful [identity, body] handling and failure return behavior.cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ExecPool.cpp (1)
80-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd braces to the
ifbodies.The repository C++ style requires braces on all
ifbodies. Lines 82-91 use unbraced single statements.♻️ Proposed fix
for (auto& c : mCtxs) { if (c.scratch != nullptr) + { TLLM_CUDA_CHECK_WARN(cudaFree(c.scratch)); + } if (c.cubTemp != nullptr) + { TLLM_CUDA_CHECK_WARN(cudaFree(c.cubTemp)); + } if (c.hostPinned != nullptr) + { TLLM_CUDA_CHECK_WARN(cudaFreeHost(c.hostPinned)); // also frees its hostPinnedDev alias + } if (c.stream != nullptr) + { TLLM_CUDA_CHECK_WARN(cudaStreamDestroy(c.stream)); + } if (c.event != nullptr) + { TLLM_CUDA_CHECK_WARN(cudaEventDestroy(c.event)); + } }Based on coding guidelines: "Use Allman brace style; always brace if/else, loop, and switch bodies".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ExecPool.cpp` around lines 80 - 92, Add Allman-style braces around every single-statement if body in the mCtxs cleanup loop, covering scratch, cubTemp, hostPinned, stream, and event resource cleanup while preserving the existing conditions and calls.Source: Coding guidelines
cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/CMakeLists.txt (1)
53-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider the imported pkg-config target for libzmq.
pkg_check_modules(ZMQ libzmq)withoutIMPORTED_TARGETgives bare library names in${ZMQ_LIBRARIES}and does not apply${ZMQ_LIBRARY_DIRS}. If libzmq lives outside the default linker search path, the link fails. AddIMPORTED_TARGETand linkPkgConfig::ZMQ, which carries include dirs, link dirs, and libraries.The cppzmq include path also hardcodes the FetchContent layout
${CMAKE_BINARY_DIR}/_deps/cppzmq-src. Prefer thecppzmq_SOURCE_DIRvariable that FetchContent populates.♻️ Proposed CMake adjustment
- pkg_check_modules(ZMQ libzmq) # optional: bounce is skipped if libzmq is - # absent + pkg_check_modules(ZMQ IMPORTED_TARGET libzmq) # optional: bounce is skipped + # if libzmq is absenttarget_include_directories( - ${NIXL_WRAPPER_TARGET} PRIVATE ${CMAKE_BINARY_DIR}/_deps/cppzmq-src - ${ZMQ_INCLUDE_DIRS}) - target_link_libraries(${NIXL_WRAPPER_TARGET} PRIVATE ${ZMQ_LIBRARIES} - ${NIXL_BUILD_LIBRARY}) + ${NIXL_WRAPPER_TARGET} PRIVATE ${cppzmq_SOURCE_DIR}) + target_link_libraries(${NIXL_WRAPPER_TARGET} PRIVATE PkgConfig::ZMQ + NIXL::nixl)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/CMakeLists.txt` around lines 53 - 74, Update the libzmq discovery and linking in the ZMQ_FOUND block: call pkg_check_modules with IMPORTED_TARGET and link the NIXL_WRAPPER_TARGET against PkgConfig::ZMQ so its include paths, library directories, and libraries are propagated. Replace the hardcoded cppzmq path in target_include_directories with the FetchContent-provided cppzmq_SOURCE_DIR variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceConfig.h`:
- Around line 192-200: Update the request-timeout configuration call using
envInt to pass false for allowZero, matching the other operational knobs and
ensuring TRTLLM_NIXL_BOUNCE_REQUEST_TIMEOUT_MS=0 falls back to the default.
Apply the same change to the corresponding occurrence identified around the
second location.
In
`@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceMessage.cpp`:
- Around line 209-218: Update decodeHandshake to check blob.size() against the
combined sizes of kHandshakeMagic and kHandshakeFormatVersion before calling
serialize_utils::deserialize. Return false for undersized blobs, then preserve
the existing deserialization and validation flow for sufficiently large
handshakes.
In
`@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceTransport.cpp`:
- Around line 816-828: Add the same nTotal <= maxEntries capacity guard used by
the scatter path to the gather path before planBufs(ctx, nTotal) and
appendSplitInto write entries. Use the existing guard behavior and return or
propagate the established failure result when the planned entry count exceeds
maxEntries, ensuring no buffer planning or writes occur beyond capacity.
- Around line 1498-1528: Add an exception boundary in BounceTransport::ioLoop
around recv(), dispatch(), and all drain operations so zmq::error_t or other
failures are caught, logged, and converted into failure handling for affected
transfers without terminating the IO thread or process. Preserve
status-code-based transfer behavior. NixlTransferEngine.cpp lines 110-166
requires no direct change; do not add exception wrappers there.
In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ExecPool.cpp`:
- Around line 50-73: Update the constructor’s resource-allocation loop to catch
exceptions from TLLM_CUDA_CHECK, invoke a private cleanup helper for all
already-initialized entries in mCtxs, then rethrow; ensure the helper safely
releases each allocated stream, event, device buffer, pinned buffer, and
optional cubTemp before construction fails.
In
`@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/NixlNotifControlChannel.cpp`:
- Around line 110-129: Update NixlNotifControlChannel::sendTo to check mPeers
membership while holding mMu, then release the mutex before invoking
mAgent->genNotif. Preserve the unknown-peer warning and early return, while
ensuring the fabric operation and its failure logging occur outside the lock.
- Around line 88-95: In the addPeer flow around loadRemoteMD, track whether this
call inserted the metadata and only invalidate or remove it on a
loadedName-versus-peer mismatch when newly inserted. Preserve existing metadata
for agents already used by the data plane, while retaining the warning and false
return behavior.
In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/transferAgent.cpp`:
- Around line 254-276: Update shouldUseBounce in
cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/transferAgent.cpp at
lines 254-276 to reject requests when any source/destination descriptor length
pair differs or any descriptor exceeds cfg.maxChunkSizeBytes, while retaining
the existing eligibility checks. In BounceTransport.cpp at lines 595-597, keep
the gate as the primary defense and catch TllmException from
BounceTransferPlan::build, resolving the returned future with kFAILURE instead
of propagating the exception.
In `@cpp/tensorrt_llm/executor/cache_transmission/transferAgent.cpp`:
- Around line 128-132: Update AgentDesc deserialization around bounceHandshake
to accept EOF immediately after the VRAM regions by using an empty handshake,
while continuing to fail when a present handshake is truncated. Preserve
validation for region data and add a regression test covering blobs produced by
the legacy serializer layout.
In `@cpp/tests/unit_tests/executor/bounce/bounceNixlE2ETest.cpp`:
- Around line 50-203: Remove the duplicated helper implementations from
bounceNixlE2ETest and include bounceTestNixlNode.h instead. Update references to
hasCuda, XferBufs, makeXferBufs, verifyXferBufs, Node, makeNode, and wirePair to
use the bounce_test:: namespace, and use the shared freeXferBufs helper where
cleanup is needed.
In `@cpp/tests/unit_tests/executor/bounce/buddyAllocatorTest.cpp`:
- Around line 298-304: Apply Allman-style braces to every specified conditional
body: in cpp/tests/unit_tests/executor/bounce/buddyAllocatorTest.cpp:298-304,
brace both the if and else branches inside the for loop; in
cpp/tests/unit_tests/executor/bounce/creditSchedulerTest.cpp:282-286, brace the
s.heldCount(p) > 0 body, and also brace the repeated statement at lines 296-297.
In `@cpp/tests/unit_tests/executor/bounce/CMakeLists.txt`:
- Around line 76-94: Update the NIXL reactor-test conditional around
BOUNCE_REACTOR_SRCS to require both ZMQ_FOUND and the existence of
${CMAKE_BINARY_DIR}/_deps/cppzmq-src, matching the earlier test gate. Keep the
existing sources, include directories, and libraries unchanged.
In `@cpp/tests/unit_tests/executor/bounce/creditSchedulerTest.cpp`:
- Around line 288-300: In the loop around the served-count guard, assert that
m.owner is non-empty before dereferencing m.owner.begin(). Keep the existing
flow that reads the owner entry, frees it, and re-grants it unchanged after the
assertion.
In `@cpp/tests/unit_tests/executor/bounce/gatherScatterKernelTest.cpp`:
- Around line 146-153: Add the existing CUDA-device availability guard used by
the other bounce tests at the start of `GatherScatterKernel.ZeroBuffersIsNoop`,
before calling `cudaStreamCreate`, so the test skips when no device is present
while preserving its current assertions otherwise.
In `@tests/integration/test_lists/test-db/l0_h100.yml`:
- Around line 100-101: Reduce the pre_merge runtime of the test entry for
test_python_nixl_cache_transceiver_uses_cpp_bounce in l0_h100.yml by limiting it
to representative parameter cases or moving the full parameter coverage to a
separate suite. Preserve coverage of the C++ bounce v2 integration while
preventing all four potentially 180-second nested pytest cases from running
serially in pre_merge.
In `@tests/unittest/disaggregated/test_cache_transceiver_single_process.py`:
- Around line 1548-1563: Update the subprocess environment setup before the
child launch to remove UCX_NET_DEVICES and explicitly set UCX_TLS to
"^ib,gdr_copy" and TRTLLM_NIXL_NUM_THREADS to "1". Keep the existing
TRTLLM_NIXL_BOUNCE_* and logging configuration unchanged.
---
Nitpick comments:
In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceNvtx.h`:
- Around line 95-103: Add the printf format attribute to the variadic
BounceNvtxScope constructor, using format index 3 and argument index 4 because
of the implicit this parameter. Apply the equivalent attribute to the
NVTX_DISABLE variant as well, so both enabled and disabled builds validate
format strings and arguments consistently.
In
`@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ControlChannel.h`:
- Around line 72-76: Mark the virtual addPeer method in the ControlChannel
interface as [[nodiscard]] so callers must handle endpoint-registration
failures. Preserve its existing signature, behavior, and documentation.
In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ExecPool.cpp`:
- Around line 80-92: Add Allman-style braces around every single-statement if
body in the mCtxs cleanup loop, covering scratch, cubTemp, hostPinned, stream,
and event resource cleanup while preserving the existing conditions and calls.
In
`@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ZmqControlChannel.cpp`:
- Around line 158-189: Update the receive path in the surrounding function so
that when the second mRouter.recv call for bodyFrame fails, it drains the
remaining multipart frames of the current message before returning false. Reuse
the existing trailing-frame drain pattern based on the message’s more flag,
while preserving the current successful [identity, body] handling and failure
return behavior.
In `@cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/CMakeLists.txt`:
- Around line 53-74: Update the libzmq discovery and linking in the ZMQ_FOUND
block: call pkg_check_modules with IMPORTED_TARGET and link the
NIXL_WRAPPER_TARGET against PkgConfig::ZMQ so its include paths, library
directories, and libraries are propagated. Replace the hardcoded cppzmq path in
target_include_directories with the FetchContent-provided cppzmq_SOURCE_DIR
variable.
In `@cpp/tests/unit_tests/executor/bounce/bounceAgentE2ETest.cpp`:
- Around line 140-148: Replace the manual bounce environment cleanup around
setBounceEnv and clearBounceEnv with an RAII guard, preferably by reusing a
shared ScopedEnv from bounceConfigTest.cpp or adding a local guard whose
destructor calls clearBounceEnv. Ensure the guard covers the test setup and body
so GTEST_SKIP or ASSERT_* still restores all bounce variables; include
TRTLLM_NIXL_BOUNCE_USE_NIXL_NOTIFICATIONS in the environment setup or otherwise
keep the managed variable set symmetric.
In `@cpp/tests/unit_tests/executor/bounce/bounceArenaTest.cpp`:
- Around line 43-44: Add Allman-style braces around the skip guards controlled
by hasCuda() at both affected locations, and apply the same bracing style to
matching unbraced skip guards in the other bounce test files.
- Around line 43-60: Brace every single-statement if and for body using Allman
style across the bounce tests: in
cpp/tests/unit_tests/executor/bounce/bounceArenaTest.cpp lines 43-60,
cpp/tests/unit_tests/executor/bounce/bounceTransportFailureTest.cpp lines
107-112, 111-112, 127-128, 154-155, 158-159, 175-176, 179-180, 202-203, 216-217,
242-243, and 293-294, cpp/tests/unit_tests/executor/bounce/execPoolTest.cpp
lines 41-42 and 82-83, and
cpp/tests/unit_tests/executor/bounce/gatherScatterKernelTest.cpp lines 180-182,
255-257, and 262-267. Preserve the existing guard and loop behavior while adding
braces to all listed bodies.
In `@cpp/tests/unit_tests/executor/bounce/bounceTransferPlanTest.cpp`:
- Around line 94-131: In the BounceTransferPlan tests, replace all five
EXPECT_ANY_THROW assertions with EXPECT_THROW assertions specifying
tensorrt_llm::common::TllmException, including the cases in
DescLargerThanChunkThrows, MaxChunkSizeBytesAboveU32Throws,
MixedSourceDeviceIdsThrow, and MixedDestinationDeviceIdsThrow. Add a direct
include for tensorrt_llm/common/tllmException.h.
In `@cpp/tests/unit_tests/executor/bounce/bounceTransportTest.cpp`:
- Around line 100-105: Update the ConcurrentRequestsToSameReceiver test to
create one shared sender/receiver pair, submit two distinct transfers through
the same receiver before waiting on either result, then await and verify both
futures before cleanup. Do not use separate sequential runTransfer calls; reuse
the existing node-pair setup and transfer verification helpers.
In `@cpp/tests/unit_tests/executor/bounce/CMakeLists.txt`:
- Around line 57-69: Update the ZMQ discovery in the CMake configuration around
pkg_check_modules to request the imported target, then link
zmqControlChannelTest against PkgConfig::ZMQ. Remove the separate
${ZMQ_INCLUDE_DIRS} include path and ${ZMQ_LIBRARIES} link entry while retaining
the cppzmq include directory.
In `@cpp/tests/unit_tests/executor/bounce/gatherScatterKernelTest.cpp`:
- Around line 93-101: The gather/scatter test currently uses the sparse source
offsets for both source and packed-region addresses. Introduce a separate
tightly packed offset sequence for the arena, and use it for gatherDst and
scatterSrc while retaining off[i] for gatherSrc and scatterDst, so the test
exercises unequal source and destination layouts.
In `@tests/unittest/disaggregated/test_cache_transceiver_single_process.py`:
- Around line 1564-1567: Remove the redundant env.pop call for
TRTLLM_NIXL_BOUNCE_DISABLE_FABRIC_MEMORY while preserving the existing comment
and prefix-based environment cleanup loop.
- Around line 1501-1582: Extend test coverage around
test_python_nixl_cache_transceiver_uses_cpp_bounce by adding a scenario that
forces bounce initialization failure and verifies the standard NIXL path still
completes. Validate that the v2_mla_tp2 and v2_ctx_tp2_gen_pp2 parameters are
gated to a two-GPU-capable tier and fit its timeout budget, and verify the
shared run_transfer_test kv_cache_bounce_size_mb=0 change does not regress
existing callers. Supply or explicitly gate the declared bounce C++ test targets
so their assertions and execution coverage are verifiable.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 7f5ace51-4d35-4a57-aa6e-1c4f9c1027f0
📒 Files selected for processing (50)
cpp/CMakeLists.txtcpp/include/tensorrt_llm/executor/transferAgent.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/CMakeLists.txtcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceArena.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceArena.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceConfig.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceMessage.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceMessage.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceNvtx.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceTransferPlan.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceTransferPlan.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceTransport.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BounceTransport.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BuddyAllocator.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/BuddyAllocator.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ControlChannel.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/CreditScheduler.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/CreditScheduler.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ExecPool.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ExecPool.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/GatherScatterKernel.cucpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/GatherScatterKernel.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/NixlNotifControlChannel.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/NixlNotifControlChannel.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/NixlTransferEngine.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/NixlTransferEngine.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/TransferEngine.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ZmqControlChannel.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/bounce/ZmqControlChannel.hcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/transferAgent.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/transferAgent.hcpp/tensorrt_llm/executor/cache_transmission/transferAgent.cppcpp/tests/unit_tests/executor/CMakeLists.txtcpp/tests/unit_tests/executor/bounce/CMakeLists.txtcpp/tests/unit_tests/executor/bounce/bounceAgentE2ETest.cppcpp/tests/unit_tests/executor/bounce/bounceArenaTest.cppcpp/tests/unit_tests/executor/bounce/bounceConfigTest.cppcpp/tests/unit_tests/executor/bounce/bounceMessageCodecTest.cppcpp/tests/unit_tests/executor/bounce/bounceNixlE2ETest.cppcpp/tests/unit_tests/executor/bounce/bounceTestNixlNode.hcpp/tests/unit_tests/executor/bounce/bounceTransferPlanTest.cppcpp/tests/unit_tests/executor/bounce/bounceTransportFailureTest.cppcpp/tests/unit_tests/executor/bounce/bounceTransportTest.cppcpp/tests/unit_tests/executor/bounce/buddyAllocatorTest.cppcpp/tests/unit_tests/executor/bounce/creditSchedulerTest.cppcpp/tests/unit_tests/executor/bounce/execPoolTest.cppcpp/tests/unit_tests/executor/bounce/gatherScatterKernelTest.cppcpp/tests/unit_tests/executor/bounce/zmqControlChannelTest.cpptests/integration/test_lists/test-db/l0_h100.ymltests/unittest/disaggregated/test_cache_transceiver_single_process.py
- BounceTransport: exception boundary around the IO reactor tick; plan-entry capacity guard on the gather path; submit() resolves plan-build failures to kFAILURE instead of throwing out of submitTransferRequests - shouldUseBounce: screen every plan precondition (src/dst length pairs, per-descriptor size cap, per-side device uniformity) so ineligible requests fall back to the standard NIXL path - NixlNotifControlChannel: genNotif no longer runs under the channel mutex - ExecPool: constructor cleans up already-allocated CUDA resources on failure - BounceMessage: decodeHandshake bounds the endpoint length so malformed blobs return false instead of throwing - BounceConfig: document that REQUEST_TIMEOUT_MS <= 0 disables the timeout - tests: dedupe bounceNixlE2ETest via bounceTestNixlNode.h, CUDA guard in ZeroBuffersIsNoop, brace style fixes, owner-map assert, cppzmq CMake gate, pin UCX env in the Python transceiver test Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
The Python transceiver test asserted two log strings in the child's stdout, which required TLLM_LOG_LEVEL_BY_MODULE=debug:executor. A non-empty per-module level map can hang the child at exit (~15% repro in batch runs): static destruction order lets CudaMemPool's deleter TLLM_LOG_TRACE through an already-destroyed Logger module map, and the corrupted std::map::find never returns, so the child spins until the 180s subprocess timeout. Replace log parsing with a programmatic probe: - NixlTransferAgent::isBounceEnabled() / getBounceSubmitCount() (atomic counter bumped when a request is routed to the bounce fast path), exposed as bounce_enabled / bounce_submit_count on the nanobind agent and the Python wrapper — also usable for deployment checks - the test asserts them inside the child (all agents bounce-enabled, total submit count > 0) and the parent only checks the child's exit code; the by-module log env and both string assertions are gone The Logger static-destruction hang itself is a pre-existing main-library issue and will be addressed separately. Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
There was a problem hiding this comment.
🧹 Nitpick comments (3)
tests/unittest/disaggregated/test_cache_transceiver_single_process.py (3)
957-957: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the required return annotation to
run_transfer_test.This helper returns no value. Add
-> Noneto the function signature.Proposed fix
def run_transfer_test( ... expect_cpp_bounce: bool = False, -): +) -> None:As per coding guidelines: annotate every function and use
Nonefor procedures.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unittest/disaggregated/test_cache_transceiver_single_process.py` at line 957, Update the run_transfer_test function signature to include the required -> None return annotation, preserving its existing parameters and behavior.Source: Coding guidelines
1139-1142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the bound property directly.
These agents are expected to expose
bounce_enabled. Replacegetattr(agent, "bounce_enabled", False)withagent.bounce_enabledso the test checks the declared binding contract directly.Proposed fix
- assert all(getattr(agent, "bounce_enabled", False) for agent in agents), ( + assert all(agent.bounce_enabled for agent in agents), (As per coding guidelines: avoid reflection when ordinary explicit code is sufficient.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unittest/disaggregated/test_cache_transceiver_single_process.py` around lines 1139 - 1142, Update the agent validation in the test’s list comprehension to access the declared bounce_enabled property directly as agent.bounce_enabled instead of using getattr with a fallback, while preserving the existing all-agents assertion and error message.Source: Coding guidelines
1534-1559: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd the missing type annotation and remove unnecessary reflection.
- Annotate
run_transfer_testwith-> None.- Access the required
agent.bounce_enabledproperty directly.- Coverage is sufficient.
test_python_nixl_cache_transceiver_uses_cpp_bounceis listed intest-db/l0_h100.yml; no applicable QA entry exists.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unittest/disaggregated/test_cache_transceiver_single_process.py` around lines 1534 - 1559, Update run_transfer_test to explicitly return None, and replace any reflection-based access to the agent’s bounce state with direct agent.bounce_enabled property access. Keep the existing test coverage and behavior unchanged, including test_python_nixl_cache_transceiver_uses_cpp_bounce.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/unittest/disaggregated/test_cache_transceiver_single_process.py`:
- Line 957: Update the run_transfer_test function signature to include the
required -> None return annotation, preserving its existing parameters and
behavior.
- Around line 1139-1142: Update the agent validation in the test’s list
comprehension to access the declared bounce_enabled property directly as
agent.bounce_enabled instead of using getattr with a fallback, while preserving
the existing all-agents assertion and error message.
- Around line 1534-1559: Update run_transfer_test to explicitly return None, and
replace any reflection-based access to the agent’s bounce state with direct
agent.bounce_enabled property access. Keep the existing test coverage and
behavior unchanged, including
test_python_nixl_cache_transceiver_uses_cpp_bounce.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 4dac4641-6c7e-476f-939f-8aab96a5d30d
📒 Files selected for processing (5)
cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/agentBindings.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/transferAgent.cppcpp/tensorrt_llm/executor/cache_transmission/nixl_utils/transferAgent.htensorrt_llm/_torch/disaggregation/nixl/_agent_cpp.pytests/unittest/disaggregated/test_cache_transceiver_single_process.py
🚧 Files skipped from review as they are similar to previous changes (2)
- cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/transferAgent.h
- cpp/tensorrt_llm/executor/cache_transmission/nixl_utils/transferAgent.cpp
Dev Engineer Review
AgentDescserialization with bounce handshake metadata.QA Engineer Review
test_python_nixl_cache_transceiver_uses_cpp_bouncewith representative V1/V2, MLA/MHA, TP/PP, and DSA configurations.tests/integration/test_lists/test-db/l0_h100.yml.Description
NIXL Bounce-Buffer v2 — Design
1. Motivation
A single disaggregated-KV
submitTransferRequestsoften carries thousands to tens of thousands ofsmall (~4 KiB) scattered descriptors. Submitting them to NIXL/NIC one-by-one is dominated by
per-descriptor overhead, leaving the link far below line rate.
The data-path idea: the sender gathers the scattered small descriptors into one pre-registered
buffer → does a single RDMA write to a peer buffer → the receiver scatters back to the final
destinations:
The hard part is the control plane: how buffers are allocated across senders, flow-controlled,
recycled, and how errors are handled. Requirements:
(bounded buffers carry an unbounded transfer).
submit.wait()(SUCCESS/FAILURE) — never hangs.buffer — no request reserves a whole buffer, which would waste memory and cap concurrency.
R1 and R8 pull in opposite directions — carry one over-sized request and pack many small ones into
the same fixed buffer — and together motivate the variable-size region arena (§2).
2. Overview
BounceArena(a device buffer registered once) is sliced into variable-sizeregions: each chunk gets a region of exactly its byte size. Many small requests pack tightly
(high concurrency, no waste); a request larger than the arena streams chunk-by-chunk with recycling.
(exclusive write permission to a region) before writing. Credits are granted incrementally per chunk
(WANT/GRANT) and recycled on scatter completion (ACK) — bounded end to end.
submit()launches a chunk's gather before the GRANT arrives,overlapping the WANT→GRANT control round-trip with the gather kernel. Eager (credit-less) staging is
capped at half the arena, so on a bidirectional deployment both sides can always still grant
incoming regions (no mutual eager-starvation); credit-backed allocations are not capped.
notifications (UCX active messages on the RDMA fabric,
TRTLLM_NIXL_BOUNCE_USE_NIXL_NOTIFICATIONS),dropping a control hop from tens of microseconds to a few. Peers must use the same control kind —
enforced by the capability handshake (§5).
lock-free), plus M scatter workers.
notifMsg, no extra flush on the data plane: data-landed is decided purely by the senderpolling
poll==SUCCESS(the NIXL UCX backend already appendsucp_ep_flush_nbxper transfer, soSUCCESS ⇒ the data is visible at the remote target).
comes from a small
ExecPool(copyStreamCountcontexts), borrowed/returned per kernel — separatefrom a region's long lifetime (which lasts until ACK).
flowchart LR subgraph SND["Sender agent (IO thread)"] REQ["Request{ numChunks,<br/>nextPost, acked }"] OUT["shared BounceArena + ExecPool<br/>acquireLocal(bytes) → gather staging<br/>(eager: before GRANT, ≤ ½ arena)"] end subgraph RCV["Receiver agent (IO thread)"] SCH["CreditScheduler (single allocator)<br/>BuddyAllocator(arena)<br/>flows{ pending, held } + localHeld<br/>round-robin ring + drain mode"] IN["shared BounceArena<br/>variable-size regions (RDMA targets)"] SW["scatter workers ×M (borrow ExecPool)"] end REQ -- "① WANT(rid, chunk_bytes[])" --> SCH SCH -- "② GRANT(regionHandle, addr, len)" --> REQ REQ -- "③ gather→RDMA write→DATA(regionHandle, scatter runs)" --> IN IN --> SW SW -- "④ scatter_done" --> SCH SCH -- "⑤ ACK(regionHandle)" --> REQ SCH -. "owns/allocates arena regions" .-> IN REQ -. "③ occupy / ⑤ release" .-> OUTOne agent can be both sender and receiver, and both roles share the same arena (most disagg agents
only send or only receive, so one buffer halves memory; a dual-role agent still shares a single arena,
deadlock-free — the eager cap above is what makes the bidirectional case safe).
3. Terminology
BounceArena; its handle = byte offset within the arena (addr = baseAddr + offset).BounceTransferPlanby bin-packing scattered (src,dst) descriptors; ≤maxChunkSizeBytes, moved by one RDMA write.GRANT{regionHandle, addr, len}."peer\x1f rid"(concurrent requests from the same peer are distinct flows).maxInflightChunksPerRequest.BounceArena).{stream, event, scratch, hostPinned}needed to run one gather/scatter, borrowed/returned fromExecPool.GatherScatterKernel).BounceScatterRun): adjacent descriptors merged when contiguous or uniformly strided.AgentDesc(§5); peers engage bounce only when compatible.4. Modules
Pure logic (no GPU / threads / IO — unit-testable):
BounceConfigfromEnv); byte-valued vars accept binary K/M/G suffixes ("256MB", "1gb", "512KiB"); garbage values fall back to defaults instead of parsing to 0.BounceTransferPlanchunks(each ≤maxChunkSizeBytes, 32 B aligned, zero-length skipped); compute each descriptor's in-region offset andpackedBytes; coalesce the scatter view intoscatterRuns(contiguous or uniform-stride merge) so the DATA message shrinks from per-desc entries (hundreds of KB) to a handful of runs.BounceMessageencodeHandshake/decodeHandshake) and the cancel codec (encodeCancel/isCancelWant).BuddyAllocatoralloc(bytes)/free(offset), coalesces buddies, no external fragmentation, internal ≤ 2×.CreditSchedulerBuddyAllocatorover the arena; also serves local-senderacquireLocal(bytes)with the eager half-arena cap. Owned by the IO thread; the only cross-thread caller isacquireLocal()fromsubmit()app threads (eager gather staging).Device / IO / integration:
BounceArenaarenaSizeBytesdevice buffer (MNNVL viacommon::FabricMemory, elsecudaMalloc), registered once;base()/baseAddr()/at(offset).ExecPoolcopyStreamCountExecCtxs;tryAcquire()(non-blocking, nullptr when full) /release(), thread-safe.GatherScatterKernelTransferEngine(abstract)registerRegion/postWrite/poll/release.NixlTransferEngine(production, wraps onenixlAgent) is the sole implementation; transport tests run over real NIXL loopback.ControlChannel(abstract)addPeer(returns success) /removePeer/sendTo/recv/localEndpoint. Two implementations:ZmqControlChannel(default): a ROUTER for receive + one DEALER per peer for send;sendTois non-blocking (drops on fullkSendHwm, never blocks the IO thread).NixlNotifControlChannel: control messages as NIXL notifications on the RDMA fabric (no TCP sockets; "endpoint" is serialized NIXL metadata).BounceTransportBounceContext+BounceSender+BounceReceiver, routes control messages to the right role, drains both roles each tick;submit()/addPeer()/forgetPeer()/shutdown(); owns the capability handshake (localHandshakeBlob/registerPeerHandshake/hasPeerHandshake).BounceContextCreditScheduler(one arena serves both directions),sendGrants().BounceSendersubmit→WANT(+ eager gather),GRANT→attach credits / gather+write,ACK→resolve; holds the request table + send-side deferred-cleanup state (mOrphanLocal/mPendingCancel).BounceReceiverWANT→grant region,DATA→scatter, replyACK; holds scatter workers + job/done queues +mScattering(orphaned flag for in-flight scatters).BounceNvtxNixlTransferAgentintegrationmaybeInitBounce(build arena+exec+transport, register arena; any init failure warns and falls back to the standard NIXL path — never fails agent construction),shouldUseBounce(routing decision, gated on the peer handshake),AgentDesccarries the local handshake blob,invalidateRemoteAgent→forgetPeer. Built only when NIXL + zmq are available (TLLM_BOUNCE_V2); decoupled fromENABLE_UCX.5. Control Plane: Credit Flow Control + Fair Scheduling (R1/R3)
Messages (all over
ControlChannel— zmq by default, NIXL notifications opt-in):WANTGRANT{addr, len, devId(receiver), regionHandle}.DATAregionHandle+ coalesced scatter runs; sent only afterpoll==SUCCESS(the scatter trigger).ACKregionHandle; scatter done, region recyclable.Capability handshake (bootstrap + compatibility, key). Each agent's
AgentDesccarries a bouncehandshake blob:
{wireVersion, controlKind (ZMQ | NIXL_NOTIF), arenaUsableCapacityBytes, maxChunkSizeBytes, endpoint}.loadRemoteAgent→registerPeerHandshakevalidates it — version,control kind, and
maxChunkSizeBytesmust match the local config, and the peer's endpoint must beregistrable — and only then marks the peer bounce-capable.
shouldUseBouncerequireshasPeerHandshake(peer), so a peer with bounce disabled, a different control transport, or mismatchedchunking silently stays on the standard NIXL path (no WANT ever stalls to
requestTimeoutMs).An agent that cannot produce a usable local endpoint advertises no handshake (never breaks
metadata exchange).
Reverse-path bootstrap. Bounce needs a two-way control channel (sender sends WANT/DATA, receiver
replies GRANT/ACK), but the disagg metadata exchange is one-directional — the KV sender
loadRemoteAgents the receiver, the receiver never loads the sender. So WANT also carries thesender's control endpoint, and the receiver
addPeer(sender)s inonWantto bootstrap thereverse path. A malformed endpoint in a WANT is rejected on the reactor thread (warn, no grant, no
exception escapes); a cancel is still honored even when endpoint registration fails, so it can reclaim
flow state left by an earlier valid WANT. Cancel/abort uses an empty WANT (still carrying the
endpoint) — no separate handshake / RETURN message.
Endpoints are routable IPs (multi-node, ZMQ).
ZmqControlChannelmust not bind127.0.0.1(unreachable cross-node).
maybeInitBounceresolves the local routable IP via the sharedcommon::getLocalIp(getEnvNixlInterface(), rank)(TRTLLM_NIXL_INTERFACEpicks the NIC, elseauto-detect by egress route / hostname — identical to UCX/NIXL addressing) and binds
tcp://<ip>:*;localEndpoint()reads the actualtcp://<ip>:<port>from zmqlast_endpointand advertises it viathe handshake / WANT. Unit tests that construct
ZmqControlChanneldirectly keep thetcp://127.0.0.1:*default. IPv6: zmq disables IPv6 by default, so an IPv6 bind address isbracketed (
tcp://[<ip>]:*) and the ROUTER setsZMQ_IPV6; the DEALER (addPeer) setsZMQ_IPV6unconditionally (harmless for IPv4) — aligned with ucx_utils. The ROUTER additionally sets
ZMQ_ROUTER_HANDOVERso a peer that is forgotten (removePeerdrops its DEALER) and later reconnectswith the same routing id is accepted, rather than having its messages silently dropped while the stale
connection is reaped.
addPeervalidates the endpoint before connecting and reports failure to thecaller. With
NixlNotifControlChannelthe "endpoint" is serialized NIXL metadata; no TCP is involved.Receiver state (lives on the IO thread, lock-free):
BuddyAllocator arena+flows{ pending: per-chunk bytes, held: region offsets, blockedAtGrantSequence }+ a round-robinringof active flow keys + a cursor + a grant sequence counter. Fixed per-flow capW = maxInflightChunksPerRequest.schedule()— on-demand, round-robin fair, never poisons the queue, never deadlocks:flowchart TD Start([event triggers schedule:<br/>onWant / onScatterDone / reclaimFlow / reclaimByPrefix / releaseLocal]) --> D0{drain mode active?<br/>a flow bypassed ≥2 full rounds} D0 -- yes --> DAlloc{"arena.alloc(drain flow's head)<br/>fits?"} DAlloc -- no --> Hold([no NEW remote grants until it fits<br/>existing regions keep freeing]) DAlloc -- yes --> DGrant[grant it, exit drain mode] --> C1 D0 -- no --> C1{ring non-empty?} C1 -- no --> Done([return accumulated GRANTs]) C1 -- yes --> Sweep[one round-robin sweep from the cursor] Sweep --> Find{current flow:<br/>pending non-empty AND held.size < W?} Find -- "none in the whole sweep" --> Done Find -- yes --> Alloc{"arena.alloc(pending.front())<br/>fits?"} Alloc -- no --> Mark[mark flow blocked at current grant sequence] --> Sweep Alloc -- yes --> Grant["off = the allocation<br/>pending.pop_front(), held += off<br/>accumulate GRANT{off, base+off, len}<br/>advance cursor past this flow"] Grant --> C1Intuition.
schedule()answers: many remote senders want to write into the same shared arena —how to hand out arena space fairly and bounded. Three constraints:
Win-flight regions (held.size() < W);more must wait for an ACK to free one — this is the pipeline depth.
doesn't fit, skip it for now (backpressure, not an error).
Think of it as taking turns at a ticket counter:
ringis the queue of flows,cursoris "who'snext". Each inner sweep hands out exactly one region — to the flow that is next, still wants more
(
pendingnon-empty), is under its cap (held < W), and whose front chunk fits right now — thenadvances
cursorpast it and starts a fresh sweep. So grants alternate across flows ratherthan filling one flow's window first. The outer loop repeats until a whole sweep grants nothing
(all pending-empty / at-cap / can't-fit).
Example (
W=2, arena currently fits 4 regions; flow A has 3 chunks c1/c2/c3, flow B has 2 chunksd1/d2, cursor starts at A):
A/c1B/d1A/c2B/d2Grant order
A,B,A,B(strict alternation); A'sc3stays inpendinguntil one of A's regions isACKed (
onScatterDone→schedule()) and the nextschedule()grants it. Eachschedule()returnsthe new batch of GRANTs, which
sendGrantssplits by flow key and sends to the right peer.Notes:
Wbounds a single flow's pipeline depth; arena capacitybounds aggregate concurrency (
allocfailure ⇒ backpressure, not deadlock). There is no "divide capby active flow count" logic.
the current grant sequence. When other grants have bypassed it for ≥2 full rounds
(
kBypassRounds), the receiver enters drain mode for the oldest such flow: no new remote grantsare issued until that head chunk fits (existing regions keep progressing and freeing space).
acquireLocal()is deliberately unaffected — this is a receiver-only admission barrier and cannotintroduce a bidirectional circular wait. Config guarantees
maxChunkSizeBytes ≤ arena usable capacity(clamped at init), so a drained arena can always fit any chunk.rid; when bothpendingand
heldare empty,eraseIfDonedrops the flow immediately — otherwiseflows/ringgrowunbounded on a long-running server and
schedule()degrades to O(historical requests).acquireLocal(bytes)takes a gather-staging region from the sameBuddyAllocator; eager (credit-less) staging is capped at half the arena (§2); the conservationinvariant holds across
{free bytes, each flow's held, localHeld}.6. Data Plane & Pipeline (R2)
heldup toW(when space allows, one GRANT message batches multiplecredits) → the sender holds W credits at once → W chunks in flight; each ACK frees a region and
the receiver refills to keep
held = W.enableEagerGather(default),submit()immediatelystages and launches gathers for the first chunks (up to the in-flight cap and the eager half-arena
budget) before any GRANT arrives;
attachCreditslater binds arriving credits toalready-gathered chunks in strict chunk order, promoting their staging regions out of the eager
budget. The WANT→GRANT round-trip and the gather kernel overlap instead of serializing.
W ≥ ⌈(write + getXferStatus + DATA + scatter + ACK/GRANT return) / single-chunk write⌉(bandwidth-delay product); gather/scatter (D2D ~TB/s) hide in the shadow of theprevious chunk's RDMA (IB ~25 GB/s), so the NIC stays the bottleneck.
ExecCtxstream, so different chunks' gather/write/scatteroverlap in wall-clock.
(bounceOffset, dstPtr) advance contiguously or by a uniform stride collapse into one
BounceScatterRun(the fully-dense case, e.g. ctx tp1 → gen tp4, collapses thousands of descs to ahandful of runs).
TRTLLM_NIXL_BOUNCE_DISABLE_SCATTER_RUN_MERGINGrestores per-desc entries forcontrol-plane A/B debugging only.
notifMsg/ no GPUDirect flush:getXferStatus==SUCCESSalready includes NIXL's per-transferucp_ep_flush_nbx(complete at both origin and target). The sender sends DATA as soon as it pollsSUCCESS; the receiver scatters on receipt.
Single-chunk timing across both planes (control = solid, data = dashed; eager gather runs ① before
②'s GRANT when enabled):
sequenceDiagram autonumber participant App as Sender app thread participant SIO as Sender IO thread participant NIC as Data plane RDMA/NIC participant RIO as Receiver IO thread participant SW as Receiver scatter worker App->>RIO: WANT(rid, chunk_bytes[]) App-->>App: ① eager GATHER (D2D): N small src → arena.at(o) (launch + eventRecord, no sync) Note over RIO: schedule(): arena.alloc carves region s, held[flow]+=s RIO->>SIO: GRANT(rid, regionHandle=s, addr, len) Note over SIO: onGrant: attachCredits to the eager-gathered chunk (or gather now if not eager) Note over SIO: drainGatherReady: cudaEventQuery==success (gather done, no block) SIO-->>NIC: ② postWrite(arena.at(o) → addr) async RDMA, no notif, return ExecCtx on done NIC-->>RIO: data written into receiver region s Note over SIO: pollSenderHandles: poll==SUCCESS (incl. ucp_ep_flush_nbx ⇒ landed at remote target) SIO->>RIO: ③ DATA(rid, chunk, regionHandle=s, scatter runs) Note over RIO: onData: validate runs against the flow's region, enqueue ScatterJob(s, runs) RIO->>SW: ScatterJob(s) SW-->>SW: ④ SCATTER (D2D): arena.at(s) → final dst×N (borrow ExecCtx, launch + streamSync) SW->>RIO: scatter_done(s) Note over RIO: onScatterDone: arena.free(s), schedule() may GRANT the next chunk RIO->>SIO: ⑤ ACK(rid, chunk, regionHandle=s) Note over SIO: onAck: releaseLocal(region o), acked++, all ACKed ⇒ promise=SUCCESS SIO->>App: future ready (SUCCESS)For a large request (K > in-flight cap / arena capacity), the loop above repeats with both sides'
memory staying at O(W) regions while the transfer size is unbounded (R1).
7. Threading (R4/R5)
CreditScheduler+ sender request table (the keyto being lock-free; the one cross-thread entry is eager
acquireLocalfromsubmit()). Each tick:recvone control message and dispatch;drainGatherReady(gather event ready → postWrite + return ExecCtx);pollSenderHandles(poll==SUCCESS→ send DATA);drainScatterDone(worker report → send ACK + free region + reschedule);drainForgets/drainPendingPosts(retry parked credits) /checkTimeouts.thread.
submit()never blocks: registers a Request + sends WANT (+ launches eager gathers,fire-and-forget), returns a
shared_future; safe to call from multiple threads.recvuses a 0 ms timeout (low latency); fullyidle uses 1 ms; after long 0 ms spinning it backs off ~50 µs so a long model-kernel gather delay
can't busy-spin a core.
8. State Machines
Unified region lifecycle (one arena serves both roles; a region is held by exactly one kind of
owner at any time — the conservation invariant):
stateDiagram-v2 [*] --> FREE: arena init FREE --> INCOMING_HELD: GRANT(region→remote flow) 【schedule()】 FREE --> OUTGOING_HELD: acquireLocal(bytes) 【local gather staging — eager ≤ ½ arena】 INCOMING_HELD --> QUEUED: DATA(regionHandle, runs) received QUEUED --> SCATTERING: scatter worker picks it up (borrow ExecPool ctx) SCATTERING --> FREE: scatter_done → onScatterDone (reply ACK + reschedule) INCOMING_HELD --> FREE: forgetPeer / reclaimByPrefix (no in-flight DATA) OUTGOING_HELD --> FREE: ACK / failure → releaseLocal (reschedule — freed bytes go to a waiting flow) FREE --> [*]: shutdown note right of OUTGOING_HELD Invariant: every region is in exactly one of { free arena bytes } ∪ { some remote flow's held } ∪ { localHeld }. INCOMING_HELD = remote RDMA-write target (QUEUED/SCATTERING are its sub-states). OUTGOING_HELD = local gather source. Never held by both. end noteSender chunk/request lifecycle (never hangs). With eager gather a chunk may reach
Gathered(gather event signalled, ExecCtx returned) while its GRANT is still in flight;
attachCreditsthenpromotes it straight to the write:
stateDiagram-v2 [*] --> WANT_SENT: submit() registers Request + sends WANT(chunk_bytes[]) + eager gathers WANT_SENT --> GATHERING: eager acquireLocal + launch gather (no credit yet) GATHERING --> GATHERED: drainGatherReady sees cudaEventQuery==success (waiting for credit) WANT_SENT --> POSTING: GRANT received (onGrant, credits queued) GATHERED --> IN_FLIGHT: attachCredits → postWrite POSTING --> POSTING: arena/exec full → credit parked, retried by drainPendingPosts (no block) POSTING --> GATHERING: borrow ExecCtx + acquireLocal(bytes) → launch gather + eventRecord (no sync) GATHERING --> IN_FLIGHT: gather done + credit attached → postWrite + return ExecCtx IN_FLIGHT --> DATA_SENT: pollSenderHandles sees poll==SUCCESS → send DATA DATA_SENT --> POSTING: ACK received and chunks remain (releaseLocal that region) DATA_SENT --> SUCCESS: acked == numChunks WANT_SENT --> FAILURE: checkTimeouts, no progress beyond requestTimeoutMs POSTING --> FAILURE: forgetPeer / shutdown GATHERING --> FAILURE: gather launch/record/stream error / forgetPeer / shutdown IN_FLIGHT --> FAILURE: poll==kFailed / forgetPeer / shutdown SUCCESS --> [*]: promise=SUCCESS FAILURE --> [*]: promise=FAILURE (empty WANT retracts credits) note right of WANT_SENT Every terminal state resolves the promise → the caller's wait() always returns, never hangs (R5) end note9. Error Handling & Lifecycle (R5)
Every request reaches a terminal state;
wait()never hangs. Bounce failures degrade, never break:maybeInitBouncecatches any construction error (fabric alloc, zmq bind, NIXL registration) with awarning and leaves the agent on the standard per-desc NIXL path.
registerPeerHandshakeatloadRemoteAgentshouldUseBounceroutes to standard NIXL (no WANT is ever sent, no timeout burned).checkTimeoutsexceedsrequestTimeoutMsonWantaddPeerfails/throwspoll==kFailedpumpRequestflags it →drainGatherReadyinvalidateRemoteAgent→forgetPeerforgetPeerdrops the peer's DEALER + its handshake registration synchronously; the IO threadreclaimByPrefix("peer\x1f")reclaims all of the peer's flows + fails its in-flight requests. A freshloadRemoteAgentmust re-validate a new handshake.kSendHwm)sendToreturns EAGAINrequestTimeoutMs(no hang, no corruption).shutdowncudaDeviceSynchronize→ fail all in-flight requests.Concurrency-safety points:
mScatteringmap (region offset → isorphaned) of all scattering regions. If
forgetPeerreclaims a region still inmScattering(aworker is reading it),
reclaimByPrefixdefers the free (marks it orphaned) and onlyfreeOrphanRegions it on scatter completion — preventing "new sender's RDMA write ⊥ worker's read".cudaStreamSynchronizeits stream (so an abandoned gather can't write a re-granted region); a sync error WARNs and clears the
sticky error.
Writingmay still be read by the NIC asa source, so it cannot be freed immediately. It's recorded in
mOrphanLocal;drainOrphanLocal()polls its xfer to a terminal state beforerelease+releaseLocal(thesend-side orphan mechanism, symmetric to the receive-side
mScatteringorphaned flag).WANT; the receiver
reclaimFlowimmediately frees the flow's granted-but-unwritten regions(otherwise they stay held until peer loss, leaking on a long-running receiver), deferring any
scattering ones. Correspondingly
onDatavalidates withheldByFlow: if an empty WANT raced ahead ofa DATA so the region was freed/re-granted, that late DATA is dropped (never scatter a region now
owned by someone else).
launch the receiver checks every run's source range lies within this flow's granted region, and
that the expanded plan doesn't exceed local scratch capacity. Any out-of-bounds → no launch, no ACK.
cudaGetErrorString).Entry routing (transparent to callers; disabling is byte-equivalent to the original NIXL path):
flowchart TD S[submitTransferRequests] --> E{shouldUseBounce?<br/>WRITE + both-VRAM + no syncMsg<br/>+ peer handshake OK + descCount/avg<br/>+ per-side uniform deviceId} E -- no --> N[standard NIXL path] E -- yes --> SUB[submit: register Request + send WANT + eager gathers + return future] SUB --> POST[IO thread: GRANT→attach/gather+postWrite→DATA, pipelined] POST --> ERR{poll==kFailed / scatter fail / peer gone / stalled beyond requestTimeoutMs?} ERR -- yes --> F[Request → FAILURE] ERR -- no --> OK{all chunks poll SUCCESS AND all ACKed?} OK -- yes --> SU[wait = SUCCESS] F --> W[wait = FAILURE → caller task.fail]Bootstrap: the bounce handshake blob is serialized with the
AgentDesc(getLocalAgentDesc/loadRemoteAgent(AgentDesc)→registerPeerHandshake), i.e. the path production disagg already uses;the first WANT starts directly with no separate handshake round-trip.
10. Configuration (env)
All prefixed
TRTLLM_NIXL_BOUNCE_. Byte-valued variables accept case-insensitive binary suffixes(
K/KB/KiB,M/MB/MiB,G/GB/GiB; all powers of two), e.g.ARENA_SIZE_BYTES=512MB. Unparsablevalues fall back to the default (never silently become 0).
enabledENABLEarenaSizeBytesARENA_SIZE_BYTESarenaAllocationGranularityBytesARENA_ALLOCATION_GRANULARITY_BYTESmaxChunkSizeBytesMAX_CHUNK_SIZE_BYTESmaxInflightChunksPerRequestMAX_INFLIGHT_CHUNKS_PER_REQUESTcopyStreamCountCOPY_STREAM_COUNTscatterWorkerCountSCATTER_WORKER_COUNTminDescriptorCountMIN_DESCRIPTOR_COUNTmaxAverageDescriptorSizeBytesMAX_AVERAGE_DESCRIPTOR_SIZE_BYTESrequestTimeoutMsREQUEST_TIMEOUT_MSdisableFabricMemoryDISABLE_FABRIC_MEMORYcudaMallocinstead of MNNVL fabric memory (CI/x86).enableEagerGatherENABLE_EAGER_GATHERsubmit()before GRANT (overlap control RTT); eager staging capped at ½ arena.useNixlNotificationsUSE_NIXL_NOTIFICATIONSuseZeroCopyArgumentsUSE_ZERO_COPY_ARGUMENTSuseCubCopyUSE_CUB_COPYcub::DeviceMemcpy::Batchedinstead of the custom copy kernel (experimental).disableScatterRunMergingDISABLE_SCATTER_RUN_MERGINGshouldUseBouncefires when: op isWRITE, src/dst are bothVRAM, no syncMessage, the peer passedthe capability handshake,
descCount ≥ minDescriptorCount, all srcs are on this agent's device andall dsts on one device, and average desc bytes
≤ maxAverageDescriptorSizeBytes; otherwise thestandard NIXL path is used.
11. Test Coverage
Tests live under
cpp/tests/unit_tests/executor/bounce/.buddyAllocatorTest(split/coalesce/fragmentation/boundaries/overflow),creditSchedulerTest(in-flight cap/fairness/drain-mode anti-starvation/reclaim/conservation/reclaim-defer/orphan/eager budget),
bounceMessageCodecTest(round-trip/truncation/magic/cross-type-reject/large-count/handshake codec),
bounceTransferPlanTest(bin-pack boundaries +scatter-run merging),
bounceConfigTest(env parsing, byte suffixes, garbage fallback).bounceArenaTest,execPoolTest,gatherScatterKernelTest(custom kernel /zero-copy args / cub backend).
zmqControlChannelTest(incl. endpoint validation),bounceTransportTest(end-to-end, byte-exact; handshake compatibility; malformed-WANT rejection),bounceTransportFailureTest(no-GRANT timeout / engine failure / shutdown in-flight / forgetPeerin-flight / multi-peer shared-arena over-subscription no-deadlock / multi-threaded submit).
nixlTransferEngineTest;bounceNixlE2ETest(RealRdmaLoopbacksingletransfer /
ConcurrentBidirectionalRealRdma8-thread bidirectional /MultiAgentManySendersToOneReceiver/ForgetPeerInFlightRecovers);bounceAgentE2ETest(production
submitTransferRequestspath: single transfer +ConcurrentSubmitUsesBounce).All e2e tests verify byte-exactly (seed-distinct pattern per transfer, ruling out cross-talk).
test_cache_transceiver_single_process.pydrives the NIXL bounce paththrough the Python cache transceiver (added to
l0_h100.yml).12. perf compare
https://docs.google.com/document/d/1J8ROqb1D-TQryIEyqLoYtP4Z3Hqk_7AreluczjdtH5w/edit?usp=sharing
gptoss gb200_gpt-oss-120b-fp4_8k1k_con128_ctx1_tp1_gen1_tp4_eplb0_mtp0_ccb-NIXL
con128
con1024
PR Checklist
Please review the following before submitting your PR:
PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.
PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.
Test cases are provided for new code paths (see test instructions)
If PR introduces API changes, an appropriate PR label is added - either
api-compatibleorapi-breaking. Forapi-breaking, includeBREAKINGin the PR title.Any new dependencies have been scanned for license and vulnerabilities
CODEOWNERS updated if ownership changes
Documentation updated as needed
Update tava architecture diagram if there is a significant design change in PR.
The reviewers assigned automatically/manually are appropriate for the PR.
Please check this after reviewing the above items as appropriate for this PR.
GitHub Bot Help
To see a list of available CI bot commands, please comment
/bot help.