Skip to content

Commit 78f0aed

Browse files
[TRTLLM-13212][refactor] Make FlashInfer a hard dependency for Torch sampler and spec
Convert the Torch sampler and one-model speculative decoding from "FlashInfer-optional with torch fallback" to a hard FlashInfer dependency. The failure is raised at sampler / speculative-worker construction time (resolve_sampling_backend, SpecWorkerBase) via a new require_flashinfer helper and a sentinel-based bind_flashinfer_sampling, so sampling_utils stays importable for torch-only consumers (AutoDeploy demollm, drafting greedy helpers). - Add require_flashinfer() in flashinfer_utils.py; IS_FLASHINFER_AVAILABLE semantics are unchanged (its ~46 legitimate fallback consumers are not touched). - ops/flashinfer.py: drop the import guard, bind flashinfer.sampling at construction via a sentinel; compiled bodies do no import work. - Remove the SimpleGroupedStrategySampler torch backend and the disable_flashinfer_sampling LlmArg + its plumbing; update the api_stability and usage golden files and the sampling/telemetry docs. - compute_probs_from_logits becomes FlashInfer-only (drop the CUDA C++ op and CPU vanilla branches). - speculative/interface.py: drop the availability guards, keep the flashinfer>=0.6.4 version gate and its version-gated branches. - Delete now-dead fallback kernels (forward_native_sampling, _apply_top_k_top_p, _random_sample, vanilla.compute_probs_from_logits_op); keep beam-search/greedy/rejection/_Fusions and the torch sample() path used by rejection-sampling draft probs and demollm. - Prune torch-vs-flashinfer parametrized sampler tests. Signed-off-by: ZhaoyangWang <zhaoyangw@nvidia.com>
1 parent 06df8da commit 78f0aed

17 files changed

Lines changed: 186 additions & 368 deletions

File tree

docs/source/developer-guide/telemetry.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ unset or when the safety sanitizer rejects the runtime value.
6363
| `cuda_graph_config.mode` | `Literal['decode']` | `categorical` | | `decode`, `encode` |
6464
| `cuda_graph_config.num_tokens` | `Optional[List[Annotated[int, Gt(gt=0)]]]` | `value` | | |
6565
| `cuda_graph_config.seq_lens` | `Optional[List[Annotated[int, Gt(gt=0)]]]` | `value` | | |
66-
| `disable_flashinfer_sampling` | `<class 'bool'>` | `value` | | |
6766
| `disable_overlap_scheduler` | `<class 'bool'>` | `value` | | |
6867
| `dtype` | `<class 'str'>` | `categorical` | allowlist | `auto`, `float16`, `bfloat16`, `float32` |
6968
| `dwdp_config.contention_opt` | `<class 'bool'>` | `value` | | |

