Skip to content

[NVBug 5969206][fix] Break circular dependency in disagg KV transfer timeout cleanup - #12313

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

[NVBug 5969206][fix] Break circular dependency in disagg KV transfer timeout cleanup#12313
nv-yna wants to merge 1 commit into
NVIDIA:mainfrom
nv-yna:disagg-fix-5969206

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 leak KV cache blocks until the pool is exhausted and the prefill worker becomes unresponsive.

Root cause: The kv_transfer_timeout_ms cleanup mechanism (Phase 2) only ran inside _send_kv_async, which requires new context requests to be scheduled. Once leaked blocks exhaust the KV pool, no new requests can be scheduled, so Phase 2 never runs — an unrecoverable circular dependency.

Fix: Call _check_disagg_ctx_cache_transfer_status(0) every iteration in the main executor loop, breaking the circular dependency. Timed-out requests are cleaned up regardless of whether new requests are being scheduled.

Reproduction

1P1D disaggregated serving with Qwen/Qwen3-0.6B on L40 PCIe (or any slow-interconnect setup):

  • Send 300 concurrent requests with ISL=2000+ via aiperf
  • Cancel (Ctrl+C) after ~60s
  • Repeat 4+ times
  • Final probe request hangs indefinitely

Symptoms:

  • [WARNING] Timed out waiting for context KV cache transfer after 1000 milliseconds repeating every 1s
  • has 2048 active_requests, scheduled 0 context requests — pool exhausted
  • GPU utilization 0%, memory held at ~87%
  • Health endpoint returns 200 but no requests are served

Confirmed on: L40 PCIe (localhost), H100 PCIe (cross-node), Dynamo 1.0.0 + TRT-LLM. Does NOT reproduce on H100 NVLink (transfers complete before cancel arrives).

Evidence

Experiment Stuck in state 21 KV timeouts Final probe
H100 NVLink (passing) 1 / 10,003 0 Passed
L40 PCIe (hang) 186 / 508 1,120 HUNG
H100 PCIe cross-node (hang) 836 / 920 5,500 HUNG
L40 + kv_transfer_timeout_ms=60s 4 (engine recovered) 0 HUNG (frontend Bug 2)

With the timeout workaround, Phase 1 correctly flags stuck requests (776 "Terminating by timeout"), and the engine recovers to 4 active_requests — but only when new requests trigger Phase 2. Without new requests (full pool exhaustion), recovery is impossible.

Test plan

  • Verify with L40 PCIe reproducer: 4 cancel cycles + probe should pass
  • Verify no regression on normal (non-cancel) disagg serving benchmark
  • Verify H100 NVLink still passes (was passing before, should still pass)

🔗 Full analysis: NVBugs 5969206

…timeout cleanup

In disaggregated serving, cancelled requests stuck in state 21
(kDISAGG_CONTEXT_TRANS_IN_PROGRESS) leak KV cache blocks permanently.
The kv_transfer_timeout_ms mechanism has a two-phase design:
- Phase 1 (_check_kv_transfer_timeout): flags timed-out requests every iteration
- Phase 2 (_check_disagg_ctx_cache_transfer_status): cleans up flagged requests

Phase 2 only ran inside _send_kv_async, which requires new context requests
to be scheduled. Once the KV pool is exhausted by leaked blocks, no new
requests can be scheduled, so Phase 2 never runs — creating an unrecoverable
circular dependency.

Move the Phase 2 call to the main executor loop so it runs every iteration
regardless of whether new requests are being scheduled. This allows timed-out
requests to be cleaned up even when the pool is fully exhausted.

Reproducer: 1P1D disagg with Qwen/Qwen3-0.6B on L40 PCIe, 4+ cancel cycles
with concurrency=300. The prefill worker enters a permanent timeout loop with
"Timed out waiting for context KV cache transfer after 1000 milliseconds"
and GPU utilization drops to 0%.

Also adds diagnostic logging to the cancel request path for debuggability.

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