vLLM Explained

"forward_context.py: the per-step context of the forward pass"

AdvancedCode mapabout 4 min read + 2 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

Purpose#

vllm/forward_context.py holds one global object during each forward pass, so that layers and CUDA graph wrappers can read per-step data without function arguments.

Filevllm/forward_context.py, 392 lines.
Main classForwardContext (vllm/forward_context.py:140-202); the entry point is set_forward_context (vllm/forward_context.py:277-392).
Runs inEach worker process (or the engine core with the uni executor), in the thread that calls the model. The context is a module-level variable, not a thread-local (vllm/forward_context.py:205).
Called byModel Runner V2 for eager and PIECEWISE steps (vllm/v1/worker/gpu/model_runner.py:2041-2053) and for CUDA graph capture (vllm/v1/worker/gpu/cudagraph_utils.py:762-771); the speculators (vllm/v1/worker/gpu/spec_decode/standalone_ar/speculator.py:140) and the KV connector (vllm/v1/worker/gpu/kv_connector.py:126) too.
Read byThe attention ops through get_attention_context (vllm/model_executor/layers/attention/attention.py:705), CUDAGraphWrapper (vllm/compilation/cuda_graph.py:242-244), MoE layers and others.
Not in this fileThe static dict of layers (in CompilationConfig, vllm/config/compilation.py:757-760); building the attention metadata (Attention backends: backend, metadata builder and implementation).

The page The forward context: how layers find per-step data explains the idea with the attention layer as the example.

Where it sits#

Figure: where this module's ForwardContext comes from and who reads it.
Open full size ↗

Who calls it#

What it calls#

Key classes and functions#

NameLinesRole
module globalsvllm/forward_context.py:22-26Batch-size logging settings, read from env at import.
BatchDescriptorvllm/forward_context.py:29-57Frozen key of a piecewise CUDA graph: tokens, requests, uniform, LoRA.
_compute_sp_num_tokensvllm/forward_context.py:60-77Tokens per rank for sequence-parallel MoE.
DPMetadatavllm/forward_context.py:80-137Token counts across DP ranks for MoE dispatch.
ForwardContextvllm/forward_context.py:140-202All per-step fields; checks the CUDA graph mode.
get_forward_contextvllm/forward_context.py:208-214Return the current context; assert that one is set.
is_forward_context_available, in_piecewise_cudagraphvllm/forward_context.py:217-227Safe checks for code that may run outside a forward pass.
create_forward_contextvllm/forward_context.py:230-259Build the dataclass; attach the static dict.
override_forward_contextvllm/forward_context.py:262-274Swap the global, restore it on exit.
set_forward_contextvllm/forward_context.py:277-392DP metadata, descriptor, platform extras, install, timing log.

Walk through the main methods#

ForwardContext#

@dataclass
class ForwardContext:
    # copy from vllm_config.compilation_config.static_forward_context
    no_compile_layers: dict[str, Any]
    attn_metadata: dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]]
    slot_mapping: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]]
    """
    Type Dict[str, AttentionMetadata] for v1, map from layer_name of each
    attention layer to its attention metadata
    Type List[Dict[str, AttentionMetadata]] for DBO. List of size two, one
    for each microbatch.
    Set dynamically for each forward pass
    """
    # set dynamically for each forward pass
    dp_metadata: DPMetadata | None = None
    # determine the cudagraph style at runtime to be FULL, PIECEWISE, or NONE.
    # by default NONE, no cudagraph is used.
    cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE
    batch_descriptor: BatchDescriptor | None = None

    ubatch_slices: UBatchSlices | None = None

    # Boolean mask over the token axis: True for padding rows that are not real
    # tokens. Consumers can use it to skip work for padded tokens. None when
    # the producer does not set it.
    is_padding: torch.Tensor | None = None

    # If True, bypass the compiled model call, e.g. by using .forward() directly
    skip_compiled: bool = False
  1. Line 143. no_compile_layers: the comment says "copy", but create_forward_context passes the same dict object (vllm/forward_context.py:248).
  2. Lines 144-145. attn_metadata and slot_mapping: dicts by layer name, or a list of two dicts for dual-batch overlap.
  3. Lines 157-158. The CUDA graph mode and the descriptor for the wrappers.
  4. Line 165. is_padding: which token rows are padding.
  5. Line 168. skip_compiled: call forward() directly, bypassing the compiled model (vllm/compilation/decorators.py:540-544).

