GPUModelRunner.execute_model: where a step's forward pass enters
Responsibilities
GPUModelRunner(gpu_model_runner.py:440-442) is the object inside the worker process that actually runs the forward pass. Worker.execute_model feeds it a SchedulerOutput, and within a single step it does it all: _update_states syncs request state, _prepare_inputs computes logits_indices, _determine_batch_execution_and_padding picks which cudagraph mode to use, maybe_create_ubatch_slices slices micro-batches, _build_attention_metadata builds attention metadata, _preprocess assembles model inputs, and _model_forward runs the model; finally it returns either ModelRunnerOutput(last PP rank) or IntermediateTensors(intermediate PP rank).
It is composed with the worker rather than owned directly: Worker.__init__ does not create it; instead load_model goes through model_runner.load_model(gpu_worker.py:406-413). The constructor itself only stores config, computes max_num_tokens/max_num_reqs, builds the sampler, and conditionally wraps the model with UBatchWrapper(gpu_model_runner.py:5350-5379).
execute_model and sample_tokens are a pair: when async scheduling or the Ray sampler split is enabled, execute_model runs the forward without sampling, instead packing the logits and other intermediate state into ExecuteModelState(gpu_model_runner.py:424-437) for stashing and returning None, then waits for sample_tokens to be called before running _sample(gpu_model_runner.py:4456-4492). This is the key to v1's ability to run "the scheduler's next step" in parallel with "sampling".
Design motivation
- State machine split into two stages: the
ExecuteModelStateNamedTuple explicitly packages all intermediate values between forward and sample(logits, hidden_states, spec_decode_metadata, cudagraph_stats, slot_mappings, etc.)(gpu_model_runner.py:424-437); at the end ofexecute_modelself.execute_model_state = ExecuteModelState(...), and enteringsample_tokensunpacks it(gpu_model_runner.py:4470-4483), avoiding scattered fields. - cudagraph mode dispatched by batch shape:
_determine_batch_execution_and_padding(gpu_model_runner.py:3836-3948) computesuniform_decode,has_lora,has_encoder_output, thencudagraph_dispatcher.dispatch(num_tokens, has_lora, uniform_decode, ...)picksNONE/PIECEWISE/FULL(gpu_model_runner.py:3881-3893); the same shape with different semantics uses different graphs. - Coordinate across DP before splitting the batch:
coordinate_batch_across_dp(gpu_model_runner.py:3907-3918) decidesshould_ubatchandnum_tokens_across_dpacross DP ranks, ensuring all ranks slice the same micro-batch shapes; otherwise ubatch's cudagraph replay would misalign. - The model can be wrapped: at the end of
load_model, depending oncudagraph_modeanduse_ubatching,self.modelis swapped forBreakableCUDAGraphWrapper/CUDAGraphWrapper/UBatchWrapper(gpu_model_runner.py:5353-5379);_model_forwardjust callsself.model(...)without caring what wraps it. - Input prep is its own method:
_prepare_inputs(gpu_model_runner.py:1914) computes logits_indices and spec_decode_metadata, and_preprocess(gpu_model_runner.py:3449) assemblesinput_ids/positions/inputs_embeds/intermediate_tensors/model_kwargs; this is separated from forward so profile and dummy_run can reuse it. - KV scales go eager on the first round: when
calculate_kv_scales=True,cudagraph_modeis forced back toNONE(gpu_model_runner.py:4311-4314), because the dynamic operation that computes KV scale cannot enter the graph; cudagraph is only allowed after the first forward.
Key files
GPUModelRunner.__init__:440-563— stores config, computesmax_num_tokens/max_num_reqs, buildsSampler,MULTIMODAL_REGISTRY,cudagraph_batch_sizes,cudagraph_dispatcher = CudagraphDispatcher(vllm_config).ExecuteModelState:424-437— NamedTuple between forward and sample, recording logits, hidden_states, spec_decode_metadata, cudagraph_stats, etc._update_states:1152— syncs the diff of new/continued requests fromscheduler_outputintoinput_batchpersistent state, returnsdeferred_state_corrections_fn._prepare_inputs:1914— computeslogits_indicesandspec_decode_metadata, decides how to map tokens into logits._determine_batch_execution_and_padding:3836-3948— computesuniform_decode/has_lora/has_encoder_output, callscudagraph_dispatcher.dispatchto pick aCUDAGraphMode; multi-DP rank goes throughcoordinate_batch_across_dp.execute_model entry:4070-4105— entry: clears routed_experts buffer, optional ngram_gpu copy of scheduler_output, KV transfer preemption handling, profile entry.execute_model batch prep:4147-4203—num_scheduled_tokens_np,_prepare_inputs,_determine_batch_execution_and_padding,maybe_create_ubatch_slices.build attn metadata:4270-4295—_get_slot_mappings+_build_attention_metadata, supports ubatch slicing(ubatch_slices=ubatch_slices_attn)._model_forward:4335-4359—set_forward_context(...)wrappingself.model(input_ids=..., positions=..., intermediate_tensors=..., inputs_embeds=..., **model_kwargs), the single line that actually runs the model.sample_tokens entry:4456-4492— unpacksexecute_model_state, optionallyapply_grammar_bitmask, callsself._sample(logits, spec_decode_metadata)._sample:3596-3624— without spec decode, directlyself.sampler(...); otherwise goes throughrejection_sampler.capture_model:6647-6712— one-shot capture of all cudagraphs at startup: large shapes first,set_cudagraph_capturing_enabled(True)+graph_capture(device=self.device), thenlock_workspace()after capture.initialize_kv_cache:7405-7462— buildsattn_groups, metadata builders,initialize_kv_cache_tensorsperKVCacheConfig, registers the KV transfer group.
Data flow
When a step comes in, execute_model first updates persistent state, then decides the batch shape and cudagraph mode, then slices ubatches, then runs forward. This is the middle of the step (preprocess):
# vllm/v1/worker/gpu_model_runner.py L4169-L4182
(
cudagraph_mode,
batch_desc,
should_ubatch,
num_tokens_across_dp,
cudagraph_stats,
) = self._determine_batch_execution_and_padding(
num_tokens=num_tokens_unpadded,
num_reqs=num_reqs,
num_scheduled_tokens_np=num_scheduled_tokens_np,
max_num_scheduled_tokens=max_num_scheduled_tokens,
use_cascade_attn=cascade_attn_prefix_lens is not None,
num_encoder_reqs=len(scheduler_output.scheduled_encoder_inputs),
)Then ubatch slices are cut, set_forward_context wraps the model forward, and all attention metadata, cudagraph runtime mode, and ubatch slices are obtained from the forward context:
# vllm/v1/worker/gpu_model_runner.py L4335-L4359
with (
set_forward_context(
attn_metadata,
self.vllm_config,
num_tokens=num_tokens_padded,
num_tokens_across_dp=num_tokens_across_dp,
cudagraph_runtime_mode=cudagraph_mode,
batch_descriptor=batch_desc,
ubatch_slices=ubatch_slices_padded,
slot_mapping=slot_mappings,
skip_compiled=has_encoder_input,
),
record_function_or_nullcontext("gpu_model_runner: forward"),
self.maybe_get_kv_connector_output(
scheduler_output,
defer_finalize=defer_kv_connector_finalize,
) as kv_connector_output,
):
model_output = self._model_forward(
input_ids=input_ids,
positions=positions,
intermediate_tensors=intermediate_tensors,
inputs_embeds=inputs_embeds,
**model_kwargs,
)_model_forward itself is the single line self.model(...)(gpu_model_runner.py:3807-3813); cudagraph replay / ubatch concurrency all happen inside the wrapper of self.model. An intermediate PP rank takes the IntermediateTensors and returns directly; the last rank stashes hidden_states in ExecuteModelState and waits for sample_tokens to sample and propose_draft_token_ids.
Boundaries and failure modes
- Empty batch early return: when
num_scheduled_tokens == 0, directlyreturn EMPTY_MODEL_RUNNER_OUTPUT(gpu_model_runner.py:4122-4138) and skip forward; withexternal_launcher+ DP>1 it also_dummy_run(1)to sync first, avoiding cross-DP-rank divergence. - State machine reentry detection:
execute_modelraises "sample_tokens() must be called after execute_model() returns None" if it seesself.execute_model_state is not Noneat the entry(gpu_model_runner.py:4075-4079). - KV scales forced eager on the first round: see design motivation above(
gpu_model_runner.py:4311-4314); after the first roundcalculate_kv_scales = False. - Encoder-decoder first step eager: when
has_encoder_input = is_encoder_decoder and num_encoder_reqs > 0,skip_compiled=Truemakes the forward context skip compilation(gpu_model_runner.py:4318-4321). - ngram_gpu modifies scheduler_output: when
use_ngram_gpu()is set, bothnum_scheduled_tokensandscheduled_spec_decode_tokensarecopy()'d thenreplace(...)'d(gpu_model_runner.py:4087-4099), to avoid in-place mutations polluting the engine-side scheduler_output. - cudagraph capture period
is_graph_capturing: during_dummy_run(..., is_graph_capturing=True),_prepare_inputstakes thefor_cudagraph_capture=Truebranch(gpu_model_runner.py:5936), fillingmax_seqlen_kwith the real encoder length. - cudagraph capture large shapes first:
capture_modelwalkscudagraph_dispatcher.get_capture_descs()in reverse(gpu_model_runner.py:6671-6679); large graphs are captured first so small graphs can reuse the memory pool. - shutdown clears graphs:
CUDAGraphWrapper.clear_all_graphs()+BreakableCUDAGraphWrapper.clear_all_graphs()(gpu_model_runner.py:6413-6415) clear historical graphs during re-profiling; otherwise duplicate captures consume extra memory.
Summary
GPUModelRunner is v1's core for running a step on the GPU. It strings "decide batch shape -> dispatch cudagraph -> slice ubatch -> attention metadata -> model forward -> state packaging" into one pipeline, and through the two-stage execute_model + sample_tokens call lets scheduling and sampling overlap. The model body is wrapped by CUDAGraphWrapper or UBatchWrapper, and actually running it is the single line self.model(...). For how the worker invokes it, see /worker/worker; for cudagraph and ubatch capture/replay details, see /worker/ubatch-cudagraph; for the executor layer, see /executor/executor.