docs/source/features/sampling.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,12 @@ llm.generate(["Hello, my name is",
104104
### Performance
105105

106106
The Torch Sampler leverages the optimized sampling kernels provided by
107-
[FlashInfer](https://docs.flashinfer.ai/api/sampling.html). The sampler
108-
also uses the [sorting-free implementations](https://flashinfer.ai/2025/03/10/sampling.html)
107+
[FlashInfer](https://docs.flashinfer.ai/api/sampling.html), which is a required
108+
dependency for the Torch Sampler. The sampler also uses the
109+
[sorting-free implementations](https://flashinfer.ai/2025/03/10/sampling.html)
109110
whenever possible. This optimization does not compute the complete set of token sampling probabilities
110111
(after top-k / top-p masking etc.), which typically can be omitted unless requested by the user or
111112
required for speculative decoding (rejection sampling).
112-
In case of unexpected problems, the use of FlashInfer in Torch Sampler can
113-
be disabled via the `disable_flashinfer_sampling` config option (note that this option is likely
114-
to be removed in a future TensorRT LLM release).
115113

116114
Moreover, Torch Sampler internally batches requests with compatible sampling parameters. This
117115
can greatly reduce the overall latency of the sampling step when request batches are comprised

tensorrt_llm/_torch/pyexecutor/_util.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2343,7 +2343,6 @@ def create_torch_sampler_args(
23432343
speculative_config: SpeculativeConfig,
23442344
max_beam_width: int,
23452345
disable_overlap_scheduler: bool,
2346-
disable_flashinfer_sampling: bool,
23472346
enable_async_worker: bool,
23482347
enable_speculative_beam_history_d2h: bool,
23492348
):
@@ -2359,7 +2358,6 @@ def create_torch_sampler_args(
23592358
max_total_draft_tokens=max_total_draft_tokens,
23602359
max_num_sequences=max_num_sequences,
23612360
max_beam_width=max_beam_width,
2362-
disable_flashinfer_sampling=disable_flashinfer_sampling,
23632361
disable_overlap_scheduler=disable_overlap_scheduler,
23642362
enable_async_worker=enable_async_worker,
23652363
enable_speculative_beam_history_d2h=enable_speculative_beam_history_d2h,
@@ -2378,7 +2376,6 @@ def instantiate_sampler(
23782376
speculative_config: SpeculativeConfig,
23792377
decoding_config: trtllm.DecodingConfig,
23802378
kv_cache_config: KvCacheConfig,
2381-
disable_flashinfer_sampling: bool,
23822379
):
23832380
enable_async_worker = (confidential_compute_enabled()
23842381
or llm_args.sampler_force_async_worker)
@@ -2390,7 +2387,6 @@ def instantiate_sampler(
23902387
speculative_config=speculative_config,
23912388
max_beam_width=max_beam_width,
23922389
disable_overlap_scheduler=llm_args.disable_overlap_scheduler,
2393-
disable_flashinfer_sampling=disable_flashinfer_sampling,
23942390
enable_async_worker=enable_async_worker,
23952391
enable_speculative_beam_history_d2h=llm_args.
23962392
enable_speculative_beam_history_d2h,

tensorrt_llm/_torch/pyexecutor/py_executor_creator.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,6 @@ def drafting_loop_wrapper(model):
763763
speculative_config=spec_config,
764764
decoding_config=decoding_config,
765765
kv_cache_config=kv_cache_config,
766-
disable_flashinfer_sampling=llm_args.disable_flashinfer_sampling,
767766
)
768767
logger.info(f"Using Sampler: {type(sampler).__name__}")
769768

tensorrt_llm/_torch/pyexecutor/sampler/ops/flashinfer.py

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,19 @@
1515
"""FlashInfer-accelerated sampling kernels.
1616
1717
Pure kernel functions with no dependency on the sampling_utils interface
18-
or other backend implementation modules. All flashinfer imports are guarded by
19-
IS_FLASHINFER_AVAILABLE. Beam search is excluded (torch-only per design).
18+
or other backend implementation modules. Beam search is excluded (torch-only
19+
per design).
20+
21+
flashinfer is a hard dependency for these ops. The module stays importable
22+
without flashinfer (so ``sampling_utils`` and its torch-only ``greedy`` helper
23+
remain importable for AutoDeploy / drafting consumers), but the actual
24+
``flashinfer.sampling`` module is bound at sampler / speculative-worker
25+
construction time via ``bind_flashinfer_sampling`` -- which raises a clear
26+
``ImportError`` when flashinfer is absent. Calling these ops before binding
27+
raises a descriptive ``RuntimeError``.
2028
"""
2129

22-
from typing import Optional
30+
from typing import Any, NoReturn, Optional
2331

2432
import torch
2533

@@ -29,14 +37,45 @@
2937
import flashinfer.sampling
3038

3139

40+
class _UnboundFlashInferSampling:
41+
"""Sentinel used until ``bind_flashinfer_sampling`` binds the real module."""
42+
43+
def __getattr__(self, name: str) -> NoReturn:
44+
raise RuntimeError(
45+
"FlashInfer sampling ops were called before flashinfer was bound. "
46+
"Construct the Torch sampler (resolve_sampling_backend) or the "
47+
"speculative worker before invoking these ops."
48+
)
49+
50+
51+
# Bound to ``flashinfer.sampling`` by ``bind_flashinfer_sampling`` at construction.
52+
_flashinfer_sampling: Any = _UnboundFlashInferSampling()
53+
54+
55+
def bind_flashinfer_sampling(feature_name: str = "Torch sampler") -> None:
56+
"""Bind the real ``flashinfer.sampling`` module, requiring flashinfer.
57+
58+
Idempotent-friendly: re-binding to the same module is harmless. Raises
59+
``ImportError`` when flashinfer is unavailable.
60+
"""
61+
if not IS_FLASHINFER_AVAILABLE:
62+
raise ImportError(
63+
f"{feature_name} requires flashinfer, but flashinfer is not "
64+
"available on this platform. flashinfer is a hard dependency here; "
65+
"please install the version pinned in requirements.txt."
66+
)
67+
global _flashinfer_sampling
68+
_flashinfer_sampling = flashinfer.sampling
69+
70+
3271
def top_k_top_p_sampling_from_logits_op(
3372
logits: torch.Tensor,
3473
top_k: torch.Tensor,
3574
top_p: torch.Tensor,
3675
seed: Optional[int] = None,
3776
offset: Optional[int] = None,
3877
) -> torch.Tensor:
39-
tokens: torch.Tensor = flashinfer.sampling.top_k_top_p_sampling_from_logits(
78+
tokens: torch.Tensor = _flashinfer_sampling.top_k_top_p_sampling_from_logits(
4079
logits, top_k, top_p, seed=seed, offset=offset
4180
)
4281
return tokens
@@ -47,7 +86,7 @@ def sampling_from_probs_op(
4786
seed: Optional[torch.Tensor] = None,
4887
offset: Optional[torch.Tensor] = None,
4988
) -> torch.Tensor:
50-
tokens: torch.Tensor = flashinfer.sampling.sampling_from_probs(
89+
tokens: torch.Tensor = _flashinfer_sampling.sampling_from_probs(
5190
probs, deterministic=True, seed=seed, offset=offset
5291
)
5392
return tokens
@@ -57,7 +96,7 @@ def softmax_op(
5796
logits: torch.Tensor,
5897
temperature: Optional[torch.Tensor],
5998
) -> torch.Tensor:
60-
probs: torch.Tensor = flashinfer.sampling.softmax(
99+
probs: torch.Tensor = _flashinfer_sampling.softmax(
61100
logits, temperature, enable_pdl=get_env_enable_pdl()
62101
)
63102
return probs
@@ -67,15 +106,15 @@ def top_k_mask_logits_op(
67106
logits: torch.Tensor,
68107
top_k: torch.Tensor,
69108
) -> torch.Tensor:
70-
masked: torch.Tensor = flashinfer.sampling.top_k_mask_logits(logits, top_k)
109+
masked: torch.Tensor = _flashinfer_sampling.top_k_mask_logits(logits, top_k)
71110
return masked
72111

73112

74113
def top_p_renorm_probs_op(
75114
probs: torch.Tensor,
76115
top_p: torch.Tensor,
77116
) -> torch.Tensor:
78-
renormed: torch.Tensor = flashinfer.sampling.top_p_renorm_probs(probs, top_p)
117+
renormed: torch.Tensor = _flashinfer_sampling.top_p_renorm_probs(probs, top_p)
79118
return renormed
80119

81120

@@ -84,7 +123,7 @@ def sampling_from_probs_generator_op(
84123
generator: Optional[torch.Generator],
85124
check_nan: bool = False,
86125
) -> torch.Tensor:
87-
tokens: torch.Tensor = flashinfer.sampling.sampling_from_probs(
126+
tokens: torch.Tensor = _flashinfer_sampling.sampling_from_probs(
88127
probs, deterministic=True, generator=generator, check_nan=check_nan
89128
)
90129
return tokens
@@ -97,7 +136,7 @@ def top_k_top_p_sampling_from_logits_with_generator_op(
97136
generator: Optional[torch.Generator],
98137
check_nan: bool = False,
99138
) -> torch.Tensor:
100-
tokens: torch.Tensor = flashinfer.sampling.top_k_top_p_sampling_from_logits(
139+
tokens: torch.Tensor = _flashinfer_sampling.top_k_top_p_sampling_from_logits(
101140
logits,
102141
top_k=top_k,
103142
top_p=top_p,
@@ -115,7 +154,7 @@ def top_k_sampling_from_probs_generator_op(
115154
generator: Optional[torch.Generator],
116155
check_nan: bool = False,
117156
) -> torch.Tensor:
118-
tokens: torch.Tensor = flashinfer.sampling.top_k_sampling_from_probs(
157+
tokens: torch.Tensor = _flashinfer_sampling.top_k_sampling_from_probs(
119158
probs,
120159
top_k=top_k,
121160
deterministic=True,
@@ -131,7 +170,7 @@ def top_p_sampling_from_probs_generator_op(
131170
generator: Optional[torch.Generator],
132171
check_nan: bool = False,
133172
) -> torch.Tensor:
134-
tokens: torch.Tensor = flashinfer.sampling.top_p_sampling_from_probs(
173+
tokens: torch.Tensor = _flashinfer_sampling.top_p_sampling_from_probs(
135174
probs,
136175
top_p=top_p,
137176
deterministic=True,
@@ -154,10 +193,10 @@ def compute_probs_from_logits_op(
154193
optional top-k masking (via fused flashinfer softmax+temp).
155194
"""
156195
if top_k is not None:
157-
logits = flashinfer.sampling.top_k_mask_logits(logits, top_k)
158-
probs: torch.Tensor = flashinfer.sampling.softmax(
196+
logits = _flashinfer_sampling.top_k_mask_logits(logits, top_k)
197+
probs: torch.Tensor = _flashinfer_sampling.softmax(
159198
logits, temperatures, enable_pdl=get_env_enable_pdl()
160199
)
161200
if top_p is not None:
162-
probs = flashinfer.sampling.top_p_renorm_probs(probs, top_p)
201+
probs = _flashinfer_sampling.top_p_renorm_probs(probs, top_p)
163202
return probs

tensorrt_llm/_torch/pyexecutor/sampler/ops/interface.py

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,30 @@
1414

1515
"""Sampling strategy-grouping backend selection.
1616
17-
Holds ``SamplerConfig`` and the ``resolve_sampling_backend`` factory that picks
18-
the strategy-grouping sampler (FlashInfer vs simple/torch) once at init time, so
19-
no per-call dispatch happens inside CUDA graph capture. The three grouping
20-
callables are tightly coupled (they share the same key type), so the whole
21-
sampler class is bound as a unit rather than field by field.
17+
Holds the ``resolve_sampling_backend`` factory that binds the strategy-grouping
18+
sampler once at init time, so no per-call dispatch happens inside CUDA graph
19+
capture. flashinfer is a hard dependency for the Torch sampler: the factory
20+
raises at sampler construction time if flashinfer is unavailable.
2221
"""
2322

2423
from collections.abc import Hashable
25-
from dataclasses import dataclass
2624
from typing import Type, cast
2725

28-
from tensorrt_llm._torch.flashinfer_utils import IS_FLASHINFER_AVAILABLE
26+
from tensorrt_llm._torch.pyexecutor.sampler.ops.flashinfer import bind_flashinfer_sampling
2927
from tensorrt_llm._torch.pyexecutor.sampler.sampling_utils import (
3028
FlashInferGroupedStrategySampler,
3129
GroupedStrategySampler,
32-
SimpleGroupedStrategySampler,
3330
)
3431

3532

36-
@dataclass(frozen=True, kw_only=True)
37-
class SamplerConfig:
38-
"""Configuration used to resolve the sampling backend at init time."""
33+
def resolve_sampling_backend() -> Type[GroupedStrategySampler[Hashable]]:
34+
"""Bind the strategy-grouping sampler class at init time; CUDA-graph safe.
3935
40-
use_flashinfer: bool = False
41-
42-
43-
def resolve_sampling_backend(
44-
is_cuda: bool,
45-
config: SamplerConfig,
46-
) -> Type[GroupedStrategySampler[Hashable]]:
47-
"""Pick the strategy-grouping sampler class at init time; CUDA-graph safe.
48-
49-
Selection order:
50-
1. FlashInfer — is_cuda AND IS_FLASHINFER_AVAILABLE AND config.use_flashinfer
51-
2. Torch — everything else (including CPU)
36+
The Torch sampler hard-depends on flashinfer, so this binds the flashinfer
37+
sampling module (raising a clear error at construction time when flashinfer
38+
is unavailable) rather than silently falling back to a torch implementation.
5239
"""
53-
# The two samplers use different key types (Strategy vs a narrower FlashInfer
54-
# key); the caller only forwards the grouping callables, so erase to Hashable.
55-
if is_cuda and IS_FLASHINFER_AVAILABLE and config.use_flashinfer:
56-
return cast(Type[GroupedStrategySampler[Hashable]], FlashInferGroupedStrategySampler)
57-
return cast(Type[GroupedStrategySampler[Hashable]], SimpleGroupedStrategySampler)
40+
bind_flashinfer_sampling("Torch sampler")
41+
# FlashInferGroupedStrategySampler uses a narrower FlashInfer key type; the
42+
# caller only forwards the grouping callables, so erase to Hashable.
43+
return cast(Type[GroupedStrategySampler[Hashable]], FlashInferGroupedStrategySampler)

tensorrt_llm/_torch/pyexecutor/sampler/ops/vanilla.py

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -353,71 +353,6 @@ def _safely_apply_temperature_inplace(
353353
return logits_inout.div_(safe_temp.unsqueeze(dim=1))
354354

355355

356-
def _apply_top_k_top_p(
357-
logits: torch.Tensor,
358-
k: Optional[torch.Tensor],
359-
p: Optional[torch.Tensor],
360-
) -> torch.Tensor:
361-
logits_sort, logits_idx = logits.sort(dim=-1, descending=False)
362-
if k is not None:
363-
top_k_mask = logits_sort.size(1) - k.to(torch.long)
364-
top_k_mask = top_k_mask.clamp(min=0)
365-
top_k_mask = logits_sort.gather(1, top_k_mask.unsqueeze(dim=1))
366-
top_k_mask = logits_sort < top_k_mask
367-
logits_sort.masked_fill_(top_k_mask, -float("inf"))
368-
if p is not None:
369-
probs_sort = logits_sort.softmax(dim=-1)
370-
probs_sum = torch.cumsum(probs_sort, dim=-1, out=probs_sort)
371-
top_p_mask = probs_sum <= 1 - p.unsqueeze(dim=1)
372-
top_p_mask[:, -1] = False
373-
logits_sort.masked_fill_(top_p_mask, -float("inf"))
374-
return logits_sort.scatter(dim=-1, index=logits_idx, src=logits_sort)
375-
376-
377-
def _random_sample(probs: torch.Tensor) -> torch.Tensor:
378-
q = torch.empty_like(probs).exponential_()
379-
return probs.div_(q).argmax(dim=-1).view(-1)
380-
381-
382-
def forward_native_sampling(
383-
logits: torch.Tensor,
384-
k: Optional[torch.Tensor],
385-
p: Optional[torch.Tensor],
386-
) -> torch.Tensor:
387-
logits = _apply_top_k_top_p(logits, k, p)
388-
probs = logits.softmax(dim=-1, dtype=torch.float32)
389-
return _random_sample(probs)
390-
391-
392-
def compute_probs_from_logits_op(
393-
logits: torch.Tensor,
394-
temperatures: torch.Tensor,
395-
top_k: Optional[torch.Tensor],
396-
top_p: Optional[torch.Tensor],
397-
) -> torch.Tensor:
398-
"""Pure-PyTorch CPU fallback for probability computation."""
399-
is_greedy = temperatures <= _GREEDY_TEMPERATURE_THRESHOLD
400-
# Greedy rows must pick the argmax of the *original* logits (before temperature).
401-
# Capture the argmax up front; _safely_apply_temperature_inplace then guards the
402-
# division against the greedy sentinel.
403-
argmax_ids = logits.argmax(dim=-1, keepdim=True)
404-
405-
logits = _safely_apply_temperature_inplace(logits, temperatures)
406-
logits = _apply_top_k_top_p(logits, top_k, top_p)
407-
probs = logits.softmax(dim=-1, dtype=torch.float32)
408-
409-
# Turn the greedy rows into a one-hot at argmax by editing `probs` in place,
410-
# instead of building a full [batch, vocab] one-hot buffer and a [batch, vocab]
411-
# torch.where copy. The torch.where here only runs on a [batch, 1] tensor.
412-
# NB: argwhere/index-select on the greedy rows would give a data-dependent shape
413-
# that breaks the surrounding torch.compile graph, so we keep it dense.
414-
greedy_col = is_greedy.unsqueeze(1)
415-
new_at_argmax = torch.where(greedy_col, 1.0, probs.gather(1, argmax_ids))
416-
probs.masked_fill_(greedy_col, 0.0)
417-
probs.scatter_(1, argmax_ids, new_at_argmax)
418-
return probs
419-
420-
421356
class _Fusions:
422357
@staticmethod
423358
@torch.compile(dynamic=None, fullgraph=True)

tensorrt_llm/_torch/pyexecutor/sampler/sampler.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
from ..llm_request import LlmRequest, LlmRequestState, get_draft_token_length
8888
from ..resource_manager import ResourceManager, ResourceManagerType
8989
from ..scheduler import ScheduledRequests
90-
from .ops.interface import SamplerConfig, resolve_sampling_backend
90+
from .ops.interface import resolve_sampling_backend
9191
from .sampling_utils import (
9292
BEAM_SEARCH_PAD_TOKEN,
9393
GREEDY,
@@ -2323,7 +2323,6 @@ class Args:
23232323
max_beam_width: int
23242324
max_total_draft_tokens: int
23252325
disable_overlap_scheduler: bool = False
2326-
disable_flashinfer_sampling: bool = False
23272326
enable_async_worker: bool = False
23282327
enable_speculative_beam_history_d2h: bool = False
23292328

@@ -2347,13 +2346,7 @@ def __init__(self, args: Args):
23472346
self.LOGPROBS_SHAPE = (self.max_num_sequences, self.max_beam_width, self.max_tokens)
23482347
self.TOPK_LOGPROBS_SHAPE = (self.max_num_sequences, self.max_tokens, self.max_topk_logprobs)
23492348

2350-
self._grouped_sampler_cls = resolve_sampling_backend(
2351-
is_cuda=True,
2352-
config=SamplerConfig(
2353-
# IS_FLASHINFER_AVAILABLE is checked inside resolve_sampling_backend.
2354-
use_flashinfer=not args.disable_flashinfer_sampling,
2355-
),
2356-
)
2349+
self._grouped_sampler_cls = resolve_sampling_backend()
23572350

23582351
# AutoDeploy build creates the sampler in inference mode,
23592352
# which would disallow in-place mutating of new_tokens.

0 commit comments

Comments
 (0)