[NVBug 5969206][fix] Force-terminate in-flight KV transfers on abort#12314
Closed
nv-yna wants to merge 1 commit into
Closed
[NVBug 5969206][fix] Force-terminate in-flight KV transfers on abort#12314nv-yna wants to merge 1 commit into
nv-yna wants to merge 1 commit into
Conversation
In disaggregated serving, when a request is cancelled while its KV cache transfer is actively in flight (state 21 DISAGG_CONTEXT_TRANS_IN_PROGRESS or state 9 DISAGG_GENERATION_TRANS_IN_PROGRESS), the C++ cancel_request() fails because it can only cancel queued transfers, not in-flight ones. Previously, failed cancels were retried every iteration indefinitely. The request stayed in transmission state forever, holding KV cache blocks that were never freed. After 2-5 cancel cycles, the KV pool was exhausted and the prefill worker hung permanently. Now, when cancel_request() returns False for an in-transmission request, force-transition it to the completed state and clean up its resources immediately. The orphaned NIXL transfer is discarded by the peer. This makes abort work unconditionally regardless of the request's state in the KV transfer pipeline, without requiring kv_transfer_timeout_ms to be configured. Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
2 tasks
yifjiang
added a commit
to yifjiang/TensorRT-LLM
that referenced
this pull request
Apr 15, 2026
… side checkContextTransferStatus retries stuck prefill-side KV cache transfers indefinitely using only the per-iteration kv_transfer_sender_future_timeout_ms. The per-request total timeout kv_transfer_timeout_ms is plumbed through config (CacheTransceiverConfig::getKvTransferTimeoutMs) but never read in batch_manager code — it is dead code. Under concurrent load with constrained cache, stuck transfers hold KV blocks forever, exhausting the pool. The prefill worker becomes permanently unresponsive while health probes continue returning 200 OK. Fix: After each per-iteration timeout in checkContextTransferStatus, check total elapsed time (via LlmRequest::getKvCacheTransferStart, already set by sendAsync) against kv_transfer_timeout_ms. When exceeded, mark the request as DISAGG_TRANS_ERROR, best-effort cancel via CacheSender, and remove from mSenderFutures so blocks can be freed. Reproducer: 1P1D disagg with Qwen3-0.6B, free_gpu_memory_fraction=0.2, NIXL over TCP, concurrency 16 with ISL 8000. Server hangs after ~2 minutes and never recovers. Related: NVIDIA#12249 (set default kv_transfer_timeout_ms=60s — config only) Related: NVIDIA#12313, NVIDIA#12314 (Python-level fixes — cannot fire due to race condition with C++ transfer completion removing requests from Python tracking before the 60s timeout elapses) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
yifjiang
added a commit
to yifjiang/TensorRT-LLM
that referenced
this pull request
Apr 15, 2026
… side checkContextTransferStatus retries stuck prefill-side KV cache transfers indefinitely using only the per-iteration kv_transfer_sender_future_timeout_ms. The per-request total timeout kv_transfer_timeout_ms is plumbed through config (CacheTransceiverConfig::getKvTransferTimeoutMs) but never read in batch_manager code — it is dead code. Under concurrent load with constrained cache, stuck transfers hold KV blocks forever, exhausting the pool. The prefill worker becomes permanently unresponsive while health probes continue returning 200 OK. Fix: After each per-iteration timeout in checkContextTransferStatus, check total elapsed time (via LlmRequest::getKvCacheTransferStart, already set by sendAsync) against kv_transfer_timeout_ms. When exceeded, mark the request as DISAGG_TRANS_ERROR, best-effort cancel via CacheSender, and remove from mSenderFutures so blocks can be freed. Reproducer: 1P1D disagg with Qwen3-0.6B, free_gpu_memory_fraction=0.2, NIXL over TCP, concurrency 16 with ISL 8000. Server hangs after ~2 minutes and never recovers. Related: NVIDIA#12249 (set default kv_transfer_timeout_ms=60s — config only) Related: NVIDIA#12313, NVIDIA#12314 (Python-level fixes — cannot fire due to race condition with C++ transfer completion removing requests from Python tracking before the 60s timeout elapses) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a permanent hang in disaggregated serving where cancelled requests with in-flight KV cache transfers leak blocks until the pool is exhausted.
Root cause: When abort arrives for a request in state 21 (
kDISAGG_CONTEXT_TRANS_IN_PROGRESS), the C++cancel_request()only succeeds for queued transfers, not in-flight ones. Failed cancels were retried every iteration indefinitely — the request stayed in transmission state forever, holding KV blocks that were never freed.Fix: When
cancel_request()returnsFalsefor an in-transmission request, force-transition it to the completed state (DISAGG_CONTEXT_COMPLETE/DISAGG_GENERATION_TRANS_COMPLETE) and clean up its resources immediately. The orphaned NIXL transfer is discarded by the peer.This makes abort work unconditionally regardless of the request's state in the KV transfer pipeline, without requiring
kv_transfer_timeout_msto be configured.Why this is the right fix
We investigated several approaches and found that any fix relying on downstream signals (first token, transfer completion, Phase 2 timeout cleanup) fails due to circular dependencies — once the KV pool is exhausted, the signal needed to trigger cleanup requires free blocks that don't exist.
The only correct approach is to make abort unconditional: when cancellation is requested, the request MUST be terminated regardless of its internal state.
kv_transfer_timeout_ms_send_kv_asyncwhich needs new requests → circular depChanges
_force_cancel_in_flight_request(): New method that force-transitions state 21→22 (or 9→12) and cleans up resources_handle_canceled_requests(): When_try_cancel_request()fails, calls force-cancel instead of retrying. Removes thestill_pending_canceled_idsretry loop entirely.Reproduction
Same as #12313. 1P1D disagg with Qwen/Qwen3-0.6B on L40 PCIe, 4+ cancel cycles with concurrency=300.
Test plan
🔗 Full analysis: NVBugs 5969206
🔗 Related: #12313 (Phase 2 circular dependency fix — complementary but insufficient alone)