[https://nvbugs/6442074][fix] Make one-model spec-dec attn-metadata save/restore exception-safe#16325
[https://nvbugs/6442074][fix] Make one-model spec-dec attn-metadata save/restore exception-safe#16325trtllm-agent wants to merge 3 commits into
Conversation
📝 WalkthroughWalkthroughEagle3, MTP, and PARD draft execution now restores speculative-decoding attention metadata when failures occur. The related H100 integration waiver is removed. ChangesSpeculative decoding metadata restoration
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tensorrt_llm/_torch/speculative/eagle3.py (1)
757-783: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winRestore all mutated attention state in
finally.Line 771 restores saved tensors, but Lines 774-776 and 783 run only on success. If the draft loop raises after changing
all_rank_num_tokensoruse_spec_decoding, the worker remains in draft state. Capture the original value before thetryand move both restorations intofinally.Proposed fix
+ original_all_rank_num_tokens = attn_metadata.all_rank_num_tokens try: ... - original_all_rank_num_tokens = attn_metadata.all_rank_num_tokens ... finally: self._restore_attn_metadata_from_spec_dec(attn_metadata) - if original_all_rank_num_tokens is not None: - attn_metadata.all_rank_num_tokens = original_all_rank_num_tokens + if original_all_rank_num_tokens is not None: + attn_metadata.all_rank_num_tokens = original_all_rank_num_tokens + attn_metadata.use_spec_decoding = True ... - attn_metadata.use_spec_decoding = True🤖 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/speculative/eagle3.py` around lines 757 - 783, Move capture of the original all_rank_num_tokens value before the try block, then update the finally block in the surrounding draft-token flow to restore both attn_metadata.all_rank_num_tokens and attn_metadata.use_spec_decoding on every exit, including exceptions. Remove the success-only restoration and ensure use_spec_decoding is restored to its pre-loop value rather than unconditionally set afterward.
🤖 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/speculative/mtp.py`:
- Around line 440-448: Move the attn_metadata update in the MTP forward flow
into the existing try/finally block so change_attn_metadata and its internal
_prepare_attn_metadata_for_spec_dec updates are protected. Ensure the finally
cleanup always clears _saved_tensors when either metadata preparation or the
draft loop raises, while preserving the current update behavior when no
exception occurs.
In `@tensorrt_llm/_torch/speculative/pard.py`:
- Around line 243-339: Move the deferred _apply_kv_rewind_after_draft call into
the existing finally block so it runs after _restore_attn_metadata_from_spec_dec
whether draft execution succeeds or raises. Track whether
_prepare_kv_for_draft_forward completed successfully if needed, and apply the
rewind only when KV preparation incremented the metadata; remove the
post-finally call while preserving restore-before-rewind ordering.
---
Outside diff comments:
In `@tensorrt_llm/_torch/speculative/eagle3.py`:
- Around line 757-783: Move capture of the original all_rank_num_tokens value
before the try block, then update the finally block in the surrounding
draft-token flow to restore both attn_metadata.all_rank_num_tokens and
attn_metadata.use_spec_decoding on every exit, including exceptions. Remove the
success-only restoration and ensure use_spec_decoding is restored to its
pre-loop value rather than unconditionally set afterward.
🪄 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: 4d120b8c-9036-406f-a7ca-9ea57204f518
📒 Files selected for processing (4)
tensorrt_llm/_torch/speculative/eagle3.pytensorrt_llm/_torch/speculative/mtp.pytensorrt_llm/_torch/speculative/pard.pytests/integration/test_lists/waives.txt
💤 Files with no reviewable changes (1)
- tests/integration/test_lists/waives.txt
f598b6a to
28ed40b
Compare
Eagle3OneModelWorker.forward…|
Re the outside-diff review comment on
Unlike |
|
/bot run --disable-fail-fast |
|
PR_Github #59053 [ run ] triggered by Bot. Commit: |
|
PR_Github #59053 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #59116 [ run ] triggered by Bot. Commit: |
… save/restore exception-safe TestGPTOSS::test_eagle3_vswa_reuse_4gpus[v2_kv_cache-one_model] fails on H100 with "AssertionError: Sampling failed". The real chain: the final general warmup (max-shape memory-pool prepop) OOMs on H100-80GB and is tolerated by model_engine, but the aborted forward had already run Eagle3OneModelWorker prepare of the attn metadata without try/finally, so the restore never ran. Every subsequent forward then trips the bare "assert len(self._saved_tensors) == 0" in prepare_for_spec_dec, whose empty str() masks the error, cascading into "Sampling failed". - eagle3.py: wrap the draft path in try/finally with the prepare INSIDE the try (the Eagle3 prepare override allocates clones after the base save, so a failure inside prepare must also reach the restore). - interface.py: add messages to the bare asserts so a recurrence names the unrestored fields instead of logging an empty error. - waives.txt: unwaive the test on H100. Verified on 4x H100-80GB (viking-prod-233) at main f81b9d2: pristine TOT reproduces the exact QA failure; with this fix the same warmup OOM occurs but serving survives - GSM8K 89.92 (ref 90.30), GPQADiamond 64.14 (ref 65.0), test PASSED. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: dongfengy <99041270+dongfengy@users.noreply.github.com>
…ion safety to MTP, PARD, DFlash and DraftTarget workers The one-model workers in mtp.py, pard.py, dflash.py and draft_target.py have the same latent bug fixed in eagle3.py: prepare/restore of the attention metadata is not exception-safe, so any tolerated failure in the draft path (e.g. a warmup OOM) poisons attn_metadata._saved_tensors and every later forward dies at the pairing assert. - mtp.py: move change_attn_metadata (which saves state and then mutates more of it) inside the try; restore in finally. - pard.py: prepare and the deferred kv_lens rewind are now covered; the rewind runs in the finally after the restore so a draft failure does not leave kv_lens_cuda incremented for the next request. - dflash.py / draft_target.py: same try/finally treatment. MTPEagleWorker subclasses Eagle3OneModelWorker and is already covered by the eagle3.py fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: dongfengy <99041270+dongfengy@users.noreply.github.com>
…up in SpecWorkerBase Replace the per-worker try/finally blocks with a template method on SpecWorkerBase: forward() extracts attn/spec metadata, runs the worker-specific _forward_impl(), and in a finally block restores any unrestored prepare_for_spec_dec state (plus the pending kv_lens rewind for PARD/DFlash). __init_subclass__ rejects subclasses that override forward directly, so a future worker cannot reintroduce the leak. - speculative/interface.py: template forward + _ensure_spec_dec_state_restored. - attention_backend/interface.py: has_spec_dec_saved_state property. - eagle3/mtp/pard/dflash/draft_target/sa_worker/eagle3_dynamic_tree: forward renamed to _forward_impl; bodies return to straight-line prepare/restore (per-worker try/finally removed). - pard/dflash: _kv_rewind_pending flag so a failed draft forward still rewinds kv_lens_cuda exactly once; consumed by _apply_kv_rewind_after_draft. - eagle3: initialize the worker-side _saved_* attrs so a failure inside the prepare override cannot turn the cleanup restore into AttributeError. - test_force_accepted_tokens.py: stub worker implements _forward_impl. Verified on 4x H100-80GB (viking-prod-233) at main f81b9d2, A/B: - workers unguarded, no template method: FAILED in 215s with the nvbugs/6442074 signature (warmup OOM then stale-save assert). - template method only: the base finally visibly restores state when the max-shape warmup OOMs, and the test PASSES - GSM8K 90.75 (ref 90.30), GPQADiamond 66.16 (ref 65.0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: dongfengy <99041270+dongfengy@users.noreply.github.com> Signed-off-by: trtllm-agent <296075020+trtllm-agent@users.noreply.github.com>
adc97c0 to
43f79e1
Compare
|
/bot kill |
|
/bot run --disable-fail-fast |
|
/bot kill |
|
Closing in favor of #16381, which carries the identical (content-verified) commits on a developer-owned branch. The repair-bot automation that owns this branch periodically force-rebases it, which unfortunately invalidates in-flight CI runs and re-triggers reviewer assignment, so continuing here would keep racing the automation. Review history in this PR remains valid: both inline CodeRabbit findings were addressed structurally and the threads resolved; see #16381 for the current CI. Thanks repair-bot for the initial draft and the base-freshness diligence! |
|
PR_Github #59241 [ ] completed with state |
|
PR_Github #59240 [ ] completed with state |
|
PR_Github #59116 [ run ] completed with state |
Summary
f81b9d2787on 4x H100-80GB): the final general warmup — the max-shape memory-pool pre-population that runs right after CUDA-graph capture — hits atorch.OutOfMemoryErroron H100-80GB, whichmodel_engine._general_warmup_impldeliberately catches and skips. That aborted forward had already runEagle3OneModelWorker's_prepare_attn_metadata_for_spec_decwith no try/finally, so the restore never ran andattn_metadata._saved_tensorswas left populated on the persistent metadata object. Every subsequent forward then trips the bareassert len(self._saved_tensors) == 0inAttentionMetadata.prepare_for_spec_dec; a bareAssertionErrorhas an emptystr(), so the log showsEncountered an error in forward function:with no message, the executor swallows it,sample_statebecomesNone, and the loop dies with the secondaryAssertionError: Sampling failed(plus a tertiarythreads can only be started onceduring restart) — the signature QA reported. Not a regression: thev2_kv_cacheparametrization was added 2026-07-10 by [None][test] KV cache manager v2: add V2 + VSWA multi-GPU test coverage #16114 and this was its first QA run; KVCacheManagerV2 shifts available-token accounting enough to push the max-shape warmup into OOM on 80GB where V1 stays under.eagle3.py: wrap the draft path in try/finally with the prepare inside the try — the Eagle3 prepare override allocates clones after the base save populates_saved_tensors, so a failure inside prepare itself must also reach the restore (restore is safe on empty/partial state). CoversMTPEagleWorkervia inheritance.mtp.py:change_attn_metadata(which saves state and then mutates more of it) moved inside the try; restore in finally.pard.py: stale_kv_rewind_amountcleared up front; the deferred kv_lens rewind runs in the finally after the restore, so a draft failure cannot leavekv_lens_cudaincremented.dflash.py: same try/finally; akv_preparedflag gates the deferred rewind so a failure before_prepare_kv_for_draft_forwardcannot apply a stale rewind.draft_target.py: same try/finally.attention_backend/interface.py: messages added to the bare asserts so a recurrence names the unrestored fields instead of logging an empty error.waives.txt: unwaive the test on H100.Verification
A/B on 4x H100-80GB (dlcluster viking-prod-233, dgxh100 — same node class as the QA failure), both runs at main
f81b9d2787:OOM during general warmup with 2048 tokens, 2048 generation tokens. Skipping.followed by all 4 ranks asserting on the first real batch. FAILED in 563s.89.92(ref 90.30), GPQADiamond64.14(ref 65.0). PASSED in 628s.Follow-up in this PR: structural guarantee (template method)
Per review feedback that per-worker try/finally is not future-proof, the third commit centralizes the invariant in
SpecWorkerBase:forward()is now a base-class template method: it runs the worker-specific_forward_impl()and restores any unrestoredprepare_for_spec_decstate in afinally(with a warning log), plus the pending kv_lens rewind for PARD/DFlash.__init_subclass__rejects subclasses that overrideforwarddirectly, so a future worker cannot reintroduce the leak.forward->_forward_implrenames vs pre-fix code).finallyvisibly restores state when the max-shape warmup OOMs and the test PASSES (GSM8K 90.75/90.22, GPQADiamond 66.16/63.13 across two runs).test_force_accepted_tokens.pyunit tests: 21 passed.Test plan
tests/integration/defs/accuracy/test_llm_api_pytorch.py::TestGPTOSS::test_eagle3_vswa_reuse_4gpus[v2_kv_cache-one_model]PASSED (unwaived by this PR)Links
This PR originally carried an automated repair-bot fix; its head was replaced by the verified fix above (root cause corrected: the trigger is the tolerated warmup OOM, not a transient serving failure; prepare moved inside the try; sibling workers covered).
Summary by CodeRabbit
Bug Fixes
Tests