@@ -207,26 +207,39 @@ def _generate_batch_impl(
207207 allow_device_greedy_sampling = allow_device_greedy_sampling ,
208208 kv_allocations = allocations ,
209209 )
210- fast_path_result = self . _executor . try_generate_batch (
211- record ,
212- requests ,
213- prefill_batch ,
214- generate_config ,
215- )
216- if fast_path_result is not None :
217- return fast_path_result
218-
219- with self ._executor .session ():
220- prefill_result = self . _executor . run_prefill (
221- runtime_model ,
210+ prefill_token_budget = record . runtime . max_num_batched_tokens
211+ if prefill_token_budget <= 0 :
212+ raise ValueError ( "max_num_batched_tokens must be positive" )
213+ long_prefill_threshold = record . runtime . long_prefill_token_threshold
214+ if long_prefill_threshold > 0 :
215+ prefill_token_budget = min ( prefill_token_budget , long_prefill_threshold )
216+ total_prefill_tokens = sum ( len ( token_ids ) for token_ids in prompt_token_ids )
217+ batch_fits_budget = total_prefill_tokens <= prefill_token_budget
218+ if batch_fits_budget :
219+ fast_path_result = self ._executor .try_generate_batch (
220+ record ,
221+ requests ,
222222 prefill_batch ,
223+ generate_config ,
223224 )
224- prefill_logits = prefill_result .logits
225- prefill_sampled_token_ids = (
226- prefill_result .sampled_token_ids
227- if allow_device_greedy_sampling
228- else None
229- )
225+ if fast_path_result is not None :
226+ return fast_path_result
227+
228+ with self ._executor .session ():
229+ if batch_fits_budget :
230+ prefill_result = self ._executor .run_prefill (runtime_model , prefill_batch )
231+ prefill_logits = prefill_result .logits
232+ prefill_sampled_token_ids = (
233+ prefill_result .sampled_token_ids
234+ if prefill_batch .allow_device_greedy_sampling
235+ else None
236+ )
237+ else :
238+ prefill_logits , prefill_sampled_token_ids = self ._run_prefill_in_chunks (
239+ runtime_model ,
240+ prefill_batch ,
241+ prefill_token_budget ,
242+ )
230243
231244 sampling_params = self ._sampler .from_generate_config (generate_config )
232245 current_tokens = self ._sample_batch_rows (
@@ -420,6 +433,135 @@ def _generate_result(self, model_id: str, prompt: str, config: GenerateConfig) -
420433 """Generate one result by reusing the batch path."""
421434 return self .generate_batch (model_id , [prompt ], config )[0 ]
422435
436+ def _run_prefill_in_chunks (
437+ self ,
438+ runtime_model ,
439+ batch : PrefillBatch ,
440+ token_budget : int ,
441+ ) -> tuple [torch .Tensor , torch .Tensor | None ]:
442+ """Run prefill calls whose combined chunk size does not exceed the budget."""
443+ if token_budget <= 0 :
444+ raise ValueError ("max_num_batched_tokens must be positive" )
445+
446+ row_count = len (batch .request_ids )
447+ prompt_lengths = [int (batch .seq_lens [row ].item ()) for row in range (row_count )]
448+ computed_tokens = [0 ] * row_count
449+ final_logits : list [torch .Tensor | None ] = [None ] * row_count
450+ final_sampled_ids : list [torch .Tensor | None ] = [None ] * row_count
451+ next_request_idx = 0
452+
453+ while any (computed_tokens [row ] < prompt_lengths [row ] for row in range (row_count )):
454+ active_rows : list [int ] = []
455+ for offset in range (row_count ):
456+ row = (next_request_idx + offset ) % row_count
457+ if computed_tokens [row ] < prompt_lengths [row ]:
458+ active_rows .append (row )
459+ selected_rows = active_rows [:1 ]
460+ per_request_budget = token_budget
461+ chunk_lengths = [
462+ min (prompt_lengths [row ] - computed_tokens [row ], per_request_budget )
463+ for row in selected_rows
464+ ]
465+ max_chunk_len = max (chunk_lengths )
466+
467+ chunk_token_ids = batch .token_ids .new_zeros ((len (selected_rows ), max_chunk_len ))
468+ chunk_embeddings = None
469+ if batch .input_embeddings is not None :
470+ chunk_embeddings = batch .input_embeddings .new_zeros (
471+ (len (selected_rows ), max_chunk_len , * batch .input_embeddings .shape [2 :])
472+ )
473+ chunk_positions = torch .full (
474+ (len (selected_rows ), max_chunk_len ),
475+ - 1 ,
476+ dtype = torch .long ,
477+ device = batch .token_ids .device ,
478+ )
479+ chunk_seq_lens = batch .seq_lens .new_empty ((len (selected_rows ),))
480+
481+ for chunk_row , (request_row , chunk_len ) in enumerate (
482+ zip (selected_rows , chunk_lengths , strict = True )
483+ ):
484+ chunk_start = computed_tokens [request_row ]
485+ chunk_end = chunk_start + chunk_len
486+ chunk_token_ids [chunk_row , :chunk_len ] = batch .token_ids [
487+ request_row , chunk_start :chunk_end
488+ ]
489+ if chunk_embeddings is not None and batch .input_embeddings is not None :
490+ chunk_embeddings [chunk_row , :chunk_len ] = batch .input_embeddings [
491+ request_row , chunk_start :chunk_end
492+ ]
493+ chunk_positions [chunk_row , :chunk_len ] = torch .arange (
494+ chunk_start ,
495+ chunk_end ,
496+ dtype = torch .long ,
497+ device = batch .token_ids .device ,
498+ )
499+ chunk_seq_lens [chunk_row ] = chunk_end
500+
501+ prefill_result = self ._executor .run_prefill (
502+ runtime_model ,
503+ PrefillBatch (
504+ request_ids = [batch .request_ids [row ] for row in selected_rows ],
505+ token_ids = chunk_token_ids ,
506+ input_embeddings = chunk_embeddings ,
507+ seq_lens = chunk_seq_lens ,
508+ allow_device_greedy_sampling = (
509+ batch .allow_device_greedy_sampling
510+ and any (
511+ computed_tokens [row ] + chunk_len == prompt_lengths [row ]
512+ for row , chunk_len in zip (
513+ selected_rows ,
514+ chunk_lengths ,
515+ strict = True ,
516+ )
517+ )
518+ ),
519+ kv_allocations = [batch .kv_allocations [row ] for row in selected_rows ],
520+ positions = chunk_positions ,
521+ block_ids = (
522+ [batch .block_ids [row ] for row in selected_rows ]
523+ if batch .block_ids
524+ else []
525+ ),
526+ ),
527+ )
528+
529+ for chunk_row , (request_row , chunk_len ) in enumerate (
530+ zip (selected_rows , chunk_lengths , strict = True )
531+ ):
532+ computed_tokens [request_row ] += chunk_len
533+ if computed_tokens [request_row ] != prompt_lengths [request_row ]:
534+ continue
535+ final_logits [request_row ] = self ._select_batch_row (
536+ prefill_result .logits ,
537+ chunk_row ,
538+ ).clone ()
539+ sampled_ids = (
540+ prefill_result .sampled_token_ids
541+ if batch .allow_device_greedy_sampling
542+ else None
543+ )
544+ if sampled_ids is not None :
545+ if sampled_ids .dim () == 0 :
546+ sampled_id = sampled_ids
547+ elif sampled_ids .dim () == 1 :
548+ sampled_id = sampled_ids [chunk_row ]
549+ else :
550+ sampled_id = sampled_ids [chunk_row ].reshape (- 1 )[0 ]
551+ final_sampled_ids [request_row ] = sampled_id .clone ()
552+
553+ next_request_idx = (selected_rows [- 1 ] + 1 ) % row_count
554+
555+ if any (logits is None for logits in final_logits ):
556+ raise RuntimeError ("prefill did not produce final logits for every request" )
557+ logits = torch .stack ([row for row in final_logits if row is not None ])
558+ sampled_ids = None
559+ if all (sampled_id is not None for sampled_id in final_sampled_ids ):
560+ sampled_ids = torch .stack (
561+ [sampled_id for sampled_id in final_sampled_ids if sampled_id is not None ]
562+ )
563+ return logits , sampled_ids
564+
423565 def _sample_batch_rows (
424566 self ,
425567 logits : torch .Tensor | None ,
0 commit comments