vLLM Explained

"v1/worker/gpu/model_runner.py: Model Runner V2"

AdvancedCode mapabout 8 min read + 2 min deep dives

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.

Filevllm/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 classGPUModelRunner(LoRAModelRunnerMixin) (vllm/v1/worker/gpu/model_runner.py:199).
Runs inEach 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 byWorker in v1/worker/gpu_worker.py, which builds it when use_v2_model_runner is true (vllm/v1/worker/gpu_worker.py:567-580).
Callsstates.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 fileKernels 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#

Figure: one step of this file over time, and who waits for its output.
Open full size ↗

Who calls it#

What it calls#

Key classes and functions#

NameLinesRole
__init__vllm/v1/worker/gpu/model_runner.py:200-368Configs, the copy stream, RequestState, speculator, input buffers, LoRA state.
load_modelvllm/v1/worker/gpu/model_runner.py:385-555Load the model, pick the model state, build the samplers.
initialize_kv_cachevllm/v1/worker/gpu/model_runner.py:622-823Attention groups, block tables, CUDA graph mode and manager, KV cache tensors.
_dummy_run, profile_runvllm/v1/worker/gpu/model_runner.py:835-1040Fake batches through execute_model for profiling and warm-up.
capture_modelvllm/v1/worker/gpu/model_runner.py:1073-1153Capture the encoder and decoder CUDA graphs; log time and memory.
finish_requests, add_requests, update_requestsvllm/v1/worker/gpu/model_runner.py:1173-1291Apply the scheduler's plan to the persistent state.
gather_batch_req_statevllm/v1/worker/gpu/model_runner.py:1293-1373Sort the batch, map to slots, find prefills and the uniform decode count.
prepare_inputsvllm/v1/worker/gpu/model_runner.py:1375-1575Build the InputBatch with GPU kernels.
prepare_attnvllm/v1/worker/gpu/model_runner.py:1577-1597Gather block tables, compute slot mappings.
sample, postprocess_sampledvllm/v1/worker/gpu/model_runner.py:1615-1730Logits, bitmask, sampler; then write the new tokens into the state.
execute_modelvllm/v1/worker/gpu/model_runner.py:1741-2110The forward half of a step.
sample_tokensvllm/v1/worker/gpu/model_runner.py:2112-2311The sampling half; returns an AsyncOutput.
poolvllm/v1/worker/gpu/model_runner.py:2316-2358The pooling replacement of sample_tokens.
shutdownvllm/v1/worker/gpu/model_runner.py:2368-2407Free weights, KV caches and graphs.
ExecuteModelState, BatchReqStatevllm/v1/worker/gpu/model_runner.py:2439-2468Hand-over from execute_model to sample_tokens; sorted batch state.
sort_batch_req_idsvllm/v1/worker/gpu/model_runner.py:2471-2483Batch 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
                )
  1. Line 398. get_model_loader picks the loader for --load-format ("Weight loading: from checkpoint files to GPU parameters").
  2. Lines 402-406. The loader builds the model and loads the weights. JIT warm-up providers chosen during the build are recorded.
  3. 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)
  1. Lines 1806-1821. Pick the CUDA graph descriptor; with DP > 1 the ranks agree on one padded size.
  2. Lines 1823-1826. If every DP rank has zero tokens, return an empty output.
  3. 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
  1. Lines 2117-2119. No stored state means execute_model failed; return None.
  2. Line 2131. The state is used once and cleared.
  3. 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()
  1. Line 1092. Garbage collection is frozen during capture.
  2. Lines 1097-1098. A multimodal model can capture its encoder too.
  3. Lines 1104-1118. The decoder graphs: PIECEWISE and FULL descriptors (torch.compile and CUDA graphs in vLLM).
  4. 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 dive

A map of the file by line ranges