set_forward_context#

    dp_metadata: DPMetadata | None = None
    if (
        (
            vllm_config.parallel_config.data_parallel_size > 1
            or vllm_config.parallel_config.use_sequence_parallel_moe
        )
        and vllm_config.parallel_config.is_moe_model is not False
        and (attn_metadata is not None or num_tokens is not None)
    ):
        # If num_tokens_across_dp hasn't already been initialized, then
        # initialize it here. Both DP padding and Microbatching will be
        # disabled.
        if (
            num_tokens_across_dp is None
            and vllm_config.parallel_config.data_parallel_size > 1
        ):
            assert ubatch_slices is None
            assert num_tokens is not None
            _, num_tokens_across_dp, _ = coordinate_batch_across_dp(
                num_tokens_unpadded=num_tokens,
                parallel_config=vllm_config.parallel_config,
                allow_microbatching=False,
            )
            assert num_tokens_across_dp is not None
        elif num_tokens_across_dp is None:
            assert num_tokens is not None
            num_tokens_across_dp = torch.tensor([num_tokens], dtype=torch.int32)
        dp_metadata = DPMetadata.make(
            vllm_config.parallel_config, num_tokens or 0, num_tokens_across_dp
        )

    # Preserve the batch size before model-level sequence parallelism.
    if num_tokens is not None:
        batch_descriptor = batch_descriptor or BatchDescriptor(num_tokens=num_tokens)

    additional_kwargs = current_platform.set_additional_forward_context(
        attn_metadata=attn_metadata,
        vllm_config=vllm_config,
        dp_metadata=dp_metadata,
        num_tokens=num_tokens,
        num_tokens_across_dp=num_tokens_across_dp,
        cudagraph_runtime_mode=cudagraph_runtime_mode,
        batch_descriptor=batch_descriptor,
        ubatch_slices=ubatch_slices,
    )

    forward_context = create_forward_context(
        attn_metadata,
        vllm_config,
        dp_metadata,
        cudagraph_runtime_mode,
        batch_descriptor,
        ubatch_slices,
        slot_mapping,
        additional_kwargs,
        skip_compiled,
        is_padding=is_padding,
    )

    try:
        with override_forward_context(forward_context):
            yield
  1. Lines 300-307. DP metadata only for MoE models with DP > 1 or sequence-parallel MoE.
  2. Lines 311-322. Without counts from the caller, a collective computes them; the assert says micro-batching must be off.
  3. Lines 331-332. A default descriptor with only num_tokens.
  4. Lines 345-356. Build the context.
  5. Line 359. Install it; the finally block of override_forward_context restores the previous one.

get_forward_context#

def get_forward_context() -> ForwardContext:
    """Get the current forward context."""
    assert _forward_context is not None, (
        "Forward context is not set. "
        "Please use `set_forward_context` to set the forward context."
    )
    return _forward_context

An assert, not an exception class: with python -O the check disappears and the function returns None. Code that may run outside a forward pass uses is_forward_context_available() first, like CUDAGraphWrapper (vllm/compilation/cuda_graph.py:236-240).

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

Data flow: what the runner passes in one PIECEWISE step

ArgumentValue from Model Runner V2
attn_metadatadict of layer name to backend metadata (vllm/v1/worker/gpu/model_runner.py:1940-1951)
num_tokensinput_batch.num_tokens_after_padding, the padded size
cudagraph_runtime_modebatch_desc.cg_mode: PIECEWISE or NONE
num_tokens_across_dpfrom the DP sync, or None with DP = 1
batch_descriptorBatchDescriptor(num_tokens=padded, has_lora, num_active_loras) (vllm/v1/worker/gpu/model_runner.py:2035-2039)
slot_mappingdict of layer name to the slot row of its KV cache group
is_paddingbool tensor [padded tokens]

Example: a batch of 300 tokens runs as PIECEWISE with 304 padded tokens. The descriptor is BatchDescriptor(num_tokens=304, has_lora=False, num_active_loras=0), which is also the key that each piece wrapper captured at start-up (derived from torch.compile and CUDA graphs in vLLM).

Deep dive

BatchDescriptor fields and the uniform flag

The descriptor has five fields (vllm/forward_context.py:36-56). Model Runner V2 sets only num_tokens, has_lora and num_active_loras; num_reqs stays None and uniform stays False, because PIECEWISE graphs do not depend on them. The FULL graph choice uses a separate class, BatchExecutionDescriptor, in gpu/cudagraph_utils.py (vllm/v1/worker/gpu/cudagraph_utils.py:70-85).

Deep dive

Threads and processes

_forward_context is one module-level variable (vllm/forward_context.py:205). Each worker process has its own copy. Inside a process, the model runs in one thread at a time. Dual-batch overlap uses threads that hand off one compute stream (vllm/v1/worker/gpu/attn_utils.py:301-303); it passes the list form of attn_metadata and slot_mapping, one dict per micro-batch (vllm/forward_context.py:146-150).

Deep dive

Error paths and environment variables

SituationResultWhere
no context is setAssertionError "Forward context is not set."vllm/forward_context.py:210-213
a mode that is not NONE, PIECEWISE or FULLAssertionError "Invalid cudagraph runtime mode"vllm/forward_context.py:199-202
DP counts that do not match this rankAssertionError in DPMetadata.makevllm/forward_context.py:104-106
VariableDefaultRead atEffect
VLLM_LOG_BATCHSIZE_INTERVAL-1 (off) (vllm/envs.py:1445-1447)import, vllm/forward_context.py:22-25>= 0: synchronize after each forward pass that has attention metadata, and log the median time per batch size at this interval in seconds (vllm/forward_context.py:295).

Check yourself#

  1. 1
    Which function do layers call to read the current step data?
    easy
    Show answer

    get_forward_context(), which returns the module-level _forward_context (vllm/forward_context.py:208-214).

  2. 2
    Is no_compile_layers a copy of static_forward_context?
    medium
    Show answer

    No. The field comment says "copy", but create_forward_context passes the dict object itself (vllm/forward_context.py:142-143, vllm/forward_context.py:248).

  3. 3
    Why can VLLM_LOG_BATCHSIZE_INTERVAL=0 make a server slower, and when is the setting read?
    hard
    Show answer

    When it is 0 or more, every forward pass inside set_forward_context with attention metadata ends with current_platform.synchronize() (vllm/forward_context.py:295, vllm/forward_context.py:363-370), which removes the CPU/GPU overlap. In Model Runner V2 these are the eager and PIECEWISE steps. The module reads the variable once, at import (vllm/forward_context.py:22-25).