Skip to content

Commit 58acf4a

Browse files
committed
Implement FlashInfer sampling backend
1 parent 717e9fc commit 58acf4a

6 files changed

Lines changed: 205 additions & 25 deletions

File tree

megatron/core/inference/config.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from dataclasses import InitVar, dataclass
44
from enum import Enum
5-
from typing import List, Optional, Tuple
5+
from typing import List, Literal, Optional, Tuple
66

77
import torch
88

9+
from megatron.core.inference.inference_request import DynamicInferenceRequest
910
from megatron.core.process_groups_config import ProcessGroupCollection
1011
from megatron.core.transformer.module import MegatronModule
1112
from megatron.core.utils import get_attr_wrapped_model
@@ -297,6 +298,9 @@ class InferenceConfig:
297298
Defaults to 0, which means no logging.
298299
"""
299300

301+
sampling_backend: Literal['torch', 'flashinfer'] = 'torch'
302+
"""Which sampling kernels to use during inference."""
303+
300304
request_metadata_types: Optional[List[Tuple[str, torch.dtype, bool]]] = None
301305
"""
302306
A list of the per-request metadata types to track. Each entry is a tuple
@@ -320,3 +324,12 @@ def __post_init__(self, verbose: bool):
320324
f"prefix_caching_routing_alpha must be in [0, 1], "
321325
f"got {self.prefix_caching_routing_alpha}"
322326
)
327+
328+
if self.request_metadata_types is None:
329+
self.request_metadata_types = DynamicInferenceRequest.get_metadata_types()
330+
# FlashInfer reads sampling params directly on GPU.
331+
if self.sampling_backend == 'flashinfer':
332+
self.request_metadata_types = [
333+
(label, dtype, True if label in {"temperature", "top_k", "top_p"} else on_gpu)
334+
for label, dtype, on_gpu in self.request_metadata_types
335+
]

megatron/core/inference/contexts/dynamic_context.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,28 @@ def get_active_request_count(self):
10691069
"""Returns the current number of active requests."""
10701070
return self.total_request_count - self.paused_request_count
10711071

