Skip to content

Commit 0fe449d

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 0fe449d

17 files changed

Lines changed: 127 additions & 354 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: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
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, but the import is guarded so the
22+
module stays importable without flashinfer (keeping ``sampling_utils`` importable
23+
for its torch-only consumers). These ops are only reached after the Torch sampler
24+
or a speculative worker has verified flashinfer at construction time.
2025
"""
2126

2227
from typing import Optional

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

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,34 @@
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 picks the strategy-grouping
18+
sampler once at init time, so no per-call dispatch happens inside CUDA graph
19+
capture.
2220
"""
2321

2422
from collections.abc import Hashable
25-
from dataclasses import dataclass
2623
from typing import Type, cast
2724

2825
from tensorrt_llm._torch.flashinfer_utils import IS_FLASHINFER_AVAILABLE
2926
from tensorrt_llm._torch.pyexecutor.sampler.sampling_utils import (
3027
FlashInferGroupedStrategySampler,
3128
GroupedStrategySampler,
32-
SimpleGroupedStrategySampler,
3329
)
3430

3531

36-
@dataclass(frozen=True, kw_only=True)
37-
class SamplerConfig:
38-
"""Configuration used to resolve the sampling backend at init time."""
39-
40-
use_flashinfer: bool = False
41-
42-
43-
def resolve_sampling_backend(
44-
is_cuda: bool,
45-
config: SamplerConfig,
46-
) -> Type[GroupedStrategySampler[Hashable]]:
32+
def resolve_sampling_backend() -> Type[GroupedStrategySampler[Hashable]]:
4733
"""Pick the strategy-grouping sampler class at init time; CUDA-graph safe.
4834
49-
Selection order:
50-
1. FlashInfer — is_cuda AND IS_FLASHINFER_AVAILABLE AND config.use_flashinfer
51-
2. Torch — everything else (including CPU)
35+
The Torch sampler hard-depends on flashinfer, so this raises a clear error
36+
at construction time when flashinfer is unavailable rather than silently
37+
falling back to a torch implementation.
5238
"""
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)
39+
if not IS_FLASHINFER_AVAILABLE:
40+
raise ImportError(
41+
"Torch sampler requires flashinfer, but flashinfer is not available "
42+
"on this platform. flashinfer is a hard dependency here; please "
43+
"install the version pinned in requirements.txt."
44+
)
45+
# FlashInferGroupedStrategySampler uses a narrower FlashInfer key type; the
46+
# caller only forwards the grouping callables, so erase to Hashable.
47+
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.

tensorrt_llm/_torch/pyexecutor/sampler/sampling_utils.py

Lines changed: 13 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,10 @@
2626

2727
import torch
2828

29-
from tensorrt_llm._torch.flashinfer_utils import IS_FLASHINFER_AVAILABLE
3029
from tensorrt_llm._torch.pyexecutor.sampler.ops import flashinfer, vanilla
3130

