"forward_context.py: the per-step context of the forward pass"
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.
| File | vllm/forward_context.py, 392 lines. |
|---|---|
| Main class | ForwardContext (vllm/forward_context.py:140-202); the entry point is set_forward_context (vllm/forward_context.py:277-392). |
| Runs in | Each 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 by | Model 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 by | The 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 file | The 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#
Who calls it#
GPUModelRunner.execute_model, around the model call in PIECEWISE and eager mode (FULL mode replays a graph without a context) (vllm/v1/worker/gpu/model_runner.py:2024-2053).ModelCudaGraphManager.capture, around each capture run (vllm/v1/worker/gpu/cudagraph_utils.py:754-771).- Any code inside the forward pass that needs step data:
get_forward_context().
What it calls#
coordinate_batch_across_dpwhen an MoE model with DP > 1 has no token counts yet (vllm/forward_context.py:311-322).- The platform hook
set_additional_forward_context(vllm/forward_context.py:334-343). current_platform.synchronize, only when batch-size logging is on (vllm/forward_context.py:368-370).
Key classes and functions#
| Name | Lines | Role |
|---|---|---|
| module globals | vllm/forward_context.py:22-26 | Batch-size logging settings, read from env at import. |
BatchDescriptor | vllm/forward_context.py:29-57 | Frozen key of a piecewise CUDA graph: tokens, requests, uniform, LoRA. |
_compute_sp_num_tokens | vllm/forward_context.py:60-77 | Tokens per rank for sequence-parallel MoE. |
DPMetadata | vllm/forward_context.py:80-137 | Token counts across DP ranks for MoE dispatch. |
ForwardContext | vllm/forward_context.py:140-202 | All per-step fields; checks the CUDA graph mode. |
get_forward_context | vllm/forward_context.py:208-214 | Return the current context; assert that one is set. |
is_forward_context_available, in_piecewise_cudagraph | vllm/forward_context.py:217-227 | Safe checks for code that may run outside a forward pass. |
create_forward_context | vllm/forward_context.py:230-259 | Build the dataclass; attach the static dict. |
override_forward_context | vllm/forward_context.py:262-274 | Swap the global, restore it on exit. |
set_forward_context | vllm/forward_context.py:277-392 | DP 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- Line 143.
no_compile_layers: the comment says "copy", butcreate_forward_contextpasses the same dict object (vllm/forward_context.py:248). - Lines 144-145.
attn_metadataandslot_mapping: dicts by layer name, or a list of two dicts for dual-batch overlap. - Lines 157-158. The CUDA graph mode and the descriptor for the wrappers.
- Line 165.
is_padding: which token rows are padding. - Line 168.
skip_compiled: callforward()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- Lines 300-307. DP metadata only for MoE models with DP > 1 or sequence-parallel MoE.
- Lines 311-322. Without counts from the caller, a collective computes them; the assert says micro-batching must be off.
- Lines 331-332. A default descriptor with only
num_tokens. - Lines 345-356. Build the context.
- Line 359. Install it; the
finallyblock ofoverride_forward_contextrestores 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_contextAn 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 diveData flow: what the runner passes in one PIECEWISE step
| Argument | Value from Model Runner V2 |
|---|---|
attn_metadata | dict of layer name to backend metadata (vllm/v1/worker/gpu/model_runner.py:1940-1951) |
num_tokens | input_batch.num_tokens_after_padding, the padded size |
cudagraph_runtime_mode | batch_desc.cg_mode: PIECEWISE or NONE |
num_tokens_across_dp | from the DP sync, or None with DP = 1 |
batch_descriptor | BatchDescriptor(num_tokens=padded, has_lora, num_active_loras) (vllm/v1/worker/gpu/model_runner.py:2035-2039) |
slot_mapping | dict of layer name to the slot row of its KV cache group |
is_padding | bool 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 diveBatchDescriptor 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 diveThreads 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 diveError paths and environment variables
| Situation | Result | Where |
|---|---|---|
| no context is set | AssertionError "Forward context is not set." | vllm/forward_context.py:210-213 |
| a mode that is not NONE, PIECEWISE or FULL | AssertionError "Invalid cudagraph runtime mode" | vllm/forward_context.py:199-202 |
| DP counts that do not match this rank | AssertionError in DPMetadata.make | vllm/forward_context.py:104-106 |
| Variable | Default | Read at | Effect |
|---|---|---|---|
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). |
Related concepts#
- The forward context: how layers find per-step data: the idea, with the attention lookup step by step.
- torch.compile and CUDA graphs in vLLM: how the wrappers use the mode and the descriptor.
- The model runner (Model Runner V2): where the metadata comes from.
Check yourself#
- 1Which 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). - 2Ismedium
no_compile_layersa copy ofstatic_forward_context?Show answer
No. The field comment says "copy", but
create_forward_contextpasses the dict object itself (vllm/forward_context.py:142-143,vllm/forward_context.py:248). - 3Why canhard
VLLM_LOG_BATCHSIZE_INTERVAL=0make a server slower, and when is the setting read?Show answer
When it is 0 or more, every forward pass inside
set_forward_contextwith attention metadata ends withcurrent_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).