1072+
def _encode_sampling_metadata(self, label: str, value: Tensor) -> Tensor:
1073+
"""Remap disabled-filter sentinels to FlashInfer passthrough values.
1074+
1075+
Megatron's user-facing convention uses ``top_k == 0`` and ``top_p == 0.0``
1076+
to mean "no filter." FlashInfer's sampling kernels instead treat a large
1077+
``top_k`` and ``top_p == 1.0`` as the identity operation. This helper
1078+
translates at the data-entry boundary so the sampling path never has to
1079+
reason about two encodings, and ``build_active_slices`` can be optimized
1080+
without silently corrupting sampling semantics.
1081+
"""
1082+
if self.config.sampling_backend != 'flashinfer':
1083+
return value
1084+
if label == 'top_k':
1085+
return torch.where(
1086+
value == 0,
1087+
torch.full_like(value, torch.iinfo(value.dtype).max),
1088+
value,
1089+
)
1090+
if label == 'top_p':
1091+
return torch.where(value == 0.0, torch.ones_like(value), value)
1092+
return value
1093+
10721094
def build_active_slices(self, batch_size: int):
10731095
"""Build the active slices of specific tensors. This is run on every forward step.
10741096
@@ -1459,9 +1481,12 @@ def add_dummy_requests_parallel(
14591481
self.request_kv_length_offsets[request_slice] = 0
14601482
self.request_kv_block_counts[request_slice] = block_counts
14611483
for i, (label, dtype, _) in enumerate(self.request_metadata_types):
1462-
self.request_metadata[label][request_slice] = torch.tensor(
1484+
value = torch.tensor(
14631485
metadata_cols[i], dtype=dtype, device=torch.cuda.current_device()
14641486
)
1487+
self.request_metadata[label][request_slice] = self._encode_sampling_metadata(
1488+
label, value
1489+
)
14651490

14661491
dummy_block_idx = self.kv_block_allocator.dummy_block_idx
14671492
self.request_last_kv_block_id[request_slice] = dummy_block_idx
@@ -2242,7 +2267,7 @@ def add_request(
22422267
dtype=self.request_metadata[label].dtype,
22432268
)
22442269

2245-
self.request_metadata[label][current_id] = m
2270+
self.request_metadata[label][current_id] = self._encode_sampling_metadata(label, m)
22462271

22472272
# Handle length and block assignments.
22482273
self.request_query_lengths[current_id] = effective_prefill_chunk_length

megatron/core/inference/engines/dynamic_engine.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,26 @@ def create_cuda_graphs(self, reset_context: bool = True):
389389
# Forward pass -> logits.
390390
controller._dynamic_step_forward_logits(input_ids, position_ids)
391391

392+
# Capture sampling CUDA graphs (plain + filtered) for all dimensions.
393+
if controller._enable_cuda_graph:
394+
n = cuda_graph_batch_dimension.req_count
395+
md = context.active_request_metadata
396+
last_token_indices = (
397+
None
398+
if context.config.materialize_only_last_token_logits
399+
else context.active_request_last_token_idxs
400+
)
401+
for filtered in (False, True):
402+
md["temperature"][:n].fill_(1.0)
403+
md["top_k"][:n].fill_(controller.vocab_size)
404+
md["top_p"][:n].fill_(1.0)
405+
controller._flashinfer_sample(
406+
controller._all_logits_cuda.squeeze(0),
407+
n,
408+
filtered=filtered,
409+
last_token_indices=last_token_indices,
410+
)
411+
392412
context.reset()
393413

394414
# Disable inference dispatcher after graph capture

megatron/core/inference/text_generation_controllers/text_generation_controller.py

Lines changed: 130 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,14 @@
5050
except ImportError:
5151
HAVE_TE = False
5252

53+
try:
54+
import flashinfer
55+
except ImportError:
56+
flashinfer = None
57+
5358
from megatron.core.inference.batch_dimensions_utils import InferenceBatchDimensions
59+
from megatron.core.inference.utils import CUDAGraphCache
60+
from megatron.core.transformer.cuda_graphs import CudaGraphManager
5461

5562

5663
# pylint: disable=line-too-long
@@ -128,13 +135,15 @@ def _init_dynamic_sampling_tensors(self):
128135
device = torch.cuda.current_device()
129136
logits_dtype = self.inference_wrapped_model.config.params_dtype
130137

131-
self._sampling_backend = "torch"
132-
self._enable_cuda_graph = False
138+
self._sampling_backend = context.config.sampling_backend
139+
self._enable_cuda_graph = (
140+
self._sampling_backend == "flashinfer" and self.model_config.cuda_graph_impl == "local"
141+
)
133142

134143
# Initialize bookkeeping tensors.
135-
if self._enable_cuda_graph:
136-
self._all_logits_cuda = torch.empty(
137-
(1, max_logits, vocab_size), dtype=logits_dtype, device=device
144+
if self._sampling_backend == "flashinfer":
145+
self._all_logits_cuda = torch.zeros(
146+
(1, max_logits, self.vocab_size), dtype=logits_dtype, device=device
138147
)
139148
else:
140149
self._all_logits_cuda = None
@@ -146,9 +155,10 @@ def _init_dynamic_sampling_tensors(self):
146155
# Last accepted sequence indices for serial MTP computation
147156
self._last_accepted_seq_indices = None
148157

149-
# Used for inefficient torch sampling.
150158
if self._sampling_backend == "torch":
151159
self._torch_sampling_buckets: List[Tuple] = []
160+
elif self._sampling_backend == "flashinfer":
161+
self._sampling_cuda_graphs = CUDAGraphCache()
152162

153163
self._init_mtp_sampling_tensor()
154164

@@ -670,7 +680,7 @@ def _dynamic_step_forward_logits(self, input_ids: Tensor, position_ids: Tensor):
670680
)
671681

672682
# Copy logits to contiguous buffer.
673-
if self._enable_cuda_graph:
683+
if self._enable_cuda_graph or self._sampling_backend == "flashinfer":
674684
self._all_logits_cuda[:, :logits_seq_len, :].copy_(logits)
675685
else:
676686
self._all_logits_cuda = logits
@@ -698,6 +708,25 @@ def _dynamic_step_sample_bookkeeping(self):
698708
self._torch_sampling_buckets = [
699709
(indices, *sampling_params) for sampling_params, indices in bucket_map.items()
700710
]
711+
elif self._sampling_backend == "flashinfer":
712+
n = active_request_count
713+
padded_n = context.padded_active_request_count
714+
md = context.active_request_metadata
715+
716+
if self._enable_cuda_graph and context.using_cuda_graph_this_step():
717+
# Pad slots beyond active with safe defaults so the kernel
718+
# doesn't trip on stale values in unused request slots.
719+
if padded_n > n:
720+
md["temperature"][n:padded_n].fill_(1.0)
721+
md["top_k"][n:padded_n].fill_(self.vocab_size)
722+
md["top_p"][n:padded_n].fill_(1.0)
723+
724+
# NOTE: This currently makes a batch-level decision.
725+
# If any request in the batch uses topk/topp, the whole batch runs the slower kernel.
726+
# Per-request decisions are possible, but not a current priority.
727+
self._fi_any_filtered_gpu = (md["top_k"][:n] < self.vocab_size).any() | (
728+
md["top_p"][:n] < 1.0
729+
).any()
701730

702731
def _rewind_kv_cache(self):
703732
"""Update the KV cache bookkeeping for speculative decoding.
@@ -820,20 +849,23 @@ def _sample_from_logits_2d(self, logits_2d: Tensor) -> Tensor:
820849
Returns:
821850
Tensor: Sampled tokens of shape [num_requests].
822851
"""
823-
spec_token_list = []
824-
indices_list = []
825-
for request_indices, temp, top_k, top_p in self._torch_sampling_buckets:
826-
request_indices_tensor = torch.tensor(
827-
request_indices, device=logits_2d.device, dtype=torch.long
828-
)
829-
spec_token_list.append(
830-
self._torch_sampling_func(logits_2d[request_indices_tensor, :], temp, top_k, top_p)
852+
n = logits_2d.shape[0]
853+
spec_tokens = torch.empty(n, device=logits_2d.device, dtype=torch.int64)
854+
855+
if self._sampling_backend == "flashinfer":
856+
self._flashinfer_sample(
857+
logits_2d, n, self._fi_any_filtered_gpu.item(), eager=True
831858
)
832-
indices_list.append(request_indices_tensor)
859+
spec_tokens[:n] = self._sampled_tokens_cuda[:n]
860+
else:
861+
for request_indices, temp, top_k, top_p in self._torch_sampling_buckets:
862+
request_indices_tensor = torch.tensor(
863+
request_indices, device=logits_2d.device, dtype=torch.long
864+
)
865+
spec_tokens[request_indices_tensor] = self._torch_sampling_func(
866+
logits_2d[request_indices_tensor, :], temp, top_k, top_p
867+
)
833868

834-
spec_tokens = torch.empty(logits_2d.shape[0], device=logits_2d.device, dtype=torch.int64)
835-
for tokens, indices in zip(spec_token_list, indices_list):
836-
spec_tokens[indices] = tokens
837869
return spec_tokens
838870

839871
def _compute_serial_mtp_and_sample(self):
@@ -982,6 +1014,23 @@ def _sample_speculative_logits(
9821014
repeats,
9831015
)
9841016

1017+
if self._sampling_backend == "flashinfer":
1018+
# Expand per-request sampling params to per-token via gather. Disabled
1019+
# filters were already remapped to passthrough values by bookkeeping.
1020+
active_n = len(request_in_prefill_status_tensor)
1021+
md = self.inference_wrapped_model.inference_context.active_request_metadata
1022+
temp_per_token = md["temperature"][:active_n][token_to_request_index]
1023+
top_k_per_token = md["top_k"][:active_n][token_to_request_index]
1024+
top_p_per_token = md["top_p"][:active_n][token_to_request_index]
1025+
1026+
scaled = required_logits.to(torch.float32, copy=True)
1027+
scaled.div_(temp_per_token.unsqueeze(1))
1028+
probs = torch.softmax(scaled, dim=-1)
1029+
output_tokens = flashinfer.sampling.top_k_top_p_sampling_from_probs(
1030+
probs, top_k_per_token, top_p_per_token, generator=self.sampling_rng
1031+
)
1032+
return output_tokens, repeats
1033+
9851034
output_tokens_jumbled_list = []
9861035
token_order_list = []
9871036

@@ -1173,15 +1222,74 @@ def _dynamic_step_sample_logits_and_verify_tokens(self, input_ids: Tensor):
11731222
dim=1
11741223
)
11751224

1225+
def _flashinfer_sample(
1226+
self,
1227+
logits: torch.Tensor,
1228+
n: int,
1229+
filtered: bool,
1230+
eager: bool = False,
1231+
last_token_indices: Optional[torch.Tensor] = None,
1232+
) -> None:
1233+
"""Sample tokens using FlashInfer.
1234+
1235+
Args:
1236+
logits (torch.Tensor): Logits of shape [>=n, vocab_size].
1237+
n (int): Number of requests to sample for (padded for graph path).
1238+
filtered (bool): Whether to use top_k_top_p filtering.
1239+
eager (bool): If True, run without CUDA graph capture/replay.
1240+
last_token_indices (Optional[torch.Tensor]): Indices to gather from logits before
1241+
sampling. Must be a view on a static context tensor for graphability.
1242+
"""
1243+
pool = CudaGraphManager.global_mempool if not eager else None
1244+
md = self.inference_wrapped_model.inference_context.active_request_metadata
1245+
1246+
for _ in self._sampling_cuda_graphs(n, filtered, pool=pool, eager=eager):
1247+
if last_token_indices is None:
1248+
scaled = logits[:n].to(torch.float32, copy=True)
1249+
else:
1250+
scaled = logits[last_token_indices[:n], :].to(torch.float32, copy=True)
1251+
scaled.div_(md["temperature"][:n].unsqueeze(1))
1252+
probs = torch.softmax(scaled, dim=-1)
1253+
if filtered:
1254+
self._sampled_tokens_cuda[:n].copy_(
1255+
flashinfer.sampling.top_k_top_p_sampling_from_probs(
1256+
probs,
1257+
md["top_k"][:n],
1258+
md["top_p"][:n],
1259+
generator=self.sampling_rng,
1260+
)
1261+
)
1262+
else:
1263+
self._sampled_tokens_cuda[:n].copy_(
1264+
flashinfer.sampling.sampling_from_probs(probs, generator=self.sampling_rng)
1265+
)
1266+
11761267
def _dynamic_step_sample_logits(self):
11771268
"""Sample tokens from logits for dynamic batching."""
11781269
# TODO(ksanthanam): Evaluate whether it makes more sense to sample on 1 rank
11791270
# and then broadcast the sampled tokens rather than broadcasting the raw logits.
11801271

1181-
# Last token logits.
11821272
context = self.inference_wrapped_model.inference_context
11831273
active_request_count = context.total_request_count - context.paused_request_count
11841274

1275+
if self._sampling_backend == "flashinfer":
1276+
use_graph = self._enable_cuda_graph and context.using_cuda_graph_this_step()
1277+
n = context.padded_active_request_count if use_graph else active_request_count
1278+
last_token_indices = (
1279+
None
1280+
if context.config.materialize_only_last_token_logits
1281+
else context.active_request_last_token_idxs
1282+
)
1283+
self._flashinfer_sample(
1284+
self._all_logits_cuda.squeeze(0),
1285+
n,
1286+
self._fi_any_filtered_gpu.item(),
1287+
eager=not use_graph,
1288+
last_token_indices=last_token_indices,
1289+
)
1290+
return
1291+
1292+
# Last token logits.
11851293
if context.config.materialize_only_last_token_logits:
11861294
# When materialize_only_last_token_logits is true, last_token_logits is
11871295
# already called in the forward pass of GPT.
@@ -1828,6 +1936,8 @@ async def async_generate_output_tokens_dynamic_batch(
18281936
else None
18291937
)
18301938

1939+
self._dynamic_step_sample_bookkeeping()
1940+
18311941
# Enable routing recording before forward pass if routing replay is enabled
18321942
config = self.inference_wrapped_model.model.config
18331943
if config.moe_enable_routing_replay:
@@ -1859,8 +1969,6 @@ async def async_generate_output_tokens_dynamic_batch(
18591969
with torch.inference_mode():
18601970
return_log_probs, return_top_n_logprobs = self._dynamic_step_log_probs_bookkeeping()
18611971

1862-
self._dynamic_step_sample_bookkeeping()
1863-
18641972
if self.num_speculative_tokens > 0:
18651973
# Phase 1: Verify speculative tokens using base logits only.
18661974
self._dynamic_step_sample_logits_and_verify_tokens(input_ids)

megatron/inference/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ def get_inference_config_from_model_and_args(model: MegatronModule, args):
362362
logging_step_interval=args.inference_logging_step_interval,
363363
num_speculative_tokens=args.num_speculative_tokens,
364364
use_synchronous_zmq_collectives=args.inference_use_synchronous_zmq_collectives,
365+
sampling_backend=args.inference_dynamic_batching_sampling_backend,
365366
)
366367

367368

megatron/training/arguments.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,15 @@ def validate_args(args, defaults={}):
944944
):
945945
raise ValueError("MXFP8 with inference optimized layers requires FlashInfer >= 0.6.4")
946946

947+
if args.inference_dynamic_batching_sampling_backend == 'flashinfer':
948+
try:
949+
import flashinfer # noqa: F401
950+
except ImportError:
951+
raise ValueError(
952+
"--inference-dynamic-batching-sampling-backend=flashinfer "
953+
"requires flashinfer to be installed."
954+
)
955+
947956
if args.use_megatron_fsdp:
948957
# NOTE: The flag `use_custom_fsdp` is deprecated and will be removed in future versions.
949958
# Please use `use_megatron_fsdp` instead, as all functionality will be migrated there.
@@ -1908,6 +1917,10 @@ def _add_inference_args(parser):
19081917
group.add_argument('--inference-dynamic-batching-cuda-graph-mixed-prefill-count',
19091918
type=int, default=16,
19101919
help='Number of mixed prefill requests to capture in a cuda graph.')
1920+
group.add_argument('--inference-dynamic-batching-sampling-backend',
1921+
type=str, default='torch',
1922+
choices=['torch', 'flashinfer'],
1923+
help='Which sampling kernels to use during inference.')
19111924
group.add_argument('--inference-logging-step-interval', type=int, default=0,
19121925
help='Step interval for logging inference metrics. '
19131926
'Default to 0 to disable inference logging.')

0 commit comments

Comments
 (0)