32-
# NB: these flashinfer op wrappers are plain Python functions that are safe to
33-
# import even without flashinfer installed (the flashinfer import inside
34-
# ops/flashinfer.py is itself guarded); they are only *called* under
35-
# IS_FLASHINFER_AVAILABLE. Importing them unconditionally keeps them defined for
36-
# static analysis (they are referenced unconditionally in the strategy impls).
31+
# These op wrappers are safe to import without flashinfer installed; they are
32+
# only called on the flashinfer sampler / speculative-worker paths.
3733
from tensorrt_llm._torch.pyexecutor.sampler.ops.flashinfer import (
3834
sampling_from_probs_generator_op as sampling_from_probs_generator_op,
3935
)
@@ -179,6 +175,8 @@ def sample(
179175
group_metadata: StrategyMetadata | None = None,
180176
return_probs: bool = True,
181177
) -> tuple[torch.Tensor, torch.Tensor | None, float | None]:
178+
"""Torch-native single-strategy sampling for the rejection-sampling
179+
draft-probs path (sampler.py), independent of the flashinfer sampler."""
182180
softmax: torch.Tensor | None
183181
# 'cast' needed b/c of https://github.com/python/mypy/issues/19081
184182
match strategy:
@@ -266,57 +264,6 @@ def sample_grouped_strategies(
266264
raise NotImplementedError
267265

268266

269-
class SimpleGroupedStrategySampler(GroupedStrategySampler[Strategy]):
270-
STRATEGY_KEY_TYPE: TypeAlias = Strategy
271-
272-
@override
273-
@staticmethod
274-
def strategy_grouping_key(strategy: Strategy) -> STRATEGY_KEY_TYPE:
275-
return strategy
276-
277-
@override
278-
@staticmethod
279-
def get_metadata_type_for_group(
280-
strategy_key: STRATEGY_KEY_TYPE,
281-
) -> Type[StrategyMetadata] | None:
282-
match strategy_key:
283-
case ("beam_search", _, _, _):
284-
return BeamSearchMetadata
285-
case _:
286-
return None
287-
288-
@override
289-
@staticmethod
290-
def sample_grouped_strategies(
291-
group_key: STRATEGY_KEY_TYPE,
292-
strategies: list[Strategy],
293-
logits: torch.Tensor,
294-
*,
295-
group_logit_indices: torch.Tensor | None = None,
296-
generator: torch.Generator | None = None,
297-
return_probs: bool,
298-
group_metadata: StrategyMetadata | None = None,
299-
) -> tuple[torch.Tensor, torch.Tensor | None, float | torch.Tensor | None]:
300-
if group_key[0] == "beam_search":
301-
beam_width_in = group_key[1]
302-
else:
303-
beam_width_in = 1
304-
305-
if group_logit_indices is not None:
306-
logits = logits[group_logit_indices]
307-
assert logits.size(0) == beam_width_in * len(strategies)
308-
309-
assert all(strategy == group_key for strategy in strategies), "group must be consistent"
310-
311-
return sample(
312-
group_key,
313-
logits,
314-
generator=generator,
315-
return_probs=return_probs,
316-
group_metadata=group_metadata,
317-
)
318-
319-
320267
class _StrategyImpls:
321268
class StrategyImpl(abc.ABC):
322269
@classmethod
@@ -928,24 +875,15 @@ def compute_probs_from_logits(
928875
top_k: Optional[torch.Tensor],
929876
top_p: Optional[torch.Tensor],
930877
) -> torch.Tensor:
931-
"""Compute filtered+normalized probs. Dispatches: flashinfer → C++ op → CPU.
878+
"""Compute filtered+normalized probs via flashinfer (hard dependency).
932879
933-
``temperatures``, ``top_k``, ``top_p`` are per-request tensors matching
934-
the spec-decoding call site in interface.py.
880+
``temperatures``, ``top_k``, ``top_p`` are per-request tensors matching the
881+
spec-decoding call site in interface.py.
935882
"""
936883
if top_k is not None:
937884
top_k = sanitize_top_k(top_k, logits.shape[-1])
938885

939-
if logits.is_cuda and IS_FLASHINFER_AVAILABLE:
940-
return flashinfer.compute_probs_from_logits_op(logits, temperatures, top_k, top_p)
941-
if logits.is_cuda:
942-
# TRT-LLM C++ op (CUDA, no flashinfer). The op keeps a skip_temperature
943-
# flag; temperature is always applied here.
944-
probs: torch.Tensor = torch.ops.trtllm.compute_probs_from_logits_op(
945-
logits, temperatures, top_k, top_p, False
946-
)
947-
return probs
948-
return vanilla.compute_probs_from_logits_op(logits, temperatures, top_k, top_p)
886+
return flashinfer.compute_probs_from_logits_op(logits, temperatures, top_k, top_p)
949887

950888

951889
@torch.compile(options={"max-autotune": True})
@@ -968,12 +906,9 @@ def sampling_batch_spec_dec_one_model(
968906
is_greedy = temperatures <= vanilla._GREEDY_TEMPERATURE_THRESHOLD
969907
greedy_tokens = logits.argmax(dim=-1)
970908
logits = vanilla._safely_apply_temperature_inplace(logits, temperatures)
971-
if IS_FLASHINFER_AVAILABLE:
972-
sampled = flashinfer.top_k_top_p_sampling_from_logits_op(
973-
logits, top_k, top_p, seed=seed, offset=offset
974-
)
975-
else:
976-
sampled = vanilla.forward_native_sampling(logits, top_k, top_p)
909+
sampled = flashinfer.top_k_top_p_sampling_from_logits_op(
910+
logits, top_k, top_p, seed=seed, offset=offset
911+
)
977912
# argmax yields int64; cast so torch.where preserves the sampler's dtype
978913
# (flashinfer returns int32) instead of promoting the result to int64.
979914
return torch.where(is_greedy, greedy_tokens.to(sampled.dtype), sampled)
@@ -989,14 +924,8 @@ def sampling_batch_spec_dec_one_model_for_rejection(
989924
offset: Optional[torch.Tensor] = None,
990925
) -> tuple[torch.Tensor, torch.Tensor]:
991926
"""Draft sampler returning tokens AND probs for the downstream rejection-sampling path."""
927+
# Rejection sampling relies on flashinfer's seed/offset support for
928+
# determinism and cross-rank consistency.
992929
probs = compute_probs_from_logits(logits, temperatures, top_k, top_p)
993-
if not IS_FLASHINFER_AVAILABLE:
994-
# The torch-native fallback samples from the global RNG and ignores
995-
# seed/offset, which breaks determinism and cross-rank consistency that
996-
# one-model speculative rejection sampling relies on. Require flashinfer
997-
# instead of silently degrading (matches the pre-refactor behavior).
998-
raise RuntimeError(
999-
"Rejection sampling for one-model speculative decoding requires flashinfer"
1000-
)
1001930
tokens = flashinfer.sampling_from_probs_op(probs, seed=seed, offset=offset)
1002931
return tokens, probs

0 commit comments

Comments
 (0)