🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

[None][fix] Drain guided-decoder host functions before GIL-holding forward calls - #19455

Merged
brnguyen2 merged 1 commit into
NVIDIA:mainfrom
brnguyen2:upstream/guided-hostfunc-drain
Sep 22, 2026
Merged

brnguyen2 merged 1 commit into
NVIDIA:mainfrom
brnguyen2:upstream/guided-hostfunc-drain

Conversation

@brnguyen2

@brnguyen2 brnguyen2 commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Dev Engineer Review

  • tensorrt_llm/_torch/pyexecutor/guided_decoder.py: CapturableGuidedDecoder now waits on bitmask_event as the last step of execute, execute_draft_batch and rollback_rejected_batch, so the CUDA host functions that build the bitmask have finished before control returns to the caller. The wait is skipped while a CUDA graph is being captured. Event.synchronize releases the GIL, which is what lets the pending host-function callbacks run.
  • tensorrt_llm/_torch/speculative/interface.py: the SpecWorkerBase guided-decoder helper routes through the draining entry points, including the skip-drafting path.
  • Behavior change: host-side ordering only. The bitmask consumer was already ordered against the same event on the device, so decode results are unchanged. Fixes a rank-wide deadlock (host function waiting for the GIL while a GIL-holding native forward triggers a context-synchronizing lazy kernel load) that CUDA-graph warmup with a one-engine drafter hit deterministically.

QA Engineer Review

  • New CPU-only unit test tests/unittest/_torch/speculative/hw_agnostic/test_guided_hostfunc_drain.py (198 lines): asserts each enqueue site drains as its last step, that the drain is skipped under CUDA-graph capture and for the non-capturable decoder, and that the SpecWorkerBase helper still executes the decoder on both the drafting and skip-drafting paths.
  • No GPU test added: the deadlock needs a multi-rank one-engine drafter with a cold kernel cache, which the L0 unit stages cannot reproduce. Validated manually on the 4-rank warmup that originally wedged.

Description

CapturableGuidedDecoder builds its bitmask from CUDA host functions, and each is a callback into Python that must take the GIL. A one-engine drafter (e.g. DFlash) runs a draft forward right after guided execution, and that forward enters native extension calls that hold the GIL for their whole duration (flash_attn_with_kvcache); when such a call triggers CUDA lazy kernel-module loading, the driver performs a context-synchronizing operation. A guided-decoder host function still pending at that point deadlocks the rank: the device is parked at the host-function node, the callback waits for the GIL, and the GIL holder waits for the device. Peer ranks then spin in NCCL, wedging the whole server with no error and no further log line. CUDA-graph warmup hits this deterministically at the first warmup shape whose kernel instantiation is not yet loaded (gdb evidence: pending cudaHostFuncTrampoline threads on 3/4 ranks, main threads in cudaFuncSetAttribute -> cuKernelGetFunction).

