Skip to content

[https://nvbugs/6480621][fix] Preserve KV ownership in disaggregated precheck - #17223

Open
chienchunhung wants to merge 5 commits into
NVIDIA:mainfrom
chienchunhung:codex/nvbug-6480621-precheck-ownership
Open

[https://nvbugs/6480621][fix] Preserve KV ownership in disaggregated precheck#17223
chienchunhung wants to merge 5 commits into
NVIDIA:mainfrom
chienchunhung:codex/nvbug-6480621-precheck-ownership

Conversation

@chienchunhung

@chienchunhung chienchunhung commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Prevent Python/NIXL blockAll callers from releasing or deregistering KV pages before transfer quiescence is proven, while still enforcing the request-level transfer deadline.

Impact

kv_transfer_sender_future_timeout_ms is a polling slice, not the request deadline. The old precheck treated one expired slice as completion and recycled source pages while NIXL could still be reading them, which can produce timing-dependent payload mismatches.

Premature page reuse is a serious ownership violation. The demonstrated in-tree impact is limited to the disaggregated perf-sanity precheck and standalone cache-transceiver harness, which run in short-lived test processes before serving. Normal PyExecutor finite status polling is unchanged, and there is no evidence from this path of corrupted user output. An out-of-tree caller of this internal Python blockAll API that recycles storage from its return alone could have the same hazard.

Fix

  • Anchor one absolute sender deadline on the first send, using kv_transfer_timeout_ms; keep the 1-second future timeout only as a polling/cancellation slice.
  • Return nonterminal TIMEOUT at true expiry and retain the session/pages. A terminal result observed at the boundary still wins.
  • Require exact request completion, rank/role consensus, and CUDA quiescence before harness/precheck release.
  • When ownership cannot be proven, persist diagnostics and hard-abort the short-lived process without transceiver/KV-manager finalizers.
  • Preserve the upstream precheck-off default as a true no-op.

Validation

  • Local focused suites: 28 harness tests passed (1 optional case skipped), 50 precheck-run tests passed (11 optional-runtime cases skipped), and 37 config tests passed (4 Slurm/pyzmq orchestration cases deselected).
  • Ruff, Ruff format, YAML, whitespace, conflict, key, and related pre-commit checks passed. Three repository-wide hooks are blocked locally by their Python 3.9 launcher parsing newer repository syntax.
  • Fresh full CI for this SHA and the exact targeted stage on stacked PR [https://nvbugs/6480621][test] Revert to 60-second KV transfer timeout for GB300 DeepSeek V4 Pro disaggregated perf-sanity #17137 are being rerun. The target is GB300-44_GPUs-11_Nodes-PyTorch-Disagg-PerfSanity-CTX3-NODE1-GPU4-GEN1-NODE8-GPU32-Post-Merge-2 with test reuse disabled.
  • Earlier stack evidence (L0 #51530) passed the same stage, but it predates this final deadline/hard-abort revision and is not the final validation claim.

The original 8-CTX, concurrency-1760 NVBug workload remains a separate stress-validation requirement.

Related PRs

@chienchunhung

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63581 [ run ] triggered by Bot. Commit: dc597f9 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63581 [ run ] completed with state FAILURE. Commit: dc597f9
/LLM/main/L0_MergeRequest_PR pipeline #51545 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

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast --stage-list "DGX_H100-PyTorch-4,A30-PyTorch-2,GB300-12_GPUs-3_Nodes-PyTorch-Disagg-PerfSanity-FUNCTIONAL-ONLY-CTX1-NODE1-GPU4-GEN1-NODE2-GPU8-1"

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63819 [ run ] triggered by Bot. Commit: 79ec91f Link to invocation

@chienchunhung
chienchunhung requested a review from nv-xtf August 4, 2026 18:34
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63819 [ run ] completed with state SUCCESS. Commit: 79ec91f
/LLM/main/L0_MergeRequest_PR pipeline #51760 (Partly Tested) completed with status: 'SUCCESS'

CI Report

Link to invocation

@chienchunhung
chienchunhung force-pushed the codex/nvbug-6480621-precheck-ownership branch from c42d28b to f9ca117 Compare August 4, 2026 23:44

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63865 [ run ] triggered by Bot. Commit: f9ca117 Link to invocation

@chienchunhung
chienchunhung marked this pull request as ready for review August 4, 2026 23:57
@chienchunhung
chienchunhung requested review from a team as code owners August 4, 2026 23:57
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change propagates LLM_MODELS_ROOT, adds bounded transfer deadlines, tightens KV-cache manager resolution, limits verification to requested prompt blocks, and delays page release until transfer ownership is confirmed. Tests cover configuration, polling, failures, ownership, cancellation, aborts, and MTP boundaries.

Changes

Cache transceiver precheck

Layer / File(s) Summary
Model-root configuration wiring
jenkins/scripts/perf/..., tests/scripts/perf-sanity/cache_transceiver_precheck/*, tests/unittest/scripts/test_perf_submit.py, tests/unittest/others/test_cache_transceiver_precheck_config.py
The submit scripts extract and forward LLM_MODELS_ROOT. Enabled prechecks require the value and export it with shell quoting. Tests cover parsing, disabled behavior, and shell-safe propagation.
Bounded transfer completion polling
tensorrt_llm/_torch/disaggregation/native/transfer.py, tensorrt_llm/_torch/disaggregation/transceiver.py, tests/unittest/disaggregated/test_transceiver_bounded_polling.py
TxSession.wait_complete uses repeated bounded waits and a shared deadline. It checks task failures, cancellation, auxiliary work, and terminal states. Sender timeouts remain nonterminal.
Precheck validation and resource ownership
tests/scripts/perf-sanity/cache_transceiver_precheck/precheck_config.py, tests/scripts/perf-sanity/cache_transceiver_precheck/run_precheck.py, tests/scripts/perf-sanity/cache_transceiver_precheck/README.md, tests/unittest/others/test_cache_transceiver_precheck_run.py
Automatic manager resolution now fails explicitly when unresolved. Block checks use the requested prompt length. Context and generation pages remain allocated until completion, synchronization, and cross-rank ownership checks succeed.
Example sweep abort and regression coverage
examples/disaggregated/slurm/cache_transceiver_test/*, tests/unittest/disaggregated/test_cache_transceiver_harness.py, tests/unittest/disaggregated/test_cache_transceiver_precheck_e2e.py
The example driver hard-aborts unsafe sweeps and persists remaining-cell status. Isolated harness tests cover ownership flows, and the end-to-end test covers an MTP transfer-block boundary.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

Suggested reviewers: brnguyen2, qijune, chuangz0

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.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
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.
Title check ✅ Passed The title follows the required ticket/type format and clearly summarizes the main KV ownership fix.
Description check ✅ Passed The description clearly explains the issue, fix, impact, validation, and related PRs, with only the explicit checklist confirmation omitted.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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.

🧹 Nitpick comments (2)
tests/unittest/disaggregated/test_transceiver_bounded_polling.py (1)

441-448: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case that fails a sibling between wait slices.

test_tx_session_blocking_wait_detects_failed_sibling_behind_pending_task never reaches the loop body. wait_complete calls has_failed() first, and the ERROR sibling is already present, so the method returns FAILED before any task.wait() call. The assertion pending_task.wait_calls == [] confirms this. The test therefore duplicates test_tx_session_blocking_wait_treats_task_failure_as_terminal instead of covering the in-loop has_failed() check at tensorrt_llm/_torch/disaggregation/native/transfer.py lines 1372-1375.

To cover that check, start with no failed task and flip a sibling to ERROR from inside the first wait() call.

♻️ Proposed test that exercises the in-loop sibling check
 def test_tx_session_blocking_wait_detects_failed_sibling_behind_pending_task() -> None:
     pending_task = _FakeTask(TaskStatus.TRANSFERRING, wait_result=False)
-    failed_task = _FakeTask(TaskStatus.ERROR)
+    sibling = _FakeTask(TaskStatus.TRANSFERRING, wait_result=False)
-    session = _make_tx_session([pending_task, failed_task])
+    session = _make_tx_session([pending_task, sibling])
+    wait = pending_task.wait
+
+    def fail_sibling_during_wait(timeout: Optional[float] = None) -> bool:
+        result = wait(timeout)
+        sibling.status = TaskStatus.ERROR
+        return result
+
+    pending_task.wait = fail_sibling_during_wait
 
     assert session.wait_complete(blocking=True) == WaitResult.FAILED
-    assert pending_task.wait_calls == []
-    assert failed_task.wait_calls == []
+    assert pending_task.wait_calls == [0.25]
+    assert sibling.wait_calls == []
🤖 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_transceiver_bounded_polling.py` around
lines 441 - 448, Update
test_tx_session_blocking_wait_detects_failed_sibling_behind_pending_task so no
task is initially in ERROR; make the first pending task’s wait() transition the
sibling task to ERROR, then assert wait_complete(blocking=True) returns
WaitResult.FAILED and verifies the expected wait calls. This must exercise the
in-loop has_failed() check rather than the initial pre-loop failure check.
tensorrt_llm/_torch/disaggregation/native/transfer.py (1)

1359-1394: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Remove stale WaitResult.TIMEOUT handling. The executor passes atLeastNum with a default of 0; no caller under tensorrt_llm passes None. TxSession and RxSession return only COMPLETED, FAILED, or None, so remove the unreachable TIMEOUT, timed_out, and related consensus plumbing.

🤖 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/disaggregation/native/transfer.py` around lines 1359 -
1394, Remove the obsolete WaitResult.TIMEOUT and timed_out consensus handling
from the blockAll execution path, including any related plumbing in TxSession
and RxSession. Preserve the existing COMPLETED, FAILED, and None outcomes, and
keep the atLeastNum default behavior without adding None handling.
🤖 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 `@tensorrt_llm/_torch/disaggregation/native/transfer.py`:
- Around line 1359-1394: Remove the obsolete WaitResult.TIMEOUT and timed_out
consensus handling from the blockAll execution path, including any related
plumbing in TxSession and RxSession. Preserve the existing COMPLETED, FAILED,
and None outcomes, and keep the atLeastNum default behavior without adding None
handling.

In `@tests/unittest/disaggregated/test_transceiver_bounded_polling.py`:
- Around line 441-448: Update
test_tx_session_blocking_wait_detects_failed_sibling_behind_pending_task so no
task is initially in ERROR; make the first pending task’s wait() transition the
sibling task to ERROR, then assert wait_complete(blocking=True) returns
WaitResult.FAILED and verifies the expected wait calls. This must exercise the
in-loop has_failed() check rather than the initial pre-loop failure check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d9c1baee-87b6-4c9b-aee8-8c23bd2307af

📥 Commits

Reviewing files that changed from the base of the PR and between e409c14 and f9ca117.

📒 Files selected for processing (12)
  • jenkins/scripts/perf/local/submit.py
  • jenkins/scripts/perf/submit.py
  • tensorrt_llm/_torch/disaggregation/native/transfer.py
  • tensorrt_llm/_torch/disaggregation/transceiver.py
  • tests/scripts/perf-sanity/cache_transceiver_precheck/README.md
  • tests/scripts/perf-sanity/cache_transceiver_precheck/precheck_config.py
  • tests/scripts/perf-sanity/cache_transceiver_precheck/run_precheck.py
  • tests/unittest/disaggregated/test_cache_transceiver_precheck_e2e.py
  • tests/unittest/disaggregated/test_transceiver_bounded_polling.py
  • tests/unittest/others/test_cache_transceiver_precheck_config.py
  • tests/unittest/others/test_cache_transceiver_precheck_run.py
  • tests/unittest/scripts/test_perf_submit.py

@BowenFu

BowenFu commented Aug 5, 2026

Copy link
Copy Markdown

Could you keep the blocking path bounded and consistent before merge?

  • Use kv_transfer_timeout_ms as the overall deadline while retaining _timeout_s as the polling interval.
  • Apply the same fix to run_cache_transceiver_test.py, which also waits for all transfers and then frees the sequence.
  • When auxiliary transfer is required but aux_task is absent, do not return from the blocking path as if waiting completed.
  • Remove WaitResult.TIMEOUT and the associated timed_out plumbing if no path can return that value after this change.

Without these changes, a stalled peer can wait forever or pages can still be released before transfer completion.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63865 [ run ] completed with state SUCCESS. Commit: f9ca117
/LLM/main/L0_MergeRequest_PR pipeline #51805 completed with status: 'FAILURE'

CI Report

⚠️ Multi-GPU Label Required:
Multi-GPU tests require the ci: full pre-merge approved label on this PR. Ask a member of NVIDIA/trt-llm-ci-approvers to add the label, then re-trigger CI with the same bot command (no rebase needed).

⚠️ 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

Copy link
Copy Markdown
Collaborator

The blocking wait now has no deadline left, while the receive side still bounds itself. Serving never takes that path, but the harnesses and precheck do, a stalled peer hangs to the stage limit instead of timing out.

for task in self.kv_tasks:
if not task.wait(timeout=self._timeout_s):
return WaitResult.TIMEOUT
while not task.wait(timeout=wait_slice_s):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This turns blocking=True from bounded into unbounded, for every caller — not just the precheck.

Before, if not task.wait(timeout=self._timeout_s): return WaitResult.TIMEOUT gave the caller control back within one slice. Now the only exits are task completion and has_failed(), which is ERROR/CANCELLED only. The sender worker marks the task TRANSFERRING and then waits on NIXL's status with no timeout, so a transfer wedged in PENDING/PROCESSING never calls complete() or fail() — nothing sets a terminal status, and this loop spins until the process dies. The old code degraded to a TIMEOUT the caller could act on; this one has no escape.

transceiver.py:698 is the concrete one: with at_least_request_num=None it used to get TIMEOUT, keep the session, and poll again. I agree the ordinary PyExecutor path polls with 0/1 and is unaffected — but block-all is a supported path, and blocking=True is still this method's default.

The ownership fix itself is right and well argued; it's the removal of the escape hatch that I'd want separated. Keeping a real deadline — the request-level kv_transfer_timeout_ms you correctly distinguish from the polling interval — would give you both: retry across slices so blockAll can't return early, and still fail rather than hang when the peer never quiesces.

elif result == WaitResult.TIMEOUT:
logger.warning(
f"TxSession rid={session.disagg_request_id} timed out after {self._sender_future_timeout_ms}ms"
logger.debug(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Worth checking before you re-word this: after the change above, nothing in tensorrt_llm/ returns WaitResult.TIMEOUT any more. git grep -n 'WaitResult.TIMEOUT' -- tensorrt_llm/ at this head matches only this consumer. RxSession.wait_complete maps its timeout to FAILED, and TxSession.wait_complete no longer produces one on either path.

So this elif is dead, and the new "keeping it in progress" wording describes behaviour that can't be reached — the session is now kept in progress by blocking inside wait_complete, not by returning here. Either drop the branch and the timed_out bookkeeping it feeds, or keep a bounded return alive so it means something again. Reads to me as a symptom of the first point rather than a separate bug.

@nv-xtf nv-xtf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
@chienchunhung
chienchunhung force-pushed the codex/nvbug-6480621-precheck-ownership branch from f9ca117 to f807789 Compare August 7, 2026 02:43
@chienchunhung
chienchunhung requested a review from a team as a code owner August 7, 2026 02:43
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@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: 4

🧹 Nitpick comments (1)
tests/unittest/disaggregated/test_cache_transceiver_harness.py (1)

61-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared driver loader.

_load_driver_ownership_helpers and _load_driver_request_flow differ only in the selected names, the injected stubs, and the module name. The parsing, selection, compile, exec, and missing-name assertion are identical. Duplicating them makes the two name sets drift, which is exactly the failure described in the previous comment.

♻️ Proposed refactor
+def _load_driver_subset(
+    module_name: str,
+    selected_names: set[str],
+    stubs: dict,
+) -> types.ModuleType:
+    """Execute selected driver top-level definitions in an isolated module."""
+    source = Path(DRIVER_SCRIPT).read_text()
+    tree = ast.parse(source, filename=DRIVER_SCRIPT)
+    selected = [
+        node
+        for node in tree.body
+        if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and node.name in selected_names
+    ]
+    module = types.ModuleType(module_name)
+    module.__dict__.update(stubs)
+    exec(
+        compile(ast.Module(body=selected, type_ignores=[]), DRIVER_SCRIPT, "exec"),
+        module.__dict__,
+    )
+    missing = selected_names - set(module.__dict__)
+    assert not missing, f"{module_name}: driver definitions not loaded: {sorted(missing)}"
+    return module

Also applies to: 113-172

🤖 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_harness.py` around lines
61 - 110, Extract the shared AST-based loading logic from
_load_driver_ownership_helpers and _load_driver_request_flow into a reusable
loader helper. Parameterize it with the selected symbols, injected module
globals/stubs, and module name, while preserving each caller’s missing-name
validation and behavior. Update both loaders to delegate to this helper so their
name sets and execution paths cannot drift.
🤖 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/disaggregation/native/transfer.py`:
- Around line 1378-1437: Update the wait_complete blocking path around
wait_slice_s and _deadline_monotonic_s so an explicit None tx_overall_timeout_s
cannot cause an unbounded wait. Use a finite fallback overall deadline (or
validate and reject None) while preserving the existing bounded wait-slice and
terminal-state handling in wait_for_task.

In `@tests/scripts/perf-sanity/cache_transceiver_precheck/precheck_config.py`:
- Around line 196-201: Update the enabled guard in the precheck configuration
flow to reject both None and empty llm_models_root values before inserting the
LLM_MODELS_ROOT export. Preserve the existing ValueError message and valid
non-empty path behavior.

In `@tests/scripts/perf-sanity/cache_transceiver_precheck/run_precheck.py`:
- Around line 389-397: Update the auto-setting path around
model_cls.get_model_defaults to avoid passing None; provide a valid TorchLlmArgs
instance containing the required fields, including tensor_parallel_size, before
invoking the hook. Preserve the existing RuntimeError wrapping for genuine hook
failures.

In `@tests/unittest/disaggregated/test_cache_transceiver_harness.py`:
- Around line 63-76: Add "_Timeout" to the selected_names set used by
cache_transceiver_harness_ownership and add the corresponding _Timeout entry to
its stub dictionary, keeping both ownership helper symbol sets consistent with
_load_driver_request_flow.

---

Nitpick comments:
In `@tests/unittest/disaggregated/test_cache_transceiver_harness.py`:
- Around line 61-110: Extract the shared AST-based loading logic from
_load_driver_ownership_helpers and _load_driver_request_flow into a reusable
loader helper. Parameterize it with the selected symbols, injected module
globals/stubs, and module name, while preserving each caller’s missing-name
validation and behavior. Update both loaders to delegate to this helper so their
name sets and execution paths cannot drift.
🪄 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: 4feb90e5-8309-4154-bc46-b76e62aa9189

📥 Commits

Reviewing files that changed from the base of the PR and between e447b6c and f807789.

📒 Files selected for processing (16)
  • examples/disaggregated/slurm/cache_transceiver_test/README.md
  • examples/disaggregated/slurm/cache_transceiver_test/config.yaml
  • examples/disaggregated/slurm/cache_transceiver_test/run_cache_transceiver_test.py
  • jenkins/scripts/perf/local/submit.py
  • jenkins/scripts/perf/submit.py
  • tensorrt_llm/_torch/disaggregation/native/transfer.py
  • tensorrt_llm/_torch/disaggregation/transceiver.py
  • tests/scripts/perf-sanity/cache_transceiver_precheck/README.md
  • tests/scripts/perf-sanity/cache_transceiver_precheck/precheck_config.py
  • tests/scripts/perf-sanity/cache_transceiver_precheck/run_precheck.py
  • tests/unittest/disaggregated/test_cache_transceiver_harness.py
  • tests/unittest/disaggregated/test_cache_transceiver_precheck_e2e.py
  • tests/unittest/disaggregated/test_transceiver_bounded_polling.py
  • tests/unittest/others/test_cache_transceiver_precheck_config.py
  • tests/unittest/others/test_cache_transceiver_precheck_run.py
  • tests/unittest/scripts/test_perf_submit.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • jenkins/scripts/perf/local/submit.py
  • tests/unittest/scripts/test_perf_submit.py
  • jenkins/scripts/perf/submit.py
  • tests/unittest/disaggregated/test_cache_transceiver_precheck_e2e.py

Comment on lines +1378 to +1437
# ``_timeout_s`` bounds one scheduler wait slice. The separate absolute
# deadline is shared by every KV task and aux; it is never reset by a
# later wait_complete() call.
wait_slice_s = self._timeout_s
if wait_slice_s is None or wait_slice_s <= 0:
wait_slice_s = _FALLBACK_TX_WAIT_SLICE_S

def wait_for_task(task: SendTaskBase) -> WaitResult:
while True:
# A task/session terminal state observed at the deadline
# boundary takes precedence over TIMEOUT.
if self.has_failed():
return WaitResult.FAILED
if task.status == TaskStatus.TRANSFERRED:
return WaitResult.COMPLETED

remaining_s = None
if self._deadline_monotonic_s is not None:
remaining_s = self._deadline_monotonic_s - time.monotonic()
if remaining_s <= 0:
# The worker can publish terminal state between the
# checks above and the clock read. Preserve boundary
# precedence before classifying this as a timeout.
if self.has_failed():
return WaitResult.FAILED
if task.status == TaskStatus.TRANSFERRED:
return WaitResult.COMPLETED
return WaitResult.TIMEOUT
timeout_s = wait_slice_s if remaining_s is None else min(wait_slice_s, remaining_s)
task.wait(timeout=timeout_s)

# A bounded slice keeps cancellation and sibling failure observable.
for task in self.kv_tasks:
if not task.wait(timeout=self._timeout_s):
return WaitResult.TIMEOUT
if task.status == TaskStatus.ERROR:
return WaitResult.FAILED
if self._need_aux and self.aux_task is not None:
if not self.aux_task.wait(timeout=self._timeout_s):
return WaitResult.TIMEOUT
if self.aux_task.status == TaskStatus.ERROR:
result = wait_for_task(task)
if result != WaitResult.COMPLETED:
return result
if self._need_aux:
if self.aux_task is None:
# _finalize_send() installs the aux task synchronously before
# publishing the request to _send_reqs. Once every KV task is
# terminal, a missing required aux task is an invariant error,
# not an asynchronously pending transfer.
with self.lock:
if self._terminal_status not in (
SessionStatus.ERROR,
SessionStatus.CANCELLED,
):
self._exception = RuntimeError(
"required auxiliary transfer was not dispatched"
)
self._terminal_status = SessionStatus.ERROR
return WaitResult.FAILED
return WaitResult.COMPLETED
result = wait_for_task(self.aux_task)
if result != WaitResult.COMPLETED:
return result
return (
WaitResult.FAILED
if self.status in (SessionStatus.ERROR, SessionStatus.CANCELLED)
else WaitResult.COMPLETED
)

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find the default value and validation of kv_transfer_timeout_ms.
rg -nP -C4 'kv_transfer_timeout_ms' --type=py --type=cpp --type=cc
rg -nP -C4 'kv_transfer_timeout_ms' -g '*.h' -g '*.hpp' -g '*.pyi'

Repository: NVIDIA/TensorRT-LLM

Length of output: 188


🏁 Script executed:

#!/bin/bash
set -eu

echo '--- matching files ---'
rg -n --glob '*.py' --glob '*.cpp' --glob '*.cc' --glob '*.h' --glob '*.hpp' \
  'kv_transfer_timeout_ms|tx_overall_timeout_s|_overall_timeout_s|_deadline_monotonic_s' .

echo '--- transfer.py structure and target region ---'
ast-grep outline tensorrt_llm/_torch/disaggregation/native/transfer.py
sed -n '1280,1460p' tensorrt_llm/_torch/disaggregation/native/transfer.py

echo '--- repository configuration references ---'
rg -n -C 5 --glob '*.yaml' --glob '*.yml' --glob '*.json' --glob '*.toml' --glob '*.py' \
  'kv_transfer_timeout_ms|tx_overall_timeout_s' .

Repository: NVIDIA/TensorRT-LLM

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu

echo '--- configuration definitions ---'
sed -n '4190,4275p' tensorrt_llm/llmapi/llm_args.py
sed -n '35,130p' tensorrt_llm/_torch/disaggregation/transceiver.py
sed -n '1200,1285p' tensorrt_llm/_torch/disaggregation/native/transfer.py
sed -n '2290,2370p' tensorrt_llm/_torch/disaggregation/native/transfer.py
sed -n '455,505p' cpp/tensorrt_llm/nanobind/executor/executorConfig.cpp
sed -n '395,425p' cpp/tensorrt_llm/batch_manager/cacheTransceiver.cpp

echo '--- focused bounded-polling tests ---'
sed -n '110,180p' tests/unittest/disaggregated/test_transceiver_bounded_polling.py
sed -n '620,760p' tests/unittest/disaggregated/test_transceiver_bounded_polling.py

echo '--- explicit unset/default construction sites ---'
sed -n '430,465p' tests/unittest/_torch/executor/test_disagg_inflight_cancel_gate.py
sed -n '610,665p' tests/unittest/others/test_kv_cache_transceiver.py

echo '--- read-only static verifier ---'
python3 - <<'PY'
import ast
from pathlib import Path

path = Path("tensorrt_llm/llmapi/llm_args.py")
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
    if isinstance(node, ast.AnnAssign) and getattr(node.target, "id", None) == "kv_transfer_timeout_ms":
        print("kv_transfer_timeout_ms annotation:", ast.unparse(node.annotation))
        print("kv_transfer_timeout_ms value:", ast.unparse(node.value))
        break

path = Path("tensorrt_llm/_torch/disaggregation/transceiver.py")
text = path.read_text()
print("passes None when config value is None:",
      "else None" in text[text.index("transfer_timeout_s"):text.index("transfer_timeout_s") + 300])
print("passes transfer_timeout_s as tx_overall_timeout_s:",
      "tx_overall_timeout_s=transfer_timeout_s" in text)

path = Path("tensorrt_llm/_torch/disaggregation/native/transfer.py")
text = path.read_text()
wait_start = text.index("    def wait_complete(self, blocking: bool = True)")
wait_end = text.index("    def set_exception", wait_start)
wait_body = text[wait_start:wait_end]
print("wait slice fallback exists:", "_FALLBACK_TX_WAIT_SLICE_S" in wait_body)
print("deadline check conditional:", "if self._deadline_monotonic_s is not None:" in wait_body)
print("unbounded branch uses task.wait:", "task.wait(timeout=timeout_s)" in wait_body)
PY

Repository: NVIDIA/TensorRT-LLM

Length of output: 31639


🏁 Script executed:

#!/bin/bash
set -eu

echo '--- current change for the reviewed implementation ---'
git diff -- tensorrt_llm/_torch/disaggregation/native/transfer.py \
  tensorrt_llm/_torch/disaggregation/transceiver.py \
  tensorrt_llm/llmapi/llm_args.py \
  tests/unittest/disaggregated/test_transceiver_bounded_polling.py

echo '--- wait_complete-focused tests ---'
rg -n -C 8 'wait_complete|WaitResult\.TIMEOUT|deadline_monotonic|no overall|overall timeout|unbounded' \
  tests/unittest/disaggregated/test_transceiver_bounded_polling.py \
  tests/unittest -g '*.py'

echo '--- all construction paths for the Python V2 transceiver ---'
rg -n -C 8 'KvCacheTransceiverV2|CacheTransceiverConfig\(' \
  tensorrt_llm/_torch/disaggregation tensorrt_llm/_torch/pyexecutor tests/unittest/disaggregated \
  tests/unittest/_torch/executor -g '*.py'

Repository: NVIDIA/TensorRT-LLM

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu

echo '--- task wait semantics ---'
sed -n '210,280p' tensorrt_llm/_torch/disaggregation/native/transfer.py

echo '--- exact deadline and fallback tests ---'
sed -n '450,625p' tests/unittest/disaggregated/test_transceiver_bounded_polling.py
sed -n '800,885p' tests/unittest/disaggregated/test_transceiver_bounded_polling.py

echo '--- focused diff summary ---'
git diff --stat -- tensorrt_llm/_torch/disaggregation/native/transfer.py \
  tensorrt_llm/_torch/disaggregation/transceiver.py \
  tensorrt_llm/llmapi/llm_args.py \
  tests/unittest/disaggregated/test_transceiver_bounded_polling.py
git diff -U12 -- tensorrt_llm/_torch/disaggregation/native/transfer.py | sed -n '1,260p'

Repository: NVIDIA/TensorRT-LLM

Length of output: 10984


Bound wait_complete(blocking=True) when kv_transfer_timeout_ms=None.

CacheTransceiverConfig defaults to 60000, but callers can explicitly set None, and KvCacheTransceiverV2 forwards it as tx_overall_timeout_s=None. A stalled peer can then block the caller indefinitely. Add a finite fallback deadline or reject None for this path.

🤖 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/disaggregation/native/transfer.py` around lines 1378 -
1437, Update the wait_complete blocking path around wait_slice_s and
_deadline_monotonic_s so an explicit None tx_overall_timeout_s cannot cause an
unbounded wait. Use a finite fallback overall deadline (or validate and reject
None) while preserving the existing bounded wait-slice and terminal-state
handling in wait_for_task.

Comment on lines +196 to +201
if enabled:
if llm_models_root is None:
raise ValueError("enabled cache-transceiver precheck requires LLM_MODELS_ROOT")
# Keep this as a top-level assignment. shlex.quote() is not safe when
# nested inside the double-quoted pytestCommand exports below.
lines.insert(0, f"export LLM_MODELS_ROOT={shlex.quote(llm_models_root)}")

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject an empty llm_models_root too.

The guard only rejects None. An empty string passes and produces export LLM_MODELS_ROOT=''. The precheck then resolves no model directory and silently falls back to FALLBACK_KV_SHAPE, which defeats the fail-fast intent of this check.

🛡️ Proposed fix
     if enabled:
-        if llm_models_root is None:
+        if not llm_models_root:
             raise ValueError("enabled cache-transceiver precheck requires LLM_MODELS_ROOT")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if enabled:
if llm_models_root is None:
raise ValueError("enabled cache-transceiver precheck requires LLM_MODELS_ROOT")
# Keep this as a top-level assignment. shlex.quote() is not safe when
# nested inside the double-quoted pytestCommand exports below.
lines.insert(0, f"export LLM_MODELS_ROOT={shlex.quote(llm_models_root)}")
if enabled:
if not llm_models_root:
raise ValueError("enabled cache-transceiver precheck requires LLM_MODELS_ROOT")
# Keep this as a top-level assignment. shlex.quote() is not safe when
# nested inside the double-quoted pytestCommand exports below.
lines.insert(0, f"export LLM_MODELS_ROOT={shlex.quote(llm_models_root)}")
🤖 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/scripts/perf-sanity/cache_transceiver_precheck/precheck_config.py`
around lines 196 - 201, Update the enabled guard in the precheck configuration
flow to reject both None and empty llm_models_root values before inserting the
LLM_MODELS_ROOT export. Preserve the existing ValueError message and valid
non-empty path behavior.

Comment on lines 389 to +397
if setting == "auto":
defaults = {}
if model_cls is not None:
if hasattr(model_cls, "get_model_defaults"):
try:
defaults = model_cls.get_model_defaults(None) or {}
except Exception as e: # noqa: BLE001 - model hooks may need llm_args
print(
f"[precheck] WARNING: get_model_defaults failed ({e!r}); assuming V1",
flush=True,
)
except Exception as e: # noqa: BLE001 - model hooks are third-party extension points
raise RuntimeError(
f"get_model_defaults failed for {model_cls.__name__}; refusing to assume V1"
) from e

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect every get_model_defaults override for llm_args usage.
ast-grep run --pattern $'def get_model_defaults($$$) {
  $$$
}' --lang python tensorrt_llm 2>/dev/null
rg -nP -A15 'def get_model_defaults\s*\(' --type=py tensorrt_llm

Repository: NVIDIA/TensorRT-LLM

Length of output: 14053


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- precheck call site ---'
sed -n '350,420p' tests/scripts/perf-sanity/cache_transceiver_precheck/run_precheck.py

printf '%s\n' '--- all definitions and references ---'
rg -n -P 'def get_model_defaults\s*\(|get_model_defaults\s*\(' tensorrt_llm tests/scripts/perf-sanity/cache_transceiver_precheck --type py

printf '%s\n' '--- registered model resolution and Whisper registration ---'
rg -n -P 'Whisper|MODEL|model_cls|ModelLoader|AutoModel|model_type' tensorrt_llm/_torch tests/scripts/perf-sanity/cache_transceiver_precheck --type py | head -300

printf '%s\n' '--- static analysis of every definition ---'
python3 - <<'PY'
import ast
from pathlib import Path

root = Path("tensorrt_llm")
for path in sorted(root.rglob("*.py")):
    try:
        tree = ast.parse(path.read_text())
    except (OSError, SyntaxError):
        continue
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == "get_model_defaults":
            args = [a.arg for a in node.args.args]
            llm_arg = args[-1] if args else None
            uses = []
            for child in ast.walk(node):
                if child is node:
                    continue
                if isinstance(child, ast.Attribute) and isinstance(child.value, ast.Name) and child.value.id == llm_arg:
                    uses.append(f"attribute:{child.attr}")
                elif isinstance(child, ast.Subscript) and isinstance(child.value, ast.Name) and child.value.id == llm_arg:
                    uses.append("subscript")
                elif isinstance(child, ast.Call) and isinstance(child.func, ast.Name) and child.func.id == llm_arg:
                    uses.append("call")
            print(f"{path}:{node.lineno} arg={llm_arg!r} uses={sorted(set(uses))}")
PY

Repository: NVIDIA/TensorRT-LLM

Length of output: 42694


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Whisper class registration and mapping ---'
sed -n '820,875p' tensorrt_llm/_torch/models/modeling_whisper.py
sed -n '835,865p' tensorrt_llm/_torch/models/modeling_utils.py
rg -n -P 'register_model|MODEL_CLASS_MAPPING|WhisperForConditionalGeneration' tensorrt_llm/_torch/models/modeling_whisper.py tensorrt_llm/_torch/models/__init__.py tensorrt_llm/_torch/models/modeling_utils.py

printf '%s\n' '--- model loader argument construction ---'
sed -n '400,440p' tensorrt_llm/_torch/pyexecutor/model_loader.py
rg -n -P 'class TorchLlmArgs|TorchLlmArgs\(' tensorrt_llm/_torch --type py | head -80

Repository: NVIDIA/TensorRT-LLM

Length of output: 6184


Do not call get_model_defaults() with None.

The registered WhisperForConditionalGeneration override reads llm_args.tensor_parallel_size. This raises AttributeError and aborts the precheck when Whisper uses "auto". Pass a valid TorchLlmArgs or make the hook accept None.

🤖 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/scripts/perf-sanity/cache_transceiver_precheck/run_precheck.py` around
lines 389 - 397, Update the auto-setting path around
model_cls.get_model_defaults to avoid passing None; provide a valid TorchLlmArgs
instance containing the required fields, including tensor_parallel_size, before
invoking the hook. Preserve the existing RuntimeError wrapping for genuine hook
failures.

Comment on lines +63 to +76
selected_names = {
"_TransferError",
"_FatalTransferError",
"_request_ids",
"_context_completion_error",
"_gen_completion_error",
"_can_release_sequence",
"_release_sequence_if_safe",
"_validate_context_completion",
"_validate_python_gen_completion",
"_first_reason",
"_exchange_release_decision",
"_hard_abort_process",
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add _Timeout to the ownership helper name set.

_exchange_release_decision contains except _Timeout: raise. _Timeout is neither in selected_names nor in the stub dict at lines 85-101, so the name is undefined in cache_transceiver_harness_ownership. The current tests pass because no exception is raised inside that try. If the handshake body ever raises, Python evaluates the except clause and fails with NameError instead of the intended handling, which hides the real failure.

_load_driver_request_flow already selects _Timeout; keep the two sets consistent.

🛠️ Proposed fix
     selected_names = {
+        "_Timeout",
         "_TransferError",
         "_FatalTransferError",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
selected_names = {
"_TransferError",
"_FatalTransferError",
"_request_ids",
"_context_completion_error",
"_gen_completion_error",
"_can_release_sequence",
"_release_sequence_if_safe",
"_validate_context_completion",
"_validate_python_gen_completion",
"_first_reason",
"_exchange_release_decision",
"_hard_abort_process",
}
selected_names = {
"_Timeout",
"_TransferError",
"_FatalTransferError",
"_request_ids",
"_context_completion_error",
"_gen_completion_error",
"_can_release_sequence",
"_release_sequence_if_safe",
"_validate_context_completion",
"_validate_python_gen_completion",
"_first_reason",
"_exchange_release_decision",
"_hard_abort_process",
}
🤖 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_harness.py` around lines
63 - 76, Add "_Timeout" to the selected_names set used by
cache_transceiver_harness_ownership and add the corresponding _Timeout entry to
its stub dictionary, keeping both ownership helper symbol sets consistent with
_load_driver_request_flow.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64468 [ run ] triggered by Bot. Commit: f807789 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64468 [ run ] completed with state FAILURE. Commit: f807789
/LLM/main/L0_MergeRequest_PR pipeline #52340 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

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.

5 participants