[TRTLLM-15017][chore] Unify one-model speculative decoding samplers into SpecSampler - #17292
[TRTLLM-15017][chore] Unify one-model speculative decoding samplers into SpecSampler#17292zhaoyangwang-nvidia wants to merge 3 commits into
Conversation
…nto SpecSampler One-model speculative decoding had five sampler classes: SpecSamplerBase plus MTPSampler, Eagle3OneModelSampler, DraftTargetOneModelSampler and SASampler. None of them overrode sample_async, update_requests, validate_request or Store; they differed only in how they sized four buffers, via three hooks (_get_max_tokens, _get_max_new_tokens, _get_draft_tokens_storage_size). Those sizes all derive from TorchSampler.Args alone. args.max_total_draft_tokens is set to spec_config.tokens_per_gen_step - 1, which equals the draft length each mode computed for itself -- including PARD, where it is 2K-1 rather than the tree size. The per-mode subclasses therefore carried no information args did not already have. - Fold the four subclasses into SpecSampler; the constructor takes only sampler_args. - Drop the three sizing hooks and _add_dummy_draft_tokens, which was never overridden and is now inlined. - Collapse get_spec_decoder from seven mode branches to two: two-model modes (EAGLE3 / MTP_EAGLE) keep TorchSampler, every one-model mode gets SpecSampler. - Keep the old class names as aliases, so __all__ and the AutoDeploy call site stay source-compatible. new_tokens is deliberately left at the wire width (max_total_draft_tokens + 1). Its true bound is the accepted-path depth, max_draft_len + 1, which would save memory for static trees and PARD, but narrowing it depends on every worker packing accepted tokens at the front of its output; sample_async truncates to the store width. That is tracked separately. Verified against tests/unittest/_torch/speculative/hw_agnostic/ (178 tests covering MTP, PARD, SA, DraftTarget, DFlash, DSpark, dynamic draft length and tree sampling/verification): no test regressed. AutoDeploy speculative decoding smoke tests pass. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughChangesSpeculative sampler unification
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 |
4089cca to
2057598
Compare
|
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. |
The previous commit kept MTPSampler, Eagle3OneModelSampler, DraftTargetOneModelSampler, SASampler and SpecSamplerBase as aliases for SpecSampler. Nothing needs them: - After the unification no code in the tree calls any of them; get_spec_decoder and the AutoDeploy shim both construct SpecSampler directly. - They live under tensorrt_llm/_torch, a private package, and none of them are covered by tests/unittest/api_stability. - Eagle3OneModelSampler and DraftTargetOneModelSampler were never even exported from tensorrt_llm._torch.speculative -- only reachable via the module path. Keeping them would leave five names a future reader cannot tell are dead, which is the same kind of vestigial indirection this series set out to remove. Also drops what the removal made dead: the SpecSampler imports the aliases required, the typing/typing_extensions `override` import (its only users were the deleted MTPSampler methods) along with the now-empty version check and the `import sys` that served it, and the SampleStateMTP alias, which was only ever MTPSampler.SampleState. Five comments and docstrings that described behavior in terms of the removed class names are updated. __all__ in tensorrt_llm/_torch/speculative now exports SpecSampler only. Re-verified with the same suites as the previous commit: 178 speculative tests plus the AutoDeploy speculative smoke tests, no regression. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
2057598 to
4a752cd
Compare
|
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. |
6061eab to
4a752cd
Compare
…ength SpecSampler sized new_tokens to the wire width, max_total_draft_tokens + 1, but that buffer only holds what a step actually accepts: the golden token the target always accepts, plus the draft tokens that matched. That is max_draft_len + 1 -- the drafter advances max_draft_len times -- which is smaller than the wire width whenever a mode carries more draft slots than it can accept: Eagle3 dynamic tree (K=6, topK=10) 61 -> 7 MTP dynamic tree (K=1, topK=4) 5 -> 2 PARD (K=6, T=2K-1) 12 -> 7 The six linear modes are unaffected: max_total_draft_tokens == max_draft_len there, so the two expressions already agree. Three of the four over-allocations predate this series; only Eagle3 dynamic tree used to be tight, via the Eagle3OneModelSampler override the first commit dropped along with the subclass. The deprecated eagle_choices static tree is the exception and keeps the wire width. Its one-model drafter never builds the tree: _forward_draft_loop is a linear loop over runtime_draft_len, which for a non-linear tree is max_total_draft_tokens, so a step can accept up to max_total_draft_tokens + 1 tokens while max_draft_len only describes a tree depth nothing constructs. Tree-aware acceptance exists solely in the two-model TorchSampler path. Both that path and eagle_choices are slated for removal in release 1.4, and this workaround goes with them; get_spec_decoder carries the override so the special case is visible at the one place that knows the config. update_requests asserts the bound, so a mode that ever exceeds it fails with a clear message instead of an opaque IndexError from add_token indexing a plain host-side list. That assertion is what caught the static tree case: the unit tests do not cover eagle_choices, and it only surfaced under a benchmark run. Verified on tests/unittest/_torch/speculative plus the AutoDeploy speculative smoke tests, and with trtllm-bench at batch size 8 on Eagle3 dynamic tree, MTP dynamic tree, PARD, SA and the static tree -- no assertion fired in any of them. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
Description
One-model speculative decoding had five sampler classes:
SpecSamplerBaseplusMTPSampler,Eagle3OneModelSampler,DraftTargetOneModelSamplerandSASampler. None of them overrodesample_async,update_requests,validate_requestorStore— they differed only in how they sized four buffers, via three hooks (_get_max_tokens,_get_max_new_tokens,_get_draft_tokens_storage_size).Those sizes all derive from
TorchSampler.Argsalone.args.max_total_draft_tokensis set tospec_config.tokens_per_gen_step - 1, which equals the draft length each mode computed for itself — including PARD, where it is2K-1rather than the tree size. The per-mode subclasses therefore carried no informationargsdid not already have.Accepted-token buffer (third commit)
new_tokensholds only what a step accepts -- the golden token the target alwaysaccepts, plus the drafts that matched -- so it is sized to
max_draft_len + 1rather than the wire width
max_total_draft_tokens + 1. Modes that carry moredraft slots than they can accept shrink; the six linear modes are unchanged
because
max_total_draft_tokens == max_draft_lenthere:Three of those over-allocations predate this series. Only Eagle3 dynamic tree was
tight, via the
Eagle3OneModelSampleroverride the first commit dropped with thesubclass, so this generalizes that bound instead of reinstating a special case.
Static tree workaround. The deprecated
eagle_choicesstatic tree keeps thewire width. Its one-model drafter never builds the tree:
_forward_draft_loopisa linear loop over
runtime_draft_len, which for a non-linear tree ismax_total_draft_tokens, so a step can accept up tomax_total_draft_tokens + 1tokens while
max_draft_lenonly describes a tree depth nothing constructs.Tree-aware acceptance exists solely in the two-model
TorchSamplerpath. Boththat path and
eagle_choicesare slated for removal in release 1.4, and thisworkaround goes with them.
get_spec_decodercarries the override so the specialcase sits at the one place that knows the config.
update_requestsasserts the bound, so a mode that ever exceeds it fails with aclear message instead of an opaque
IndexErrorfromadd_tokenindexing a plainhost-side list.
Test Coverage
tests/unittest/_torch/speculative/— 251 passed, 25 skipped, covering MTP, PARD, SA, DraftTarget, DFlash, DSpark, dynamic draft length, and tree sampling/verification. Compared against a pre-change baseline: no test regressed, and the new assertion never fired.test_deepseek_eagle3,test_deepseek_mla_eagle3) fail identically with and without these changes on an sm90-only build, since CUTLASS FP4 GEMM requires sm100. Confirmed by re-running them on the pre-change tree.tests/unittest/auto_deploy/singlegpu/smoke/test_ad_speculative_decoding.py— 5 passed, exercising the one external consumer of the renamed sampler.Baseline showed one failure,
test_advanced_sampling_mode.py::test_no_topk_matches_full[0.9], which is pre-existing and flaky (3 failures in 8 consecutive runs on H200) with no call path to the changed code.PR Checklist
[JIRA][type] descriptionformat🤖 Generated with Claude Code
Dev Engineer Review
SpecSampler.TorchSampler.Args.max_draft_len + 1.get_spec_decoderto useTorchSamplerfor two-model modes andSpecSamplerfor one-model modes.QA Engineer Review
tests/unittest/auto_deploy/singlegpu/smoke/test_ad_speculative_decoding.py.