Skip to content

[NVBug 5969206][fix] Force-terminate in-flight KV transfers on abort#12314

Closed
nv-yna wants to merge 1 commit into
NVIDIA:mainfrom
nv-yna:disagg-fix-5969206-v2
Closed

[NVBug 5969206][fix] Force-terminate in-flight KV transfers on abort#12314
nv-yna wants to merge 1 commit into
NVIDIA:mainfrom
nv-yna:disagg-fix-5969206-v2

Conversation

@nv-yna

@nv-yna nv-yna commented Mar 18, 2026

Copy link
Copy Markdown
Collaborator

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() returns False for 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_ms to 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.

Approach Works? Why
kv_transfer_timeout_ms Partial Phase 2 cleanup only runs in _send_kv_async which needs new requests → circular dep
Move Phase 2 to main loop Partial Still requires timeout to be configured
First-token guard (delay abort) No Held requests consume blocks, preventing first token → circular dep
Force-terminate on cancel (this PR) Yes No dependencies — abort is immediate and unconditional

Changes

  • _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 the still_pending_canceled_ids retry loop entirely.

Reproduction

Same as #12313. 1P1D disagg with Qwen/Qwen3-0.6B on L40 PCIe, 4+ cancel cycles with concurrency=300.

Test plan

  • Verify with L40 PCIe reproducer: 4 cancel cycles + probe should pass
  • Verify with Dynamo + ISL=8000 reproducer (which failed with all other fixes)
  • Verify no regression on normal disagg serving benchmark
  • Verify orphaned NIXL transfers don't cause peer-side issues

🔗 Full analysis: NVBugs 5969206
🔗 Related: #12313 (Phase 2 circular dependency fix — complementary but insufficient alone)

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>
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>
@nv-yna nv-yna closed this Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant