"v1/worker/gpu/model_runner.py: Model Runner V2"
Code version: vLLM @ 3e44c0d · 2026-10-22
Purpose#
vllm/v1/worker/gpu/model_runner.py is the GPU half of an engine step: it keeps the request states on the GPU, builds each step's inputs, runs the model and samples the next tokens.
| File | vllm/v1/worker/gpu/model_runner.py, 2,483 lines. The header asks contributors to keep the file "minimal" and to put model-specific code elsewhere (vllm/v1/worker/gpu/model_runner.py:3-17). |
|---|---|
| Main class | GPUModelRunner(LoRAModelRunnerMixin) (vllm/v1/worker/gpu/model_runner.py:199). |
| Runs in | Each worker process, or the engine core process with the uni executor. The Python code runs on the CPU and only queues GPU work; one extra CUDA stream copies outputs back. |
| Called by | Worker in v1/worker/gpu_worker.py, which builds it when use_v2_model_runner is true (vllm/v1/worker/gpu_worker.py:567-580). |
| Calls | states.py, input_batch.py, block_table.py, attn_utils.py, cudagraph_utils.py, async_utils.py, the model states, the sampler, the speculators and the model itself (vllm/v1/worker/gpu/model_runner.py:90-190). |
| Not in this file | Kernels for input preparation (in input_batch.py and block_table.py), CUDA graph capture details (torch.compile and CUDA graphs in vLLM), attention (Attention backends: backend, metadata builder and implementation), the sampler (gpu/sample/). |
The page The model runner (Model Runner V2) explains the design. This page is a map of the file and a walk through its main methods.
Where it sits#
Who calls it#
- Start-up, through the worker.
load_model(vllm/v1/worker/gpu_worker.py:591-598),profile_runduring memory profiling (vllm/v1/worker/gpu_worker.py:697),initialize_kv_cache(vllm/v1/worker/gpu_worker.py:898), warm-up_dummy_runcalls andcapture_model(vllm/v1/worker/gpu_worker.py:961-985). - Every step.
Worker.execute_modelandWorker.sample_tokensforward the engine core's calls (vllm/v1/worker/gpu_worker.py:1318-1321,vllm/v1/worker/gpu_worker.py:1350-1353). - Subclasses. The CPU runner subclasses this class (
vllm/v1/worker/cpu/model_runner.py:10). An out-of-tree runner can pick its own PCP manager throughpcp_manager_cls(vllm/v1/worker/gpu/model_runner.py:2433-2437).
What it calls#
RequestStatefor the persistent batch (vllm/v1/worker/gpu/model_runner.py:286-295),InputBuffersandInputBatchfor the per-step tensors,BlockTablesfor block tables and slot mappings.init_attn_backend,init_kv_cacheandbuild_slot_mappings_by_layerfromattn_utils.py.ModelCudaGraphManageranddispatch_cg_and_sync_dpfor CUDA graphs.set_forward_contextaround the model call ("forward_context.py: the per-step context of the forward pass").Sampler,RejectionSampler,StructuredOutputsWorker, the speculator,AsyncOutput.
Key classes and functions#
| Name | Lines | Role |
|---|---|---|
__init__ | vllm/v1/worker/gpu/model_runner.py:200-368 | Configs, the copy stream, RequestState, speculator, input buffers, LoRA state. |
load_model | vllm/v1/worker/gpu/model_runner.py:385-555 | Load the model, pick the model state, build the samplers. |
initialize_kv_cache | vllm/v1/worker/gpu/model_runner.py:622-823 | Attention groups, block tables, CUDA graph mode and manager, KV cache tensors. |
_dummy_run, profile_run | vllm/v1/worker/gpu/model_runner.py:835-1040 | Fake batches through execute_model for profiling and warm-up. |
capture_model | vllm/v1/worker/gpu/model_runner.py:1073-1153 | Capture the encoder and decoder CUDA graphs; log time and memory. |
finish_requests, add_requests, update_requests | vllm/v1/worker/gpu/model_runner.py:1173-1291 | Apply the scheduler's plan to the persistent state. |
gather_batch_req_state | vllm/v1/worker/gpu/model_runner.py:1293-1373 | Sort the batch, map to slots, find prefills and the uniform decode count. |
prepare_inputs | vllm/v1/worker/gpu/model_runner.py:1375-1575 | Build the InputBatch with GPU kernels. |
prepare_attn | vllm/v1/worker/gpu/model_runner.py:1577-1597 | Gather block tables, compute slot mappings. |
sample, postprocess_sampled | vllm/v1/worker/gpu/model_runner.py:1615-1730 | Logits, bitmask, sampler; then write the new tokens into the state. |
execute_model | vllm/v1/worker/gpu/model_runner.py:1741-2110 | The forward half of a step. |
sample_tokens | vllm/v1/worker/gpu/model_runner.py:2112-2311 | The sampling half; returns an AsyncOutput. |
pool | vllm/v1/worker/gpu/model_runner.py:2316-2358 | The pooling replacement of sample_tokens. |
shutdown | vllm/v1/worker/gpu/model_runner.py:2368-2407 | Free weights, KV caches and graphs. |
ExecuteModelState, BatchReqState | vllm/v1/worker/gpu/model_runner.py:2439-2468 | Hand-over from execute_model to sample_tokens; sorted batch state. |
sort_batch_req_ids | vllm/v1/worker/gpu/model_runner.py:2471-2483 | Batch order: drafts, decodes, then by token count. |
Walk through the main methods#
load_model#
with DeviceMemoryProfiler() as m:
model_loader = get_model_loader(self.vllm_config.load_config)
logger.info_once("Loading model from scratch...")
# Capture warmup providers selected while constructing the model.
with self.jit_warmup_registry.activate():
self.model = model or model_loader.load_model(
vllm_config=self.vllm_config,
model_config=self.vllm_config.model_config,
)
if self.lora_config:
self.model = self.load_lora_model(
self.model, self.vllm_config, self.device
)- Line 398.
get_model_loaderpicks the loader for--load-format("Weight loading: from checkpoint files to GPU parameters"). - Lines 402-406. The loader builds the model and loads the weights. JIT warm-up providers chosen during the build are recorded.
- Lines 407-410. With LoRA, the model's layers are wrapped.
After loading, it builds the model state and decode_query_len (vllm/v1/worker/gpu/model_runner.py:438-445). On the last PP rank of a generative model it builds the Sampler, a RejectionSampler with speculative decoding, the prompt-logprobs worker and the structured-output worker (vllm/v1/worker/gpu/model_runner.py:464-532). A pooling model gets a PoolingRunner instead (lines 534-535). In STOCK_TORCH_COMPILE mode the whole model is compiled here (lines 554-555).
initialize_kv_cache#
if isinstance(self.speculator, DraftModelSpeculator):
draft_attn_layer_names = self.speculator.draft_attn_layer_names
# Metadata builders select attention kernels that need JIT warmup.
with self.jit_warmup_registry.activate():
(
self.attn_groups,
attn_cg_support,
self.kernel_block_sizes,
) = init_attn_backend(
self.kv_cache_config,
self.vllm_config,
self.device,
draft_layer_names=draft_attn_layer_names,
)
additional_attn_cg_support = self.model_state.get_additional_cg_support()
attn_cg_support = attn_cg_support.narrow(*additional_attn_cg_support)init_attn_backend returns the attention groups, the weakest CUDA graph support and the kernel block sizes. Then the method builds BlockTables (lines 716-728), resolves the CUDA graph mode with the backend support, and builds the CUDA graph manager:
cudagraph_mode = self.compilation_config.resolve_cudagraph_mode_and_sizes(
attn_cg_support.min_cg_support,
attn_cg_support.min_cg_attn_backend,
self.decode_query_len,
use_v2_model_runner=True,
kv_cache_config=self.kv_cache_config,
max_num_reqs=self.max_num_reqs,
is_profiling=is_profiling,
piecewise_capture_available=piecewise_capture_available,
)
self.cudagraph_manager = ModelCudaGraphManager(
self.vllm_config,
self.device,
cudagraph_mode,
decode_query_len=self.decode_query_len,
lora_capture_cases=self.lora_capture_cases,
varlen_decode=self.adaptive_verification is not None,
ubatch_runner=self.ubatch_runner,
)Last, it allocates the KV cache and binds each view to its layer through static_forward_context (vllm/v1/worker/gpu/model_runner.py:792-808), and creates the KV connector unless this is a profiling call (vllm/v1/worker/gpu/model_runner.py:813-823).
execute_model#
The state updates (lines 1753-1771) and the input steps are on The model runner (Model Runner V2). The core of the method:
batch_desc, dp_sync_state = dispatch_cg_and_sync_dp(
self.cudagraph_manager,
num_reqs,
num_toks,
uniform_tok_count,
self.dp_size,
self.dp_rank,
max_query_len=max_query_len,
need_eager=is_profile or skip_compiled,
num_active_loras=num_active_loras,
parallel_config=self.parallel_config,
allow_ubatching=(
self.ubatch_runner is not None and not skip_attn_for_dummy_run
),
uniform_decode=uniform_tok_count == self.decode_query_len,
)
if batch_desc.num_tokens == 0:
# All DP ranks have zero tokens to run.
empty_output = self.kv_connector.no_forward(scheduler_output)
return self._merge_ec_connector_no_forward(scheduler_output, empty_output)
cudagraph_stats = None
if not dummy_run:
# Common case.
# Prepare all the inputs and copy to the input buffers.
if self.observability_config.cudagraph_metrics:
cudagraph_stats = make_cudagraph_stats(batch_desc, num_toks)
assert batch_req_state is not None
input_batch = self.prepare_inputs(
scheduler_output, batch_req_state, batch_desc, num_active_loras
)
block_tables, slot_mappings = self.prepare_attn(input_batch)- Lines 1806-1821. Pick the CUDA graph descriptor; with DP > 1 the ranks agree on one padded size.
- Lines 1823-1826. If every DP rank has zero tokens, return an empty output.
- Lines 1835-1838. Build the step tensors, then the block tables and slot mappings.
Then the method builds the attention metadata (lines 1924-1951), the model inputs (lines 1984-2010), and runs the model in FULL, PIECEWISE or eager mode (lines 2024-2070). It stores an ExecuteModelState and returns None, or IntermediateTensors on a non-last PP rank (vllm/v1/worker/gpu/model_runner.py:2090-2110).
sample_tokens#
if self.execute_model_state is None:
# The prior execute_model call must have failed.
return None
input_batch = self.execute_model_state.input_batch
attn_metadata = self.execute_model_state.attn_metadata
slot_mappings_by_layer = self.execute_model_state.slot_mappings_by_layer
hidden_states = self.execute_model_state.hidden_states
aux_hidden_states = self.execute_model_state.aux_hidden_states
dp_sync_state = self.execute_model_state.dp_sync_state
finished_req_ids = self.execute_model_state.finished_req_ids
ec_connector_output = self.execute_model_state.ec_connector_output
cudagraph_stats = self.execute_model_state.cudagraph_stats
num_spec_tokens = self.execute_model_state.num_spec_tokens_to_schedule
self.execute_model_state = None
if not self.is_last_pp_rank:
# Non-last PP rank: hidden_states is None because this rank produced- Lines 2117-2119. No stored state means
execute_modelfailed; returnNone. - Line 2131. The state is used once and cleared.
- Line 2133. A non-last PP rank only receives the sampled tokens from the last rank and updates its counters (lines 2134-2153).
On the last rank: sample() (line 2163), prompt logprobs (lines 2176-2192), the ModelRunnerOutput shell with sampled_token_ids=None (lines 2195-2204), the AsyncOutput that starts the copy (lines 2210-2218), postprocess_sampled (lines 2249-2255), and the speculator (lines 2257-2293). It returns the AsyncOutput (line 2311).
prepare_inputs#
# Prepare positions and seq_lens.
prepare_pos_seq_lens(
idx_mapping,
query_start_loc,
self.req_states.num_computed_tokens.gpu,
self.input_buffers.positions,
self.input_buffers.seq_lens,
)
seq_lens = self.input_buffers.seq_lens[:num_reqs_padded]
# Some input token ids are directly read from the last sampled tokens
# and draft tokens. Also, get the logits indices to sample tokens from.
logits_indices = combine_sampled_and_draft_tokens(
self.input_buffers.input_ids,
idx_mapping,
self.req_states.last_sampled_tokens,
query_start_loc,
seq_lens,
self.req_states.prefill_len.gpu,
self.req_states.draft_tokens,
cu_num_logits,
total_num_logits,
self.model_state.num_new_sampled_tokens_per_step,
)Two of the four GPU steps: positions and sequence lengths, then the input tokens from the last sampled tokens and the drafts. The returned logits_indices select the rows for the sampler. The method ends with an InputBatch of more than 30 fields (vllm/v1/worker/gpu/model_runner.py:1532-1574).
capture_model#
start_time = time.perf_counter()
with freeze_gc_for_cudagraph_capture():
torch.accelerator.empty_cache()
start_free_gpu_memory = torch.accelerator.get_memory_info()[0]
with self.maybe_setup_dummy_loras(self.lora_config):
if capture_encoder:
self.model_state.encoder_runner.capture()
if capture_decoder:
input_buffers = self.input_buffers
if self.pcp_manager is not None:
input_buffers = self.pcp_manager.input_buffers
self.cudagraph_manager.capture(
self.model,
self.model_state,
input_buffers,
self.intermediate_tensors,
self.block_tables,
self.attn_groups,
self.kv_cache_config,
pcp_manager=self.pcp_manager,
has_lora=self.lora_config is not None,
use_aux_hidden_state_outputs=self.use_aux_hidden_state_outputs,
lora_capture_hook=create_lora_capture_hook(
self.lora_config, self
),
)
self.model_state.capture_inner_cudagraphs(
input_buffers,
self.block_tables,
self.attn_groups,
self.kv_cache_config,
)
if self.speculator is not None:
with use_workspace_lane(self._draft_workspace_lane):
self.speculator.capture()- Line 1092. Garbage collection is frozen during capture.
- Lines 1097-1098. A multimodal model can capture its encoder too.
- Lines 1104-1118. The decoder graphs: PIECEWISE and FULL descriptors (torch.compile and CUDA graphs in vLLM).
- Lines 1119-1127. Model-state graphs and the speculator's graphs.
The method returns the graph memory and logs "Graph capturing finished in ... secs, took ... GiB" (vllm/v1/worker/gpu/model_runner.py:1144-1153).
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveA map of the file by line ranges
| Lines | Section |
|---|---|
| 1-17 | Style note: keep this file minimal (vllm/v1/worker/gpu/model_runner.py:3-17) |
| 19-197 | Imports |
| 199-383 | __init__, update_max_model_len, get_supported_tasks |
| 385-620 | Model loading, small accessors, weight reload, get_kv_cache_spec |
| 622-833 | initialize_kv_cache, _init_kv_zero_meta |
| 835-1071 | Dummy runs, profiling, CUDA graph memory profile |
| 1073-1153 | capture_model |
| 1155-1291 | Request state updates |
| 1293-1613 | Batch gathering, prepare_inputs, prepare_attn, dummy attention inputs |
| 1615-1739 | sample, postprocess_sampled, EC connector helper |
| 1741-2110 | execute_model |
| 2112-2366 | sample_tokens, take_draft_token_ids, pool, postprocess_num_computed_tokens |
| 2368-2437 | shutdown, EPLB properties, pcp_manager_cls |
| 2439-2483 | ExecuteModelState, BatchReqState, sort_batch_req_ids |
Deep diveData flow: types and shapes
| Value | Type, shape | Where |
|---|---|---|
scheduler_output | SchedulerOutput, from the engine core | vllm/v1/worker/gpu/model_runner.py:1742-1745 |
idx_mapping | int64 [num_reqs], GPU | vllm/v1/worker/gpu/model_runner.py:1392-1395 |
query_start_loc | int32 [num_reqs_padded + 1], persistent GPU buffer | vllm/v1/worker/gpu/model_runner.py:1455-1467 |
input_ids, positions | int32 / int64 [num_tokens_after_padding] | vllm/v1/worker/gpu/input_batch.py:25-26 |
logits_indices | int64 [total_num_logits] | vllm/v1/worker/gpu/input_batch.py:486 |
| block tables | tuple of int32 [num_reqs_padded, max_num_blocks], one per KV cache group | vllm/v1/worker/gpu/model_runner.py:1584-1588 |
| slot mappings | int64 [num_groups, num_tokens_padded] | vllm/v1/worker/gpu/model_runner.py:1589-1596 |
attn_metadata | dict[layer name, metadata] | vllm/v1/worker/gpu/model_runner.py:1940-1951 |
hidden_states | [num_tokens_after_padding, hidden_size] | vllm/v1/worker/gpu/model_runner.py:2074-2081 |
result of sample_tokens | AsyncOutput; after get_output(), ModelRunnerOutput with sampled_token_ids: list[list[int]] | vllm/v1/worker/gpu/async_utils.py:173-184 |
Deep diveProcesses, threads and streams
- One thread calls the runner: the engine core main thread (uni executor) or the worker main thread (multiproc executor). The runner has no threads of its own.
- Two CUDA streams. The main stream, cached as
main_stream(vllm/v1/worker/gpu/model_runner.py:601-604), andoutput_copy_streamfor the copy to the CPU (line 235). The structured-output worker uses its own stream for the bitmask copy (vllm/v1/worker/gpu/structured_outputs.py:67). - The waiter.
AsyncOutput.get_output()is called later, outside this file (The model runner (Model Runner V2), Step 9).
Deep diveError paths
| Situation | Result | Where |
|---|---|---|
skip_attn outside a profile run | ValueError | vllm/v1/worker/gpu/model_runner.py:850-853 |
a dummy run above max_num_batched_tokens | AssertionError | vllm/v1/worker/gpu/model_runner.py:878-881 |
sample_tokens after a failed execute_model | returns None | vllm/v1/worker/gpu/model_runner.py:2117-2119 |
capture with mode NONE and no encoder graph | WARNING "Skipping encoder and decoder CUDA graph capture", returns 0 | vllm/v1/worker/gpu/model_runner.py:1081-1087 |
FULL capture with skip_attn | AssertionError: metadata needed | vllm/v1/worker/gpu/model_runner.py:1877-1881 |
batch-sharded sampling without compute_logits_local | WARNING, feature off | vllm/v1/worker/gpu/model_runner.py:447-461 |
Deep diveEnvironment variables
| Variable | Default | Read at | Effect |
|---|---|---|---|
VLLM_MOE_SKIP_PADDING | 1 (vllm/envs.py:1640) | vllm/v1/worker/gpu/model_runner.py:1386 | Marks padding rows in is_padding. |
VLLM_USE_BREAKABLE_CUDAGRAPH | off unless set or auto-enabled (vllm/envs.py:777-779) | vllm/v1/worker/gpu/model_runner.py:746-748 | Piecewise capture without a compiled submodule. |
VLLM_USE_V2_MODEL_RUNNER | unset (vllm/envs.py:2147-2149) | vllm/config/vllm.py:730-732 | Selects this file (or the V1 fallback). |
Related concepts#
- The model runner (Model Runner V2): the design of MRV2, with a worked example.
- The forward context: how layers find per-step data: how the layers get the metadata set in
execute_model. - Attention backends: backend, metadata builder and implementation: the builders called from the model state.
- torch.compile and CUDA graphs in vLLM: the modes and the dispatch rule.
- Async scheduling: overlap the CPU and the GPU: why the two calls never wait.
Check yourself#
- 1Which method returns theeasy
AsyncOutput, and what doesexecute_modelreturn on the last PP rank?Show answer
sample_tokensreturns it (vllm/v1/worker/gpu/model_runner.py:2311).execute_modelreturnsNoneand keeps its results inexecute_model_state(vllm/v1/worker/gpu/model_runner.py:2090-2110). - 2Where is the CUDA graph mode of the run finally decided, and with which inputs?medium
Show answer
In
initialize_kv_cache, byresolve_cudagraph_mode_and_sizeswith the weakest attention support, its backend name,decode_query_len, the KV cache config andpiecewise_capture_available(vllm/v1/worker/gpu/model_runner.py:754-763). - 3Why doesmedium
sample_tokenscreate theAsyncOutputbeforepostprocess_sampled?Show answer
So the copy event is recorded first and the copy to the CPU does not wait for the post-processing kernels (
vllm/v1/worker/gpu/model_runner.py:2244-2248). - 4A dummy run for the profile useshard
skip_attn=True. Why can a FULL capture not do the same?Show answer
A FULL graph records the attention kernels too, so it needs real attention metadata at capture; the code asserts this (
vllm/v1/worker/gpu/model_runner.py:1877-1881). The profile run only measures memory and may skip attention (vllm/v1/worker/gpu/model_runner.py:1022-1027).