Skip to content

[https://nvbugs/6114140][fix] avoid indefinite block in disagg gen-side KV transfer - #14414

Closed
Shixiaowei02 wants to merge 1 commit into
NVIDIA:mainfrom
Shixiaowei02:user/xiaoweis/fix_nvbug_6114140
Closed

[https://nvbugs/6114140][fix] avoid indefinite block in disagg gen-side KV transfer#14414
Shixiaowei02 wants to merge 1 commit into
NVIDIA:mainfrom
Shixiaowei02:user/xiaoweis/fix_nvbug_6114140

Conversation

@Shixiaowei02

@Shixiaowei02 Shixiaowei02 commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Description

CacheTransceiver::checkGenTransferStatus used a bare std::future::get() on each receiver future, which blocks forever when the corresponding sender never completes. On gb300 NVL72 the receiver-side disagg KV transfer for ctxtp2_genpp2 has been hitting this on the CI cluster, leaving the PP executor loop stuck inside _recv_disagg_gen_cache → _check_disagg_gen_cache_transfer_status → kv_cache_transceiver.check_gen_transfer_status until the 300 s server-start-timeout fires the client off.

The sender side (CacheTransceiver::checkContextTransferStatus) already deals with this by reading kv_transfer_sender_future_timeout_ms from the config and using future.wait_for(...) so the executor loop can fall back to Python, observe py_kv_transfer_timed_out, and cancel the stuck request. This PR mirrors the same pattern on the gen side.

Changes

  • cpp/tensorrt_llm/batch_manager/cacheTransceiver.cpp — in checkGenTransferStatus, when atLeastRequestNum has a value, read getKvTransferSenderFutureTimeoutMs() and use it as the receiver-side wait_for budget. Timed-out futures are left in mRequesterFutures for the next iteration; only ready or unbounded-blocking futures are completed/erased. Exception paths still mark kDISAGG_TRANS_ERROR and erase.
  • tensorrt_llm/_torch/pyexecutor/py_executor.py_check_disagg_gen_cache_transfer_status now walks active gen requests and, for any whose py_kv_transfer_timed_out is set, calls kv_cache_transceiver.cancel_request(req) and marks DISAGG_TRANS_ERROR — symmetric with the existing logic in _check_disagg_ctx_cache_transfer_status.
  • tests/integration/test_lists/waives.txt — remove the test_disaggregated_ctxtp2_genpp2[TinyLlama-1.1B-Chat-v1.0] SKIP entry that pointed at https://nvbugs/6114140, since the test now runs to completion with the C++ side no longer blocking indefinitely.

Test plan

  • Verified test_disaggregated_ctxtp2_genpp2[TinyLlama-1.1B-Chat-v1.0] passes on gb300 (theia0288, ~114 s) with this fix.
  • Confirmed the test also passes on the unfixed code on gb200 (~98 s) and gb300 (~134 s) on the lyris/theia cluster, so the legacy code path was not the only hang trigger and the new wait_for budget only activates when atLeastRequestNum is set (i.e., the timeout is opt-in via the existing kv_transfer_sender_future_timeout_ms config, default 1000 ms).
  • Awaiting /bot run on full CI for confirmation.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved timeout handling for disaggregated generation transfers to prevent indefinite waits and enable proper request cancellation when timeouts occur.
    • Enhanced error recovery mechanisms for transfer operations that exceed configured time limits.
  • Tests

    • Re-enabled a previously waived disaggregated transfer test to restore coverage.

Review Change Stack

…de KV transfer

CacheTransceiver::checkGenTransferStatus used a bare std::future::get()
to wait on receiver futures, which blocks forever when the
corresponding sender never completes (e.g. UCX path unhealthy on
gb300 NVL72 nodes). The sender-side checkContextTransferStatus already
mirrors this with std::future::wait_for(kvTransferSenderFutureTimeoutMs)
so the executor loop can periodically check py_kv_transfer_timed_out
and cancel a stuck request.

Mirror the same pattern on the gen side:

* cacheTransceiver.cpp: when atLeastRequestNum has a value (i.e. the
  caller did not request unbounded blocking) read
  kvTransferSenderFutureTimeoutMs from the config and use it as the
  receiver-side wait_for budget. Timed-out futures stay in
  mRequesterFutures for the next iteration; only ready or
  caller-permitted-unbounded futures are completed/erased.

* py_executor.py: after the C++ call returns, walk the active
  generation requests and, for any that have crossed
  kv_transfer_timeout_ms, call cancel_request and surface
  DISAGG_TRANS_ERROR -- symmetric with the existing logic in
  _check_disagg_ctx_cache_transfer_status.

Verified that test_disaggregated_ctxtp2_genpp2[TinyLlama-1.1B-Chat-v1.0]
passes on gb300 (theia0288, ~114s) with this fix; also passes the
unfixed paths used by other ctxtp/genpp combinations because the new
timeout only activates when atLeastRequestNum is set. The waive in
waives.txt is removed so CI exercises this path again.

Signed-off-by: Shixiaowei02 <39303645+Shixiaowei02@users.noreply.github.com>
@Shixiaowei02

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR adds bounded-wait timeout handling for disaggregated generation KV-cache transfers. The C++ receiver derives a timeout from config and replaces blocking future.get() with time-bounded wait_for, logging and re-polling on timeout rather than blocking indefinitely. The Python executor adds a cancellation path for timed-out in-progress transfers. A previously waived test is re-enabled.

Changes

Disaggregated Generation KV-Transfer Timeout Handling

Layer / File(s) Summary
C++ generation receiver timeout configuration and wait logic
cpp/tensorrt_llm/batch_manager/cacheTransceiver.cpp
checkGenTransferStatus derives receiverFutureTimeoutMs from config when blockAll is false, then replaces unconditional blocking future.get() with time-bounded wait_for: on ready or expiration it completes the transfer and updates KV bandwidth; on timeout it logs a warning and re-polls later; on error it marks the request as kDISAGG_TRANS_ERROR and removes it from pending futures.
Python executor cancellation for timed-out generation transfers
tensorrt_llm/_torch/pyexecutor/py_executor.py
_check_disagg_gen_cache_transfer_status adds a detection and cancellation loop for generation transfers that have timed out: when py_kv_transfer_timed_out is true and transmission is in progress, it calls kv_cache_transceiver.cancel_request(), clears the transfer start timer, and marks the request as DISAGG_TRANS_ERROR.
Re-enable generation timeout test
tests/integration/test_lists/waives.txt
Removes the waived entry for test_disaggregated_ctxtp2_genpp2, allowing the test to run now that the timeout-based cancellation fix resolves the underlying issue.

Sequence Diagram

sequenceDiagram
    participant PyExecutor
    participant CacheTransceiver
    participant Request as Request Future

    PyExecutor->>CacheTransceiver: checkGenTransferStatus()
    CacheTransceiver->>Request: wait_for(timeout)
    
    alt Timeout expires
        Request-->>CacheTransceiver: timeout
        CacheTransceiver->>CacheTransceiver: log warning
        CacheTransceiver-->>PyExecutor: keep pending for re-poll
        
        PyExecutor->>PyExecutor: detect py_kv_transfer_timed_out
        PyExecutor->>CacheTransceiver: cancel_request(req)
        CacheTransceiver->>Request: mark kDISAGG_TRANS_ERROR
        CacheTransceiver-->>PyExecutor: cancellation done
        PyExecutor->>PyExecutor: mark DISAGG_TRANS_ERROR
        
    else Future ready
        Request-->>CacheTransceiver: ready
        CacheTransceiver->>CacheTransceiver: update KV bandwidth
        CacheTransceiver->>CacheTransceiver: log completion
        CacheTransceiver-->>PyExecutor: request complete
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

The changes span C++ and Python with moderate logic density around timeout configuration, wait_for semantics, and request cancellation coordination. The timeout flow is straightforward but requires understanding the interaction between the receiver-side bounded wait and the executor-side cancellation path.

Possibly related PRs

  • NVIDIA/TensorRT-LLM#13347: Both PRs modify the disaggregated generation KV-transfer lifecycle in the PyExecutor/CacheTransceiver path—this PR adds timeout-based cancellation/cleanup for timed-out gen receivers, while the other rewrites the benchmark fill gate to open only when gen transfer states/transceiver completion indicate no pending receive sessions.

Suggested reviewers

  • pcastonguay
  • brb-nv
  • yingguo-trt
  • StanleySun639
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: avoiding indefinite blocking in disaggregated generation-side KV transfer using timeouts.
Description check ✅ Passed The PR description includes all required sections: description of the issue, changes made, test coverage, and verification on target hardware.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@tensorrt_llm/_torch/pyexecutor/py_executor.py`:
- Around line 3835-3842: The loop handling timed-out generation transfers sets
req.state = LlmRequestState.DISAGG_TRANS_ERROR after
kv_cache_transceiver.cancel_request(req), but
_check_cache_transfer_errors("generation requests") may remove the Python
request from active_requests before the matching C++ future in
CacheTransceiver::mRequesterFutures is reaped; fix by either (A) updating
kv_cache_transceiver.cancel_request to also erase the corresponding future from
mRequesterFutures (ensure CacheTransceiver::cancelRequest removes the requester
future on successful cancel), or (B) change the Python-side flow so cancelled
generation transfers remain pollable until check_gen_transfer_status() has
reaped them (e.g., do not clear py_kv_transfer_start_time or remove the request
from active_requests inside _check_cache_transfer_errors while
is_disagg_generation_transmission_in_progress is true); reference symbols:
is_disagg_generation_transmission_in_progress, py_kv_transfer_timed_out,
kv_cache_transceiver.cancel_request, py_kv_transfer_start_time,
LlmRequestState.DISAGG_TRANS_ERROR, _check_cache_transfer_errors,
check_gen_transfer_status, CacheTransceiver::cancelRequest, mRequesterFutures.
🪄 Autofix (Beta)

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: 3ecc16c3-dcdf-4d6a-b7f4-2654395f8e8b

📥 Commits

Reviewing files that changed from the base of the PR and between 6698825 and 3b05c70.

📒 Files selected for processing (3)
  • cpp/tensorrt_llm/batch_manager/cacheTransceiver.cpp
  • tensorrt_llm/_torch/pyexecutor/py_executor.py
  • tests/integration/test_lists/waives.txt
💤 Files with no reviewable changes (1)
  • tests/integration/test_lists/waives.txt

Comment on lines +3835 to 3842
for req in self.active_requests:
if (req.is_disagg_generation_transmission_in_progress
and req.py_kv_transfer_timed_out):
is_cancelled = self.kv_cache_transceiver.cancel_request(req)
if is_cancelled:
req.py_kv_transfer_start_time = None
req.state = LlmRequestState.DISAGG_TRANS_ERROR
self._check_cache_transfer_errors("generation requests")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't remove timed-out gen requests before the C++ future is reaped.

On a successful timeout cancel, this path flips the request to DISAGG_TRANS_ERROR, and _check_cache_transfer_errors() can drop it from active_requests immediately afterward. But CacheTransceiver::cancelRequest() does not remove the matching entry from mRequesterFutures, so once the Python request is gone the executor may stop polling and leave the cancelled receiver future stranded in C++ state indefinitely. Please either erase the future as part of cancel_request() or keep cancelled gen transfers pollable until check_gen_transfer_status() has reaped them.

🤖 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 `@tensorrt_llm/_torch/pyexecutor/py_executor.py` around lines 3835 - 3842, The
loop handling timed-out generation transfers sets req.state =
LlmRequestState.DISAGG_TRANS_ERROR after
kv_cache_transceiver.cancel_request(req), but
_check_cache_transfer_errors("generation requests") may remove the Python
request from active_requests before the matching C++ future in
CacheTransceiver::mRequesterFutures is reaped; fix by either (A) updating
kv_cache_transceiver.cancel_request to also erase the corresponding future from
mRequesterFutures (ensure CacheTransceiver::cancelRequest removes the requester
future on successful cancel), or (B) change the Python-side flow so cancelled
generation transfers remain pollable until check_gen_transfer_status() has
reaped them (e.g., do not clear py_kv_transfer_start_time or remove the request
from active_requests inside _check_cache_transfer_errors while
is_disagg_generation_transmission_in_progress is true); reference symbols:
is_disagg_generation_transmission_in_progress, py_kv_transfer_timed_out,
kv_cache_transceiver.cancel_request, py_kv_transfer_start_time,
LlmRequestState.DISAGG_TRANS_ERROR, _check_cache_transfer_errors,
check_gen_transfer_status, CacheTransceiver::cancelRequest, mRequesterFutures.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #49700 [ run ] triggered by Bot. Commit: 3b05c70 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #49700 [ run ] completed with state FAILURE. Commit: 3b05c70
/LLM/main/L0_MergeRequest_PR pipeline #39306 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Shixiaowei02
Shixiaowei02 deleted the user/xiaoweis/fix_nvbug_6114140 branch May 21, 2026 13:38
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.

2 participants