The existing set_stance("force_eager") mitigation (pytorch#163061) covers only torch.compile kernels, not a plain extension call.

Fix: drain at the enqueue sites. CapturableGuidedDecoder waits on bitmask_event (recorded on the side stream after every host function) as the last step of execute, execute_draft_batch and rollback_rejected_batch, skipped while a CUDA graph is being captured (host waits are illegal there, and captured host nodes do not run until replay). Event.synchronize releases the GIL, which is exactly what lets the pending callbacks run. Host-side ordering only; the bitmask consumer is already ordered against the same event on the device, so results are unchanged, and no caller discipline is required.

Test Coverage

  • tests/unittest/_torch/speculative/hw_agnostic/test_guided_hostfunc_drain.py (CPU-only): asserts each enqueue site drains as its last step, that the drain is skipped under CUDA-graph capture and for the non-capturable decoder, and that the SpecWorkerBase helper still executes the decoder (including the skip-drafting path).

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: NVIDIA/TensorRT-LLM/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 27bce9ae-ea26-4dec-a421-a7c23f8712bd

📥 Commits

Reviewing files that changed from the base of the PR and between 8961297 and c119c9e.

📒 Files selected for processing (2)
  • tensorrt_llm/_torch/pyexecutor/guided_decoder.py
  • tensorrt_llm/_torch/speculative/interface.py

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


Walkthrough

The change drains pending guided-decoder host callbacks outside CUDA graph capture. It applies this behavior to execution paths and routes skip_drafting through the shared guided-decoder helper. GPU-independent tests cover synchronization order, capture behavior, and worker delegation.

Changes

Guided decoder host-function draining

Layer / File(s) Summary
Decoder host-function drain
tensorrt_llm/_torch/pyexecutor/guided_decoder.py, tests/unittest/_torch/speculative/hw_agnostic/test_guided_hostfunc_drain.py
CapturableGuidedDecoder synchronizes pending host callbacks after bitmask synchronization when CUDA graph capture is inactive. The tests verify ordering, capture behavior, and non-capturable decoder behavior.
Speculative helper routing
tensorrt_llm/_torch/speculative/interface.py, tests/unittest/_torch/speculative/hw_agnostic/test_guided_hostfunc_drain.py
skip_drafting uses _execute_guided_decoder_if_present. Tests verify guided-decoder delegation, no-decoder behavior, and sampled output preservation.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: juney-nvidia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 3 files. 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 [None][fix] format and clearly describes draining guided-decoder host functions before GIL-holding forward calls.
Description check ✅ Passed The description explains the deadlock cause, the implementation, the capture condition, and the relevant CPU-only tests. It also includes the requested checklist and marks the review complete.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

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

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74641 [ run ] triggered by Bot. Commit: 1f0a1d4 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74641 [ run ] completed with state SUCCESS. Commit: 1f0a1d4
/LLM/main/L0_MergeRequest_PR pipeline #61427 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

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74655 [ run ] triggered by Bot. Commit: 1f0a1d4 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74655 [ run ] completed with state SUCCESS. Commit: 1f0a1d4
/LLM/main/L0_MergeRequest_PR pipeline #61441 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

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74668 [ run ] triggered by Bot. Commit: 1f0a1d4 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74668 [ run ] completed with state SUCCESS. Commit: 1f0a1d4
/LLM/main/L0_MergeRequest_PR pipeline #61454 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

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

Note on the test waive in this PR

CI here is blocked by a pre-existing failure on main that is unrelated to this change (unrelated failure (unittest/usage/test_llmapi_config_telemetry_docs.py::test_build_capture_manifest_matches_committed_golden) unaddressed on main for 10h (merge-base age)):

  • unittest/usage/test_llmapi_config_telemetry_docs.py::test_build_capture_manifest_matches_committed_golden — tracked in https://nvbugs/6811958

The test(s) are waived in this PR so its CI can proceed. The same waive is being landed on main via #19462. Once main carries the waive, the line here becomes redundant and is dropped automatically (on the next rebase at the latest).

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74678 [ run ] triggered by Bot. Commit: 8961297 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

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

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

…rward calls

CapturableGuidedDecoder builds its bitmask from CUDA host functions, and
each is a callback into Python that must take the GIL. A one-engine
drafter (e.g. DFlash) runs a draft forward right after guided execution,
and that forward enters native extension calls that hold the GIL for
their whole duration (flash_attn_with_kvcache); when such a call triggers
CUDA lazy kernel-module loading, the driver performs a
context-synchronizing operation. A guided-decoder host function still
pending at that point deadlocks the rank: the device is parked at the
host-function node, the callback waits for the GIL, and the GIL holder
waits for the device. Peer ranks then spin in NCCL, wedging the whole
server with no error and no further log line. CUDA-graph warmup hits this
deterministically at the first warmup shape whose kernel instantiation is
not yet loaded (gdb evidence: pending cudaHostFuncTrampoline threads on
3/4 ranks, main threads in cudaFuncSetAttribute -> cuKernelGetFunction).

The existing set_stance("force_eager") mitigation (pytorch#163061) covers
only torch.compile kernels, not a plain extension call.

Drain at the enqueue sites: CapturableGuidedDecoder waits on
bitmask_event (recorded on the side stream after every host function) as
the last step of execute, execute_draft_batch and
rollback_rejected_batch, skipped while a CUDA graph is being captured
(host waits are illegal there, and captured host nodes do not run until
replay). Event.synchronize releases the GIL, which is exactly what lets
the pending callbacks run. Host-side ordering only; the bitmask consumer
is already ordered against the same event on the device, so results are
unchanged. No caller discipline required.

Adds CPU-only unit tests
(tests/unittest/_torch/speculative/hw_agnostic/test_guided_hostfunc_drain.py)
asserting each enqueue site drains as its last step, that the drain is skipped under
capture or for the non-capturable decoder, and that the worker helper still executes the decoder.

Signed-off-by: Brian Nguyen <brnguyen@nvidia.com>
@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run

@brnguyen2
brnguyen2 force-pushed the upstream/guided-hostfunc-drain branch from 8961297 to c119c9e Compare September 21, 2026 01:46
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74701 [ run ] triggered by Bot. Commit: c119c9e Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74702 [ run ] triggered by Bot. Commit: c119c9e Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74701 [ run ] completed with state ABORTED. Commit: c119c9e

Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

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

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74757 [ run ] triggered by Bot. Commit: c119c9e Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #74757 [ run ] completed with state SUCCESS. Commit: c119c9e
/LLM/main/L0_MergeRequest_PR pipeline #61535 completed with status: 'SUCCESS'

CI Report

Link to invocation

@asfiyab-nvidia

Copy link
Copy Markdown
Contributor

@brnguyen2 The dev/engineer QA review sections in the description indicate that this is a formatting change with no associated tests. Can you update the description to accurately reflect the change?

@asfiyab-nvidia

asfiyab-nvidia commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Was making the drain unconditional (rather than being conditional on one-engine drafter + GIL-holding native forward) a deliberate "always-safe, some throughput cost" tradeoff, or should it be opt-in?

@brnguyen2

Copy link
Copy Markdown
Collaborator Author

Updated. The summary block at the top was CodeRabbit's auto-generated text from a stale pass that only saw the import reflow, so it described this as formatting-only. It now states the actual change (the drain at the three enqueue sites, skipped under graph capture) and the test coverage (the CPU-only unit test in test_guided_hostfunc_drain.py). The Description and Test Coverage sections below it were already accurate.

@github-actions

Copy link
Copy Markdown

Automatically added "ci: full pre-merge approved" because this PR has satisfied the required GitHub review approvals. Unresolved review conversations and other required checks remain independent merge requirements.

@brnguyen2

brnguyen2 commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Was making the drain unconditional (rather than being conditional on one-engine drafter + GIL-holding native forward) a deliberate "always-safe, some throughput cost" tradeoff, or should it be opt-in?

Deliberate. The drain is a host wait on an event the bitmask consumer already orders against on the device, so it adds no device work, and the only new cost is the CPU blocking until the host functions that were already enqueued finish. The unsafe condition is "a pending host function plus any GIL-holding call that can trigger a context sync", and the second half is not something the guided decoder can see. Gating on the drafter type would fix the case I hit and leave the same wedge for the next extension call that loads a kernel lazily.

Keeping it unconditional also keeps the enqueue sites free of a mode branch: one rule (drain last, except under capture) is easier to read and to keep correct than a predicate over drafter type and attention backend that has to be revisited whenever either changes.

If the wait shows up in profiles I would rather make the skip opt-in than the safety, but I have not measured a difference on the guided-decoding paths so far.

@brnguyen2
brnguyen2 merged commit 8a7239a into NVIDIA:main Sep 22, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants