5050except ImportError :
5151 HAVE_TE = False
5252
53+ try :
54+ import flashinfer
55+ except ImportError :
56+ flashinfer = None
57+
5358from 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 )
0 commit comments