"model_executor/layers/attention/attention.py: the Attention layer and its custom ops"
Code version: vLLM @ 3e44c0d · 2026-10-22
Purpose#
vllm/model_executor/layers/attention/attention.py is the bridge between a model file and an attention backend: the model calls Attention, and this file finds the per-step data and calls the backend's impl.
| File | vllm/model_executor/layers/attention/attention.py, 803 lines. |
|---|---|
| Main class | Attention(nn.Module, AttentionLayerBase) (vllm/model_executor/layers/attention/attention.py:229-239). |
| Runs in | Each worker. The constructor runs once at model build; forward and the ops run in every eager or PIECEWISE step, between the compiled pieces. |
| Called by | Model files, for example LlamaAttention (vllm/model_executor/models/llama.py:212-222); subclasses such as EncoderOnlyAttention; Model Runner V2 for the KV cache spec through get_kv_cache_spec (vllm/v1/worker/gpu/attn_utils.py:138-155). |
| Calls | The selector get_attn_backend, the backend's impl class, the quantization config, and the forward context. |
| Not in this file | MLA attention (mla_attention.py), backend classes ("v1/attention/backend.py and selector.py: the backend interfaces and the selector"), kernels. |
Where it sits#
Who calls it#
- Model files build one
Attentionper decoder layer and callself.attn(q, k, v)(How a vLLM model file is written (llama.py)). - The runner reads the layers from the static dict to build the KV cache spec and the attention groups (
vllm/v1/worker/gpu/attn_utils.py:228-234). - The compiled graph calls
torch.ops.vllm.unified_kv_cache_updateandtorch.ops.vllm.unified_attention_with_outputas opaque nodes.
What it calls#
get_attn_backendin its constructor (vllm/model_executor/layers/attention/attention.py:349-360); see How vLLM chooses an attention backend.impl_cls(...)of the backend (vllm/model_executor/layers/attention/attention.py:419-432).resolve_quant_methodfor KV cache scales (vllm/model_executor/layers/attention/attention.py:194-198).get_forward_context()insideget_attention_context(vllm/model_executor/layers/attention/attention.py:705).
Key classes and functions#
| Name | Lines | Role |
|---|---|---|
validate_kv_sharing_target | vllm/model_executor/layers/attention/attention.py:61-91 | Check a KV sharing target: earlier layer, same type. |
should_load_quant_weights, set_default_quant_scales, _init_kv_cache_quant | vllm/model_executor/layers/attention/attention.py:94-226 | KV cache scales q/k/v/prob; FP8 checkpoint scales. |
_largest_kernel_block_within | vllm/model_executor/layers/attention/attention.py:101-130 | Block size for sliding-window layers that share a padded page. |
Attention.__init__ | vllm/model_executor/layers/attention/attention.py:241-488 | KV dtype, backend, impl, registration, quantization. |
Attention.forward | vllm/model_executor/layers/attention/attention.py:490-581 | Reshape q/k/v, allocate output, call the two ops. |
Attention.get_kv_cache_spec | vllm/model_executor/layers/attention/attention.py:608-679 | FullAttentionSpec, SlidingWindowSpec or None for encoder attention. |
get_attention_context | vllm/model_executor/layers/attention/attention.py:682-723 | Layer name → metadata, layer, KV cache, slot mapping. |
unified_kv_cache_update (+ fake, registration) | vllm/model_executor/layers/attention/attention.py:726-764 | Write k/v into the cache; return a dummy for ordering. |
unified_attention_with_output (+ registration) | vllm/model_executor/layers/attention/attention.py:767-803 | Call impl.forward with the cache and metadata. |
Walk through the main methods#
Attention.__init__: data type and backend#
vllm_config = get_current_vllm_config()
if cache_config is not None:
kv_cache_dtype = cache_config.cache_dtype
else:
kv_cache_dtype = "auto"
# llm-compressor models declare an FP8 KV-cache scheme in their
# checkpoint config. Honor it only when the user did not explicitly
# pick a kv_cache_dtype; an explicit choice (e.g. bfloat16) must win.
# The "auto" case is normally resolved upstream in
# resolve_kv_cache_dtype_string, but we re-apply here defensively in
# case anything bypassed that path.
kv_cache_scheme = getattr(quant_config, "kv_cache_scheme", None)
if kv_cache_scheme is not None and kv_cache_dtype == "auto":
kv_cache_dtype = "fp8"
if cache_config is not None:
cache_config.cache_dtype = "fp8"
# Check if per-head quant scales are required based on kv_cache_scheme
use_per_head_quant_scales = (
kv_cache_scheme is not None
and kv_cache_scheme.get("strategy") == "attn_head"
)- Lines 276-279. The KV cache dtype comes from
--kv-cache-dtype, else"auto". - Lines 287-291. A checkpoint with an FP8
kv_cache_schemeturns"auto"into"fp8". The comment says this case "is normally resolved upstream". - Lines 294-297. A scheme with
strategy == "attn_head"needs per-head scales; the selector checks backend support for it.
Then the constructor stores the head counts and asserts that num_heads divides by num_kv_heads (vllm/model_executor/layers/attention/attention.py:327-338). It selects the backend unless the caller passed one (lines 349-362). It rejects options the backend cannot do, for example use_alibi_sqrt (vllm/model_executor/layers/attention/attention.py:363-370).
Attention.__init__: impl, registration, placeholder#
impl_cls = self.attn_backend.get_impl_cls()
self.impl = impl_cls( # type: ignore[assignment] # impl_cls always returns an AttentionImpl subclass
num_heads,
head_size,
scale,
num_kv_heads,
alibi_slopes,
sliding_window,
kv_cache_dtype,
logits_soft_cap,
attn_type,
kv_sharing_target_layer_name,
**extra_impl_args,
)
self.backend = AttentionBackendEnum[self.attn_backend.get_name()]
self.dtype = dtype
# For cuda-alike (CUDA and ROCM) and cpu platforms, we control how
# torch.compile works by registering the attention as one giant
# opaque custom op. For other platforms, we directly call them
# and let torch.compile handle them.
self.use_direct_call = not current_platform.opaque_attention_op()
compilation_config = vllm_config.compilation_config
if prefix in compilation_config.static_forward_context:
raise ValueError(f"Duplicate layer name: {prefix}")
compilation_config.static_forward_context[prefix] = self
self.attn_type = attn_type
if kv_sharing_target_layer_name is not None:
validate_kv_sharing_target(
prefix,
kv_sharing_target_layer_name,
compilation_config.static_forward_context,
)
self.kv_sharing_target_layer_name = kv_sharing_target_layer_name
# Set by models that write KV before calling attention with key=None.
self.kv_cache_update_bypass = False
# Gemma4: clamp mm_prefix bidirectional ranges by the sliding window
# (read by the Triton backend impl). Default False for all other models.
self.mm_prefix_clamp_sliding_window = mm_prefix_clamp_sliding_window
# use a placeholder kv cache tensor during init, which will be replaced
# by bind_kv_cache
# this variable will not be accessed if use_direct_call is True
self.kv_cache = torch.tensor([])
# Initialize KV cache quantization attributes
_init_kv_cache_quant(self, quant_config, prefix)- Lines 419-432. The impl gets ten positional arguments plus backend extras.
- Line 440.
use_direct_callis false on CUDA, ROCm, CPU and XPU, which want the opaque op. - Lines 443-445. Register in
static_forward_contextby prefix; a duplicate raises. - Lines 448-453. Validate a KV sharing target.
- Line 464. An empty placeholder until the runner binds the real cache view. The comment says the attribute is not read with direct calls; in this file the direct path also reads it, through
get_attention_context(lines 553, 716-717). - Line 467. KV cache quantization scales.
Attention.forward#
if output_shape is None:
# Handle both 2D [num_tokens, hidden] and
# 3D [num_tokens, heads, head_dim] query
num_tokens = query.shape[0]
output_shape = torch.Size((num_tokens, self.num_heads * self.head_size_v))
output = torch.empty(output_shape, dtype=output_dtype, device=query.device)
hidden_size = output_shape[-1]
# Reshape the query, key, and value tensors.
# NOTE(woosuk): We do this outside the custom op to minimize the
# CPU overheads from the non-CUDA-graph regions.
query = query.view(-1, self.num_heads, self.head_size)
output = output.view(-1, self.num_heads, self.head_size_v)
if key is not None:
key = key.view(-1, self.num_kv_heads, self.head_size)
if value is not None:
value = value.view(-1, self.num_kv_heads, self.head_size_v)
kv_cache_dummy_dep = None
if self.use_direct_call:
# Skip this if sharing KV cache with an earlier attention layer.
if (
not self.attn_backend.forward_includes_kv_cache_update
and self.kv_sharing_target_layer_name is None
and key is not None
and value is not None
):
kv_cache_dummy_dep = unified_kv_cache_update(
key, value, self.layer_name
)
unified_attention_with_output(
query,
key,
value,
output,
self.layer_name,
kv_cache_dummy_dep=kv_cache_dummy_dep,
)- Lines 525-530. Allocate the output:
[num_tokens, num_heads × head_size_v]unless the model gives a shape. - Lines 533-540. Reshape q, k and v to
[tokens, heads, head_size]outside the op, "to minimize the CPU overheads from the non-CUDA-graph regions". - Lines 542-552. The direct-call path; the cache write runs only if the backend does not write in its forward and the layer does not share another layer's cache.
- Lines 553-560. The attention call. The opaque path (lines 561-580) is the same with
torch.ops.vllm.*and an encoded name.
get_kv_cache_spec#
else:
return FullAttentionSpec(
block_size=block_size,
num_kv_heads=self.num_kv_heads,
head_size=self.head_size,
head_size_v=self.head_size_v,
dtype=self.kv_cache_torch_dtype,
kv_quant_mode=quant_mode,
)A normal decoder layer returns a FullAttentionSpec with the current block size, per-rank KV heads, head sizes and the KV dtype. Encoder and encoder-only layers return None: they keep no KV cache (vllm/model_executor/layers/attention/attention.py:616-617). A sliding-window layer returns a SlidingWindowSpec and may choose its own block size (vllm/model_executor/layers/attention/attention.py:620-670); see The hybrid KV cache: specs, groups and one manager per layer type.
get_attention_context and the two ops#
@eager_break_during_capture
@maybe_transfer_kv_layer
def unified_attention_with_output(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
output: torch.Tensor,
layer_name: LayerNameType,
output_scale: torch.Tensor | None = None,
output_block_scale: torch.Tensor | None = None,
kv_cache_dummy_dep: torch.Tensor | None = None,
) -> None:
# kv_cache_dummy_dep is not used but accepting it creates a data dependency
# that ensures torch.compile preserves ordering between KV cache update and
# attention forward.
del kv_cache_dummy_dep
layer_name = _resolve_layer_name(layer_name)
attn_metadata, self, kv_cache, _ = get_attention_context(layer_name)
self.impl.forward(
self,
query,
key,
value,
kv_cache,
attn_metadata,
output=output,
output_scale=output_scale,
output_block_scale=output_block_scale,
)
direct_register_custom_op(
op_name="unified_attention_with_output",
op_func=unified_attention_with_output,
mutates_args=["output", "output_block_scale"],
)- Lines 767-768. Two decorators:
@eager_break_during_capturemakes the op a break point of breakable CUDA graphs;@maybe_transfer_kv_layeradds the KV connector's per-layer load and save. The breakable decorator must be outermost (vllm/compilation/breakable_cudagraph.py:82-96). - Line 782. The dummy dependency is dropped; it only ordered the ops.
- Line 784. One lookup gives metadata, layer, KV cache (the slot mapping is not needed here).
- Lines 786-796. The impl call; the result lands in
output. - Lines 799-803. Registration:
mutates_args=["output", "output_block_scale"]tells the compiler which inputs change.
get_attention_context itself is explained line by line on The forward context: how layers find per-step data (Step 5).
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: shapes for Llama-3-8B, TP = 2
Per rank: 16 query heads, 4 KV heads, head size 128 (from How a vLLM model file is written (llama.py)). A step with T tokens:
| Tensor | Shape | Where |
|---|---|---|
| q in, k in, v in | [T, 2048], [T, 512], [T, 512] | from the QKV split in the model |
| q, k, v to the ops | [T, 16, 128], [T, 4, 128], [T, 4, 128] | lines 535-540 |
| output | [T, 16, 128] inside, [T, 2048] returned | lines 527-536, 581 |
| KV cache view | [num_blocks, 4, 16, 256] BF16 with FlashAttention | Attention backends: backend, metadata builder and implementation |
| slot mapping | int64 [T (padded)] | from the forward context |
Deep diveError paths
| Situation | Result | Where |
|---|---|---|
| two layers with one prefix | ValueError: Duplicate layer name | vllm/model_executor/layers/attention/attention.py:443-444 |
| heads do not divide by KV heads | AssertionError | vllm/model_executor/layers/attention/attention.py:329-331 |
| KV sharing target is wrong | ValueError with the reason | vllm/model_executor/layers/attention/attention.py:69-91 |
use_alibi_sqrt on a backend without it | ValueError | vllm/model_executor/layers/attention/attention.py:365-369 |
fp8_e5m2 KV cache with an FP8 checkpoint that has KV scales | ValueError | vllm/model_executor/layers/attention/attention.py:205-220 |
impl without do_kv_cache_update on the update path | AssertionError | vllm/model_executor/layers/attention/attention.py:737-739 |
| FlexAttention block size above the cache block size with batch invariance | ValueError | vllm/model_executor/layers/attention/attention.py:398-410 |
Deep diveInteraction with prefix caching, compile and KV transfer
- Prefix caching. With
VLLM_BATCH_INVARIANTand the FlashInfer or Triton MLA backend, the constructor turns prefix caching off and logs a warning (vllm/model_executor/layers/attention/attention.py:373-388). - torch.compile. The ops are splitting ops (torch.compile and CUDA graphs in vLLM). The query quantization in
forwarduses a plain torch op so that the compiler can fuse it (vllm/model_executor/layers/attention/attention.py:512-517). - KV transfer.
@maybe_transfer_kv_layercallswait_for_layer_loadbefore and saves after the attention, only with an active KV connector and connector metadata (vllm/model_executor/layers/attention/kv_transfer_utils.py:37-55).
Related concepts#
- The forward context: how layers find per-step data: the name lookup in detail.
- Attention backends: backend, metadata builder and implementation: what the impl and the metadata are.
- How vLLM chooses an attention backend: how the constructor's backend is chosen.
- The hybrid KV cache: specs, groups and one manager per layer type: the specs that
get_kv_cache_specreturns.
Check yourself#
- 1What doeseasy
Attention.forwardreturn, and where is the attention result written first?Show answer
It returns
output.view(-1, hidden_size)(vllm/model_executor/layers/attention/attention.py:581). The impl writes into theoutputtensor thatforwardallocated, because the op mutatesoutput(vllm/model_executor/layers/attention/attention.py:799-803). - 2When doesmedium
Attention.forwardskip the cache write op?Show answer
When the backend writes the cache inside its own forward (
forward_includes_kv_cache_update), when the layer shares another layer's cache, or when key or value isNone(vllm/model_executor/layers/attention/attention.py:564-572). - 3Which decorator must be outermost onhard
unified_attention_with_output, and why?Show answer
@eager_break_during_capture. The KV connector's wait and save calls in@maybe_transfer_kv_layermust run in the eager segment; inside a captured graph they would hang on replay (vllm/compilation/breakable_cudagraph.py:82-96).