LinesSection
1-17Style note: keep this file minimal (vllm/v1/worker/gpu/model_runner.py:3-17)
19-197Imports
199-383__init__, update_max_model_len, get_supported_tasks
385-620Model loading, small accessors, weight reload, get_kv_cache_spec
622-833initialize_kv_cache, _init_kv_zero_meta
835-1071Dummy runs, profiling, CUDA graph memory profile
1073-1153capture_model
1155-1291Request state updates
1293-1613Batch gathering, prepare_inputs, prepare_attn, dummy attention inputs
1615-1739sample, postprocess_sampled, EC connector helper
1741-2110execute_model
2112-2366sample_tokens, take_draft_token_ids, pool, postprocess_num_computed_tokens
2368-2437shutdown, EPLB properties, pcp_manager_cls
2439-2483ExecuteModelState, BatchReqState, sort_batch_req_ids
Deep dive

Data flow: types and shapes

ValueType, shapeWhere
scheduler_outputSchedulerOutput, from the engine corevllm/v1/worker/gpu/model_runner.py:1742-1745
idx_mappingint64 [num_reqs], GPUvllm/v1/worker/gpu/model_runner.py:1392-1395
query_start_locint32 [num_reqs_padded + 1], persistent GPU buffervllm/v1/worker/gpu/model_runner.py:1455-1467
input_ids, positionsint32 / int64 [num_tokens_after_padding]vllm/v1/worker/gpu/input_batch.py:25-26
logits_indicesint64 [total_num_logits]vllm/v1/worker/gpu/input_batch.py:486
block tablestuple of int32 [num_reqs_padded, max_num_blocks], one per KV cache groupvllm/v1/worker/gpu/model_runner.py:1584-1588
slot mappingsint64 [num_groups, num_tokens_padded]vllm/v1/worker/gpu/model_runner.py:1589-1596
attn_metadatadict[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_tokensAsyncOutput; after get_output(), ModelRunnerOutput with sampled_token_ids: list[list[int]]vllm/v1/worker/gpu/async_utils.py:173-184
Deep dive

Processes, threads and streams

Deep dive

Error paths

SituationResultWhere
skip_attn outside a profile runValueErrorvllm/v1/worker/gpu/model_runner.py:850-853
a dummy run above max_num_batched_tokensAssertionErrorvllm/v1/worker/gpu/model_runner.py:878-881
sample_tokens after a failed execute_modelreturns Nonevllm/v1/worker/gpu/model_runner.py:2117-2119
capture with mode NONE and no encoder graphWARNING "Skipping encoder and decoder CUDA graph capture", returns 0vllm/v1/worker/gpu/model_runner.py:1081-1087
FULL capture with skip_attnAssertionError: metadata neededvllm/v1/worker/gpu/model_runner.py:1877-1881
batch-sharded sampling without compute_logits_localWARNING, feature offvllm/v1/worker/gpu/model_runner.py:447-461
Deep dive

Environment variables

VariableDefaultRead atEffect
VLLM_MOE_SKIP_PADDING1 (vllm/envs.py:1640)vllm/v1/worker/gpu/model_runner.py:1386Marks padding rows in is_padding.
VLLM_USE_BREAKABLE_CUDAGRAPHoff unless set or auto-enabled (vllm/envs.py:777-779)vllm/v1/worker/gpu/model_runner.py:746-748Piecewise capture without a compiled submodule.
VLLM_USE_V2_MODEL_RUNNERunset (vllm/envs.py:2147-2149)vllm/config/vllm.py:730-732Selects this file (or the V1 fallback).

Check yourself#

  1. 1
    Which method returns the AsyncOutput, and what does execute_model return on the last PP rank?
    easy
    Show answer

    sample_tokens returns it (vllm/v1/worker/gpu/model_runner.py:2311). execute_model returns None and keeps its results in execute_model_state (vllm/v1/worker/gpu/model_runner.py:2090-2110).

  2. 2
    Where is the CUDA graph mode of the run finally decided, and with which inputs?
    medium
    Show answer

    In initialize_kv_cache, by resolve_cudagraph_mode_and_sizes with the weakest attention support, its backend name, decode_query_len, the KV cache config and piecewise_capture_available (vllm/v1/worker/gpu/model_runner.py:754-763).

  3. 3
    Why does sample_tokens create the AsyncOutput before postprocess_sampled?
    medium
    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).

  4. 4
    A dummy run for the profile uses skip_attn=True. Why can a FULL capture not do the same?
    hard
    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).