[TRTLLM-14736][chore] Split the sampler package into per-feature modules - #16981
Conversation
The per-request-tensor spec-decoding samplers (sanitize_top_k, compute_probs_from_logits, sampling_batch_spec_dec_one_model[_for_rejection]) only depend on the vanilla/flashinfer ops, not on Strategy or LlmRequest, so move them out of sampling_utils into a dedicated ops submodule. Speculative and auto_deploy call sites now import directly from ops (spec_decode / vanilla), decoupling them from sampling_utils. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
Introduce sampler_common.py as the shared base layer (generic helpers, per-request queries reading LlmRequest/SamplingConfig, request->strategy bridge) and rename sampling_utils.py to sampler_strategy.py (the strategy types, resolution and dispatch it already held). The request-query helpers (_unwrap_singleton, int_tensor, add_token, _get_*_beam_width, _request_get_sampling_params, _request_strategy, UtilsSamplingParams) move out of sampler.py into these layers. Import graph stays acyclic and single-directional: sampler -> sampler_common -> sampler_strategy -> ops. Update the package __init__ lazy-forward submodule set accordingly. No behavior change. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
3f4f94d to
1c7f255
Compare
The spec-decoding samplers moved into flashinfer.py (they hard-depend on flashinfer and combine it with vanilla's temperature helpers), removing the separate spec_decode.py. Speculative call sites import from ops.flashinfer / ops.vanilla directly. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
The feature's per-slot store and its whole lifecycle (admission, pre-sample metadata, post-sample recurrence, retirement) move out of TorchSampler into a TopPDecayHandler in a dedicated submodule, mirroring how token_ban.py is organized. The handler owns both the TopPDecayStore buffers and the host-side membership set that gates them, so the two can no longer drift: the set was previously a TorchSampler attribute while the store hung off TorchSampler.Store. TorchSampler keeps a single _top_p_decay attribute and drives it through the four lifecycle hooks. update_after_sample now takes the already-indexed per-slot step tokens instead of the full new_tokens tensor, keeping the decay-is-single-token assumption at the call site. No behavior change. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
The handler was an 847-line class nested inside TorchSampler that never touched the outer instance: it takes plain sizes at construction and owns all of its own state (the finish-reason store, the stop-word rule and past-token buffers, their resize/refresh logic, and the end-id / max-length / stop-word device checks). Promote it to a module-level class in its own submodule, alongside token_ban.py and top_p_decay.py. Only the construction site and the two self-referencing type annotations change; the test monkeypatch targets move from TorchSampler.FinishReasonsHandler to the imported class. Formatting reflows follow from the reduced indentation. No behavior change. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
Extract the log-probs data types (LogProbsState, LogProbsStateList, the device-side LogProbsStore) and the three conversion helpers that never touched the sampler instance: they turn staged host tensors into the per-request result format and take only their arguments, so they become module-level functions. Sizing and the per-step device gather deliberately stay in TorchSampler: _prepare_log_probs grows TOPK_LOGPROBS_SHAPE and resizes the store in place, and _process_logprobs reads the sampler-wide shapes, so both are bound to sampler state rather than to the log-probs data. SimpleTokenLogprobs / TokenLogprobs / cast move with get_logprobs_from_request, their only user. Formatting reflows follow from the reduced indentation. No behavior change. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
sampler_common previously imported sampler_strategy for a single function, which made it look like a layer *under* strategy when it is not. Restructure so the split follows what each piece actually needs: sampler_common now imports nothing from the package and holds everything a module can need without resolving a sampling strategy -- the per-request queries (_request_get_sampling_params and friends), UtilsSamplingParams (the plain scalar view of a request's sampling config, which is the *input* to strategy resolution, not a product of it), the shared DEFAULT_BEAM_IDX / DEFAULT_STEP_IDX constants and FinishReasonsList, plus the existing tensor helpers. sampler_strategy keeps _request_strategy, the one request-facing helper that has to know what a Strategy is, and now reads its inputs from sampler_common. This is what feature modules need: the in-flight penalties (NVIDIA#16485) and beam search (NVIDIA#16620) work both hit circular imports because the per-request helpers lived in sampler.py, and neither needs Strategy -- penalties uses _unwrap_singleton / _get_max_beam_width / DEFAULT_BEAM_IDX and never resolves a strategy. Those now sit in a layer any feature module can import. top_p_decay drops to sampler_common for the request query and takes Fusions straight from ops.vanilla (sampler_strategy only re-exported it), leaving TopPDecayMetadata as its one genuine strategy dependency. No behavior change. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
1c7f255 to
d4b3d45
Compare
|
Hi @lori-ren please help to review, let's merge this PR ASAP. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughTorchSampler functionality is reorganized into dedicated finish-reason, logprob, top-p decay, request-parameter, and sampling-operation modules. Speculative decoding imports move to vanilla and FlashInfer operation modules, with corresponding sampler wiring and test updates. ChangesTorch sampler modularization
Estimated code review effort: 5 (Critical) | ~90+ minutes Possibly related PRs
Suggested labels: 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
🧹 Nitpick comments (4)
tensorrt_llm/_torch/pyexecutor/sampler/finish_reasons.py (1)
219-244: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
@dataclass(kw_only=True)is inert here.
_TemporaryDatadeclares no fields and defines its own__init__, so the decorator contributes nothing (and its generated__init__is shadowed). Either drop the decorator or express the state as dataclass fields withdefault_factoryand letclear()reassign.♻️ Field-based alternative
- `@dataclass`(kw_only=True) class _TemporaryData: """Data structure to store the temporary data during setup_sampler_step for new requests""" def __init__(self) -> 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 `@tensorrt_llm/_torch/pyexecutor/sampler/finish_reasons.py` around lines 219 - 244, Remove the inert `@dataclass`(kw_only=True) decorator from _TemporaryData, since the class defines no dataclass fields and provides its own __init__. Preserve the existing initialization and clear() behavior unchanged.tensorrt_llm/_torch/pyexecutor/sampler/sampler_common.py (2)
110-120: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAvoid tensor allocation to compute a scalar max.
_get_max_beam_widthbuilds atorch.Tensorfrombeam_width_arrayjust to call.max().item(). Since this path is not cached for beam-search requests (_request_sampling_params_cachablereturnsFalsewhenuse_beam_search), and_request_strategyis invoked multiple times per sampling step, this tensor allocation runs repeatedly on the hot path for every beam-search request. Plain Pythonmax()on the list avoids the allocation entirely.⚡ Proposed fix
if sampling_config.beam_width_array is not None: - max_beam_width = max( - max_beam_width, - cast( - int, torch.tensor(sampling_config.beam_width_array, dtype=torch.int32).max().item() - ), - ) + max_beam_width = max(max_beam_width, max(sampling_config.beam_width_array))🤖 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/sampler/sampler_common.py` around lines 110 - 120, Update _get_max_beam_width to compute the maximum of sampling_config.beam_width_array with Python’s built-in max() instead of constructing a torch.Tensor and calling tensor.max().item(), while preserving the existing comparison with beam_width and None handling.Source: Learnings
31-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd
__all__for the public surface.This module exports several public names (
UtilsSamplingParams,int_tensor,add_token,DEFAULT_BEAM_IDX,DEFAULT_STEP_IDX,FinishReasonsList) but has no__all__, unlike the siblingsampler_strategy.pywhich defines one for its public surface.As per coding guidelines: "keep
__all__updated for public interfaces."🤖 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/sampler/sampler_common.py` around lines 31 - 46, Add an __all__ declaration in sampler_common.py listing the module’s public interfaces: UtilsSamplingParams, int_tensor, add_token, DEFAULT_BEAM_IDX, DEFAULT_STEP_IDX, and FinishReasonsList. Keep the list synchronized with these existing exported symbols and follow the pattern used by sampler_strategy.py.Source: Coding guidelines
tensorrt_llm/_torch/pyexecutor/sampler/ops/flashinfer.py (1)
283-303: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the in-place
logitsmutation.
sampling_batch_spec_dec_one_modelmutates the caller'slogitstensor in place viavanilla.safely_apply_temperature_inplace(adiv_). Current callers pass disjoint slices so this is currently safe, but the docstring doesn't disclose the mutation, which is risky for a shared utility with multiple call sites across speculative-decoding backends.📝 Proposed docstring update
"""CUDA-graph compatible sampling; supports mixed sampling params. Returns sampled tokens.""" + # NOTE: mutates `logits` in place (via safely_apply_temperature_inplace). + # Callers must not reuse the same tensor/view afterwards. top_k = sanitize_top_k(top_k, logits.shape[-1])🤖 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/sampler/ops/flashinfer.py` around lines 283 - 303, Update the docstring of sampling_batch_spec_dec_one_model to explicitly state that it mutates the input logits tensor in place during temperature application; leave the sampling behavior and implementation unchanged.
🤖 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/sampler/ops/flashinfer.py`:
- Around line 265-280: Update compute_probs_from_logits so top_k and top_p
always remain tensor inputs during the compiled path, replacing None-based
toggles with the established sentinel or mask representation expected by
compute_probs_from_logits_op. Preserve sanitization and ensure
skip_top_k/skip_top_p changes do not create separate torch.compile
specializations.
In `@tensorrt_llm/_torch/pyexecutor/sampler/sampler.py`:
- Around line 1394-1396: Move the _top_p_decay initialization into the
torch.inference_mode(False) block alongside self.store and the other mutable
sampler state. Keep TopPDecayHandler construction unchanged otherwise, ensuring
setup_sampler_step() operates on regular mutable tensors rather than inference
tensors.
---
Nitpick comments:
In `@tensorrt_llm/_torch/pyexecutor/sampler/finish_reasons.py`:
- Around line 219-244: Remove the inert `@dataclass`(kw_only=True) decorator from
_TemporaryData, since the class defines no dataclass fields and provides its own
__init__. Preserve the existing initialization and clear() behavior unchanged.
In `@tensorrt_llm/_torch/pyexecutor/sampler/ops/flashinfer.py`:
- Around line 283-303: Update the docstring of sampling_batch_spec_dec_one_model
to explicitly state that it mutates the input logits tensor in place during
temperature application; leave the sampling behavior and implementation
unchanged.
In `@tensorrt_llm/_torch/pyexecutor/sampler/sampler_common.py`:
- Around line 110-120: Update _get_max_beam_width to compute the maximum of
sampling_config.beam_width_array with Python’s built-in max() instead of
constructing a torch.Tensor and calling tensor.max().item(), while preserving
the existing comparison with beam_width and None handling.
- Around line 31-46: Add an __all__ declaration in sampler_common.py listing the
module’s public interfaces: UtilsSamplingParams, int_tensor, add_token,
DEFAULT_BEAM_IDX, DEFAULT_STEP_IDX, and FinishReasonsList. Keep the list
synchronized with these existing exported symbols and follow the pattern used by
sampler_strategy.py.
🪄 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: db75bfe6-ad85-42cd-ba2f-bb070ca7d469
📒 Files selected for processing (18)
tensorrt_llm/_torch/auto_deploy/shim/demollm.pytensorrt_llm/_torch/pyexecutor/llm_request.pytensorrt_llm/_torch/pyexecutor/sampler/__init__.pytensorrt_llm/_torch/pyexecutor/sampler/finish_reasons.pytensorrt_llm/_torch/pyexecutor/sampler/logprobs.pytensorrt_llm/_torch/pyexecutor/sampler/ops/flashinfer.pytensorrt_llm/_torch/pyexecutor/sampler/ops/vanilla.pytensorrt_llm/_torch/pyexecutor/sampler/sampler.pytensorrt_llm/_torch/pyexecutor/sampler/sampler_common.pytensorrt_llm/_torch/pyexecutor/sampler/sampler_strategy.pytensorrt_llm/_torch/pyexecutor/sampler/top_p_decay.pytensorrt_llm/_torch/speculative/drafting_loops.pytensorrt_llm/_torch/speculative/dynamic_tree_ops.pytensorrt_llm/_torch/speculative/eagle3_dynamic_tree.pytensorrt_llm/_torch/speculative/interface.pytests/unittest/_torch/sampler/test_beam_search.pytests/unittest/_torch/sampler/test_logits_logprobs.pytests/unittest/_torch/sampler/test_torch_sampler.py
…le comments safely_apply_temperature_inplace had a single caller (sampling_batch_spec_dec_one_model). Inline it there: the call site already computes the same is_greedy predicate the helper recomputed internally, so the expansion reuses it and drops one torch.where. With the helper gone, flashinfer.py needs only GREEDY_TEMPERATURE_THRESHOLD from vanilla, so the module-wide import narrows to that name. Comment cleanups, no code effect: - Five references to sampling_utils, renamed to sampler_strategy earlier in this branch, were left behind in flashinfer.py, vanilla.py, sampler_common.py and sampling_params.py. - The spec-decoding section comment claimed these ops combine flashinfer with 'vanilla's temperature helpers', which is no longer true after the inlining. - The module doc-strings for flashinfer, sampler_common and sampler_strategy argued for their own layering; reduced to describing what each module holds. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
|
/bot run --disable-fail-fast |
|
PR_Github #62448 [ run ] triggered by Bot. Commit: |
|
PR_Github #62448 [ run ] completed with state
|
|
/bot run |
|
PR_Github #62508 [ run ] triggered by Bot. Commit: |
|
PR_Github #62508 [ run ] completed with state |
…y, diversity_rate, early_stopping, VBWS, and CBA performance Rebase of NVIDIA#16620 onto the sampler package restructure (NVIDIA#16981), squashed from its 19 commits because each one re-added the per-request helpers that the restructure relocated. Adaptations to the new layout, no change to the feature itself: - The three new UtilsSamplingParams fields (length_penalty, beam_search_diversity_rate, early_stopping) and their SamplingConfig reads land in sampler_common, where the class now lives, instead of sampling_utils. - beam_search.py slots into the feature layer next to token_ban / top_p_decay / finish_reasons; sampler_strategy imports the beam types from it. - sampler.py takes _BeamHistory* / _gather_beam_path from beam_search, and _unwrap_singleton / _get_beam_width_in from sampler_common. - Log-probs extraction uses logprobs.get_logprobs_from_request (renamed from the TorchSampler method). Verified the resulting tree defines each moved symbol exactly once, that beam_search.py's top-level API is byte-identical to the original branch, and that the UtilsSamplingParams field list matches NVIDIA#16620 exactly. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
Dev Engineer Review
finish_reasons.pyfor finish-reason handling (FinishReasonsHandler).top_p_decay.pyfor Top-P decay lifecycle management (TopPDecayHandler/TopPDecayStore).logprobs.pyfor log-prob state, storage, and tensor↔list conversion helpers.sampler_common.pyfor shared sampler utilities and small request/tensor helpers (UtilsSamplingParams, index defaults, token append helper).sampler_strategy.pyreplacingsampling_utils.pyand introducing request→strategy routing (_request_strategy).sampler/ops/*modules:ops/flashinfer.py(includingsanitize_top_kand speculative one-model helpers with per-request tensors).sampler/ops/vanilla.py.sampler/__init__.py, while removing/redirecting direct imports of the oldsampling_utilsmodule.sampler.pysurface area (per author notes) and re-established an acyclic sampler dependency graph with maximum depth four.tests/integration/test_lists/,test-db/,qa/, orwaives.txt).QA Engineer Review
Modified test code:
tests/unittest/_torch/sampler/test_beam_search.pyconvert_logprobs_tensor_to_listfromtensorrt_llm._torch.pyexecutor.sampler.logprobs.test_beam_search_e2e,test_beam_search_disagg_e2etest_logprobs_trtllm_sampler,test_logprobs_torch_samplertest_create_beam_history,test_finish_beams, etc.).tests/unittest/_torch/sampler/test_logits_logprobs.py_StrategyImplsimport totensorrt_llm._torch.pyexecutor.sampler.sampler_strategy.test_logprobs_simple_format,test_processed_logprobs_e2e, andtest_logprobs_with_grouped_samplings_strategies.tests/unittest/_torch/sampler/test_torch_sampler.pyFinishReasonsHandlermonkeypatch targets to match the new import location.sampler._top_p_decaynew internal API (store,_slots,update_after_sample).TestFinishReasonsmethods (test_are_stop_words_*, stop-word buffer resize tests) andTestTopPDecay.test_runtime_update_parity.Reported validation: sampler and token-ban suites passed (246 passed, 0 failed).
Verdict: needs follow-up (no CBTS/CI test-list coverage metadata provided for the modified test modules).
Description
sampler.pyhad grown to ~5.8k lines, withTorchSampleralone accounting for~3.9k of them. This PR splits it into focused modules and gives the package a
dependency-free base layer. It is a pure refactor: no behavior change.
Resulting structure
sampler_commonlogprobstoken_banops/vanillaops/flashinferops/vanillafinish_reasonssampler_commonsampler_strategysampler_common,ops/*top_p_decaysampler_common,sampler_strategy,ops/vanillasamplerAcyclic, max depth 4.
What moved
finish_reasons.py—FinishReasonsHandlerwas an 847-line class nestedinside
TorchSamplerthat never touched the outer instance: it takes plainsizes at construction and owns all of its own state (the finish-reason store,
the stop-word rule and past-token buffers with their resize/refresh logic, and
the end-id / max-length / stop-word device checks). Promoted to a module-level
class.
top_p_decay.py— the Top-P Decay store and its full lifecycle (admission,pre-sample metadata, post-sample recurrence, retirement), now behind a
TopPDecayHandler. The handler owns both the per-slot buffers and thehost-side membership set that gates them, so the two can no longer drift —
previously the set was a
TorchSamplerattribute while the store hung offTorchSampler.Store, even though the set is the sole gate for reading thestore.
logprobs.py— the log-probs data types (LogProbsState,LogProbsStateList,LogProbsStore) and the three conversion helpers thattook no sampler state, now module-level functions. Log-probs sizing
(
_prepare_log_probs) and the per-step device gather (_process_logprobs)deliberately stay in
TorchSampler: they read and grow the sampler-wide shapes(
TOPK_LOGPROBS_SHAPEand friends), so they are bound to sampler state ratherthan to the log-probs data.
sampler_common.py— the package's base layer. Holds everything a modulecan need without resolving a sampling strategy: the per-request queries
(
_request_get_sampling_paramsand friends),UtilsSamplingParams, the sharedDEFAULT_BEAM_IDX/DEFAULT_STEP_IDXconstants,FinishReasonsList, and thetensor helpers. It imports nothing from the package, so any module may depend
on it.
UtilsSamplingParamslives here rather than insampler_strategyon purpose:it is the plain scalar view of a request's sampling config — the input to
strategy resolution, not a product of it — and several features read it without
ever resolving a strategy.
sampler_strategy.py(renamed fromsampling_utils.py) — strategyresolution, the strategy implementations and the grouped samplers. Keeps
_request_strategy, the one request-facing helper that has to know what aStrategyis, and reads its inputs fromsampler_common.Also in this branch: the spec-decoding sampling ops move out of
sampling_utilsintoops/flashinfer.py(they hard-depend on flashinfer),decoupling the speculative and auto_deploy call sites.
Why the base layer matters beyond this PR
Two in-flight PRs hit circular imports because per-request helpers lived in
sampler.py:for all penalty code in one file under
pyexecutor/sampler/, which wasblocked on exactly this.
length_penalty,diversity_rate,early_stopping).Neither needs
Strategy. The penalty code uses_unwrap_singleton,_get_max_beam_widthandDEFAULT_BEAM_IDXand never resolves a strategy —all three now sit in
sampler_common, so apenalties.pycan join the featurelayer next to
token_ban.pywithout importingsampler.py.sampler.pydrops from 5812 to 4319 lines. External import paths areunaffected — everything still resolves through the package's lazy
__getattr__forwarding, so
pyexecutor.sampler.TorchSampler,.DEFAULT_BEAM_IDXetc. keepworking.
Test Coverage
tests/unittest/_torch/sampler/test_torch_sampler.pyandtest_token_ban.py:246 passed, 0 failed (H200). These cover all three extracted feature areas —
TestFinishReasons(whose monkeypatch targets moved to the promoted class), thetop-p decay runtime-update parity test (whose store/membership access paths
changed), and the token-ban paths.
Test-only updates: monkeypatch targets move from
TorchSampler.FinishReasonsHandlerto the imported class, the top-p decay testreaches the store via the handler, and
test_beam_search.pyimportsconvert_logprobs_tensor_to_listfromlogprobs.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-compatibleorapi-breaking. Forapi-breaking, includeBREAKINGin 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.
GitHub Bot Help
To see a list of available CI bot commands, please comment
/bot help.