1010from contextlib import ExitStack , contextmanager
1111from inspect import isclass , signature
1212from logging import DEBUG
13- from typing import Any , TypeVar
13+ from typing import Any , TypeVar , cast
1414
1515import msgspec
1616import zmq
@@ -168,7 +168,7 @@ def __init__(
168168 if kv_connector is not None :
169169 # Collect and store KV connector xfer metadata from workers
170170 # (after KV cache registration)
171- xfer_handshake_metadata = self .model_executor .get_kv_connector_handshake_metadata ()
171+ xfer_handshake_metadata = self .modeling .get_kv_connector_handshake_metadata ()
172172
173173 if xfer_handshake_metadata :
174174 # xfer_handshake_metadata is list of dicts from workers
@@ -305,9 +305,13 @@ def step(self) -> tuple[dict[int, EngineCoreOutputs], bool]:
305305 if not self .scheduler .has_requests ():
306306 return {}, False
307307 scheduler_output = self .scheduler .schedule ()
308+ future = self .modeling .execute_model (scheduler_output , non_block = True )
309+ grammar_output = self .scheduler .get_grammar_bitmask (scheduler_output )
308310
309311 with self .log_error_detail (scheduler_output ):
310- model_output = self .modeling .execute_model (scheduler_output )
312+ model_output = future .result ()
313+ if model_output is None :
314+ model_output = self .modeling .sample_tokens (grammar_output )
311315
312316 engine_core_outputs = self .scheduler .update_from_output (scheduler_output , model_output )
313317
@@ -345,16 +349,39 @@ def step_with_batch_queue(
345349 assert len (batch_queue ) < self .batch_queue_size
346350
347351 model_executed = False
352+ deferred_scheduler_output = None
348353 if self .scheduler .has_requests ():
349354 scheduler_output = self .scheduler .schedule ()
350- future = self .modeling .execute_model (scheduler_output , non_block = True )
351- batch_queue .appendleft ((future , scheduler_output ))
355+ exec_future = self .modeling .execute_model (scheduler_output , non_block = True )
352356
353357 model_executed = scheduler_output .total_num_scheduled_tokens > 0
354- if model_executed and len (batch_queue ) < self .batch_queue_size and not batch_queue [- 1 ][0 ].done ():
355- # Don't block on next worker response unless the queue is full
356- # or there are no more requests to schedule.
357- return None , True
358+ if scheduler_output .pending_structured_output_tokens :
359+ # We need to defer sampling until we have processed the model output
360+ # from the prior step.
361+ deferred_scheduler_output = scheduler_output
362+ # Block-wait for execute to return (continues running async on the GPU).
363+ with self .log_error_detail (scheduler_output ):
364+ exec_result = exec_future .result ()
365+ assert exec_result is None
366+ else :
367+ # We aren't waiting for any tokens, get any grammar output immediately.
368+ grammar_output = self .scheduler .get_grammar_bitmask (scheduler_output )
369+ # Block-wait for execute to return (continues running async on the GPU).
370+ with self .log_error_detail (scheduler_output ):
371+ exec_result = exec_future .result ()
372+
373+ if exec_result is None :
374+ # Call sample tokens.
375+ future = self .modeling .sample_tokens (grammar_output , non_block = True )
376+ else :
377+ # No sampling required (e.g. all requests finished).
378+ future = cast (Future [ModelRunnerOutput ], exec_future )
379+ # Add this step's future to the queue.
380+ batch_queue .appendleft ((future , scheduler_output ))
381+ if model_executed and len (batch_queue ) < self .batch_queue_size and not batch_queue [- 1 ][0 ].done ():
382+ # Don't block on next worker response unless the queue is full
383+ # or there are no more requests to schedule.
384+ return None , True
358385
359386 elif not batch_queue :
360387 # Queue is empty. We should not reach here since this method should
@@ -368,6 +395,17 @@ def step_with_batch_queue(
368395 model_output = future .result ()
369396
370397 engine_core_outputs = self .scheduler .update_from_output (scheduler_output , model_output )
398+
399+ # NOTE: We can either handle the deferred tasks here or save
400+ # in a field and do it immediately once step_with_batch_queue is
401+ # re-called. The latter slightly favors TTFT over TPOT/throughput.
402+ if deferred_scheduler_output :
403+ # We now have the tokens needed to compute the bitmask for the
404+ # deferred request. Get the bitmask and call sample tokens.
405+ grammar_output = self .scheduler .get_grammar_bitmask (deferred_scheduler_output )
406+ future = self .modeling .sample_tokens (grammar_output , non_block = True )
407+ batch_queue .appendleft ((future , deferred_scheduler_output ))
408+
371409 return engine_core_outputs , model_executed
372410
373411 def shutdown (self ):
0 commit comments