Attention backends: backend, metadata builder and implementation
Code version: vLLM @ 3e44c0d · 2026-10-22
In one sentence
An attention backend is three classes: a description, a metadata builder that runs once per step and group, and an implementation that runs per layer.
The idea#
Attention computes softmax(QKT)V over the cached keys and values of each request (Attention basics). vLLM has many attention backends: FlashAttention, FlashInfer, Triton, FlashMLA and more. Which one a layer gets is decided when the model is built (How vLLM chooses an attention backend). This page explains what a backend is made of, and how vLLM calls it each step. Kernel code is out of scope.
Each backend has three classes, with three lifetimes:
AttentionBackend, a class with only static and class methods. It names the other two classes and answers questions: which head sizes, data types, block sizes and features it supports.AttentionMetadataBuilder, the metadata builder. The runner makes one per group of layers at start-up. Each step it turns the CommonAttentionMetadata into the backend's own attention metadata.AttentionImpl, one object per attention layer, made in the layer's constructor. Each step it writes the new keys and values into the KV cache and runs the kernel.
The split saves work. Metadata depends on the batch, not on the layer. So a 32-layer model builds it once per group and step, and all 32 layers use the same object.
An everyday comparison. Think of a train line. The timetable office (the builder) prints one timetable for all trains each day. Each driver (the impl) reads the same timetable and drives one train. The rule book (the backend class) says which trains and tracks the line can use. The comparison stops here: a builder can also say that a CUDA graph cannot replay some batches. Then vLLM changes the whole run plan at start-up.
The picture#
Read the diagram from top to bottom. Panel 1 runs once at start-up. Panel 2 runs once per KV cache group in every step. Panel 3 runs once per layer in every step, inside the forward pass.
In the code#
Step 1: the backend class names the two other classes#
Every backend implements three abstract static methods (vllm/v1/attention/backend.py:81-94). FlashAttention, for example:
@staticmethod
def get_name() -> str:
return "FLASH_ATTN" @staticmethod
def get_impl_cls() -> type["FlashAttentionImpl"]:
return FlashAttentionImpl
@staticmethod
def get_builder_cls() -> type["FlashAttentionMetadataBuilder"]:
return FlashAttentionMetadataBuilderThe rest of the class answers questions. The supports_* class methods and validate_configuration are used during selection (How vLLM chooses an attention backend, vllm/v1/attention/backend.py:279-370). Four more members matter at run time:
| Member | Default | Used for |
|---|---|---|
forward_includes_kv_cache_update | True (vllm/v1/attention/backend.py:69-70) | False means the layer calls the separate KV cache update op first (The forward context: how layers find per-step data). FlashAttention, FlashInfer and Triton set False. |
get_supported_kernel_block_sizes() | [MultipleOf(1)] (vllm/v1/attention/backend.py:75-80) | Block sizes the kernel can use; the default block size and the kernel block size come from it (vllm/v1/attention/backend.py:155-163). |
customize_spec() | no change (vllm/v1/attention/backend.py:140-152) | Lets a backend pack its KV cache in another way. The comment calls it a "temporary compatibility API". |
supported_kv_cache_layouts() | None (vllm/v1/attention/backend.py:371-375) | Physical memory layouts the kernels can read, most preferred first (Deep dive). |
Step 2: one impl per layer#
The Attention constructor makes the impl right after the backend is selected (vllm/model_executor/layers/attention/attention.py:419-432). The impl gets the per-rank head counts, the head size, the scale, the sliding window and the KV cache data type. The abstract forward has this signature:
@abstractmethod
def forward(
self,
layer: AttentionLayer,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
kv_cache: torch.Tensor,
attn_metadata: T,
output: torch.Tensor,
output_scale: torch.Tensor | None = None,
output_block_scale: torch.Tensor | None = None,
) -> torch.Tensor:
raise NotImplementedErrorquery is [num_tokens, num_heads, head_size]; the layer reshapes q, k and v before the op (vllm/model_executor/layers/attention/attention.py:533-540). The impl writes into output; it does not return a new tensor. Some backends set forward_includes_kv_cache_update = False. They also implement do_kv_cache_update(layer, key, value, kv_cache, slot_mapping), which writes the keys and values of the new tokens into their slots. For FlashAttention it is a scatter by slot (vllm/v1/attention/backends/flash_attn.py:1430-1464).
In the profile run there is no metadata. FlashAttention then only fills the output with zeros (vllm/v1/attention/backends/flash_attn.py:1224-1226).
Step 3: attention groups and builders, at start-up#
When the KV cache is initialized, init_attn_backend groups the layers. Inside each KV cache group, layers with the same backend class, the same KV cache spec and the same number of query heads form one AttentionGroup:
layer_kv_cache_spec: KVCacheSpec = kv_cache_group_spec.kv_cache_spec
if isinstance(layer_kv_cache_spec, UniformTypeKVCacheSpecs):
layer_kv_cache_spec = layer_kv_cache_spec.kv_cache_specs[layer_name]
# Split on per-rank num_heads_q so layers with different Q-head
# counts (e.g. a spec-decode draft head and its target) get separate
# metadata builders.
num_heads_q = getattr(attn_layers[layer_name], "num_heads", 0)
key = (attn_backend.full_cls_name(), layer_kv_cache_spec, num_heads_q)
if key not in group_map:
group_map[key] = AttentionGroup(
attn_backend, [layer_name], layer_kv_cache_spec, kv_cache_group_id
)
group_order.append(key)
else:
group_map[key].layer_names.append(layer_name)
attn_groups.append([group_map[key] for key in group_order])Then each group makes its builders. It passes the KV cache spec (with the kernel block size), the layer names, the config and the device (vllm/v1/worker/gpu/attn_utils.py:291-300, vllm/v1/worker/utils.py:330-339). Normally there is one builder per group; with dual-batch overlap there is one per micro-batch, because "some builders keep the prepared metadata on themselves" (vllm/v1/worker/gpu/attn_utils.py:296-299).
Step 4: CommonAttentionMetadata, once per KV cache group and step#
build_attn_metadata loops over the KV cache groups. For each group it makes one CommonAttentionMetadata from the step's input tensors (The model runner (Model Runner V2)):
common_attn_metadata = CommonAttentionMetadata(
query_start_loc=query_start_loc_gpu,
query_start_loc_cpu=query_start_loc_cpu,
seq_lens=seq_lens,
seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
max_seq_len=max_seq_len,
num_reqs=num_reqs,
num_actual_tokens=num_tokens,
max_query_len=max_query_len,
block_table_tensor=block_table,
slot_mapping=slot_mapping,
causal=group_causal,
dcp_local_seq_lens=dcp_local_seq_lens,
dcp_local_seq_lens_cpu_upper_bound=dcp_local_seq_lens_cpu_upper_bound,
positions=positions,
is_prefilling=group_is_prefilling,
mm_req_doc_ranges=mm_req_doc_ranges,
rswa_prefix_lens=rswa_prefix_lens,
req_idx=req_idx,
_token_to_req_indices_cache=token_to_req_indices,
**common_attn_metadata_extra_kwargs,
)Mainly the block table and the slot mapping differ between groups: each group has its own block table and its own row of slot mappings (vllm/v1/worker/gpu/attn_utils.py:504-505). A hybrid drafter can also set a causal flag per group, and a model state can add its own fields (vllm/v1/worker/gpu/attn_utils.py:506-520). The main fields:
| Field | Type, shape | Meaning |
|---|---|---|
query_start_loc, query_start_loc_cpu | int32 [num_reqs + 1], GPU and CPU | where each request's tokens start in the flat batch |
seq_lens | int32 [num_reqs] | computed tokens plus this step's tokens (see the warning below) |
num_reqs, num_actual_tokens | int | batch size; token count, which may be padded |
max_query_len, max_seq_len | int | longest query; longest context, maybe an upper bound |
block_table_tensor | int32 [num_reqs, max_num_blocks] | this group's block ids per request, in batch order |
slot_mapping | int64 [num_tokens] | KV cache slot of each new token; -1 for padding |
causal | bool or tensor | causal mask; per request for some models |
is_prefilling, positions, seq_lens_cpu_upper_bound | optional | extras that some backends read |
Definitions: vllm/v1/attention/backend.py:389-475.
Step 5: builder.build, and one metadata object for many layers#
For each attention group of the KV cache group, the runner calls the builder and stores the result under every layer name of the group:
for attn_group in attn_groups[i]:
attn_metadata_builder = attn_group.get_metadata_builder(ubatch_idx)
reuse_key = None
if attn_metadata_builder.supports_update_block_table and isinstance(
attn_group.kv_cache_spec, MambaSpec
):
reuse_key = (attn_group.kv_cache_spec, type(attn_metadata_builder))
if reuse_key in cached_metadata:
metadata = attn_metadata_builder.update_block_table(
cached_metadata[reuse_key], block_table, slot_mapping
)
elif for_cudagraph_capture:
metadata = attn_group.build_metadata_for_cudagraph_capture(
common_attn_metadata, ubatch_idx
)
else:
attn_metadata_extra_kwargs = (
model_specific_attn_metadata.get_extra_attn_kwargs(
attn_metadata_builder,
num_reqs,
)
if model_specific_attn_metadata is not None
else {}
)
metadata = attn_group.build_metadata(
common_attn_metadata, ubatch_idx, **attn_metadata_extra_kwargs
)
if reuse_key is not None and reuse_key not in cached_metadata:
cached_metadata[reuse_key] = metadata
for layer_name in attn_group.layer_names:
attn_metadata[layer_name] = metadata- Lines 553-560. Mamba groups with the same spec and builder reuse the first metadata and only swap the block table, through
update_block_table. - Lines 561-564. During CUDA graph capture,
build_for_cudagraph_captureis called instead. Its default just callsbuild(vllm/v1/attention/backend.py:720-729). - Lines 574-576. The normal path:
AttentionGroup.build_metadatacallsbuild(common_prefix_len=0, common_attn_metadata)(vllm/v1/worker/utils.py:341-352). Thecommon_prefix_lenargument is "Deprecated and always 0" (vllm/v1/attention/backend.py:686-704). - Lines 579-580. The same object goes under every layer name of the group.
If the backend's kernel uses smaller blocks than the KV cache manager, the attention group first rewrites the block table into kernel blocks (vllm/v1/worker/utils.py:368-378).
Step 6: CUDA graph support, and how it lowers the mode#
A builder reports how much CUDA graph capture it allows, with AttentionCGSupport:
class AttentionCGSupport(Enum):
"""Constants for the cudagraph support of the attention backend"""
ALWAYS = 3
"""Cudagraph always supported; supports mixed-prefill-decode"""
UNIFORM_BATCH = 2
"""Cudagraph supported for batches the only contain query lengths that are
the same, this can be used for spec-decode
i.e. "decodes" are 1 + num_speculative_tokens"""
UNIFORM_SINGLE_TOKEN_DECODE = 1
"""Cudagraph supported for batches the only contain query_len==1 decodes"""
NEVER = 0
"""NO cudagraph support"""The default is NEVER (vllm/v1/attention/backend.py:593-595). The runner asks every group and keeps the weakest answer (vllm/v1/worker/gpu/attn_utils.py:314-341). Then resolve_cudagraph_mode_and_sizes may lower the configured mode (vllm/v1/worker/gpu/model_runner.py:754-763):
- FULL for mixed batches, but support below
ALWAYS.NEVERraisesValueError. Otherwise the mode becomesFULL_AND_PIECEWISEif attention is a splitting op, elseFULL_DECODE_ONLY(vllm/config/compilation.py:1394-1418). - FULL decode graphs, but support
NEVER. The mode becomesPIECEWISEif attention is compiled piecewise, elseNONE(vllm/config/compilation.py:1421-1445). - FULL decode graphs with speculative decoding (decode query length above 1), but support below
UNIFORM_BATCH. The mode becomesPIECEWISEorNONEin the same way (vllm/config/compilation.py:1448-1465). - Last check: a mode with FULL graphs and support
NEVERraises (vllm/config/compilation.py:1482-1493).
Each change logs a warning that names the backend. torch.compile and CUDA graphs in vLLM explains the modes.
Step 7: the MLA variant#
Models with multi-head latent attention, such as DeepSeek-V3, use the layer MLAAttention and an impl based on MLAAttentionImpl. Its constructor gets the MLA dimensions and the kv_b_proj layer (vllm/v1/attention/backend.py:1034-1064). The impl has two paths instead of one forward:
forward_mqafor decode tokens, over the compressed latent cache (vllm/v1/attention/backend.py:1080-1089);forward_mhafor prefill tokens, "MHA-style" (vllm/v1/attention/backend.py:1066-1079).
The layer sends the first num_decode_tokens rows to forward_mqa and the rest to forward_mha (vllm/model_executor/layers/attention/mla_attention.py:984-985, vllm/model_executor/layers/attention/mla_attention.py:1017-1020). The MLA builder finds the split with split_decodes_and_prefills (vllm/model_executor/layers/attention/mla_attention.py:2605-2612). This is why Model Runner V2 puts the decodes first in the batch (vllm/v1/worker/gpu/model_runner.py:2476-2483). The cache write is one shared method, do_kv_cache_update, which calls the concat_and_cache_mla op (vllm/v1/attention/backend.py:1108-1128). The custom ops are unified_mla_kv_cache_update and unified_mla_attention_with_output (vllm/model_executor/layers/attention/mla_attention.py:1475-1479, vllm/model_executor/layers/attention/mla_attention.py:1524-1530).
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 diveWhere get_kv_cache_shape went: the KV cache layout
Older code and many guides use AttentionBackend.get_kv_cache_shape(). At this commit the base class has no such method; only one MLA backend still defines it (git grep, vllm/v1/attention/backends/mla/flashinfer_mla_sparse_sm90.py:227). The shape now comes from the KV cache spec and a layout descriptor:
- Logical shape per layer.
[B, H, N, C]: blocks, head slots, states (tokens) per block, and C bytes of content (vllm/v1/kv_cache_interface.py:345-361). For normal attention, C is(head_size + head_size_v) × dtype bytes: key and value of one token and head (vllm/v1/kv_cache_interface.py:570-575). - Physical order. A
KVCacheLayoutmember is a permutation of[L, B, H, N, C], where L is the layer inside a shared allocation (vllm/v1/kv_cache_layout.py:11-28). - Choice. The engine core resolves one layout for the whole model. A backend lists the layouts it can read with
supported_kv_cache_layouts(); with no list from any backend, the default preference starts withLBNHC(vllm/v1/attention/backends/utils.py:228-235,vllm/v1/attention/backends/utils.py:240-276).VLLM_KV_CACHE_LAYOUTcan force a layout that every backend supports (vllm/v1/attention/backends/utils.py:361-367). - Views.
allocate_kv_cacheallocates one int8 buffer and makes a strided view per layer, then casts it to the KV dtype (vllm/v1/worker/utils.py:564-625,vllm/v1/kv_cache_interface.py:461-466).
FlashAttention, traced. FlashAttention declares no layout list (it does not override the method), so a model with only FlashAttention layers gets LBNHC. Its forward docstring gives the view: [num_blocks, num_kv_heads, block_size, 2 * head_size] (vllm/v1/attention/backends/flash_attn.py:1199-1200). The cache write transposes it to (B, N, H, 2D) and splits the last dimension into a key half and a value half (vllm/v1/attention/backends/flash_attn.py:1445-1446). With LBNHC, the N axis is outside the H axis in memory (derived). FlashInfer on compute capability 10 lists LBHNC first, because "the trtllm-gen kernels consume head-major block interiors" (vllm/v1/attention/backends/flashinfer.py:700-706).
Worked example: Llama-3-8B with TP = 1, BF16 and block size 16. The view of one layer is [num_blocks, 8, 16, 256] bfloat16. So one block of one layer is 8 × 16 × 256 × 2 = 65,536 bytes (derived from the formula above). The hybrid KV cache: specs, groups and one manager per layer type explains page sizes across groups.
Deep diveFlashAttention metadata, field by field
class FlashAttentionMetadata:
# NOTE(sang): Definition of context_len, query_len, and seq_len.
# |---------- N-1 iteration --------|
# |---------------- N iteration ---------------------|
# |- tokenA -|......................|-- newTokens ---|
# |---------- context_len ----------|
# |-------------------- seq_len ---------------------|
# |-- query_len ---|
num_actual_tokens: int # Number of tokens excluding padding.
max_query_len: int
query_start_loc: torch.Tensor
max_seq_len: int
seq_lens: torch.Tensor
block_table: torch.Tensor
slot_mapping: torch.Tensor
# For GQA DCP
max_dcp_context_kv_len: int | None = None
dcp_context_kv_lens: torch.Tensor | None = None
# Split counts for FA2 DCP context attention. num_prefill_* tracks
# context-bearing extend rows; pure prefills do not attend to DCP context.
num_decode_reqs: int = 0
num_prefill_reqs: int = 0
num_decode_tokens: int = 0
num_prefill_tokens: int = 0
# Optional aot scheduling
scheduler_metadata: torch.Tensor | None = None
max_num_splits: int = 0
causal: bool | torch.Tensor = TrueThe builder copies most fields straight from the common metadata (vllm/v1/attention/backends/flash_attn.py:821-829). It adds backend-only fields, for example scheduler_metadata for ahead-of-time scheduling and the DCP context lengths. The fast_build flag "disables AOT scheduling, used when there will be few iterations i.e. spec-decode" (vllm/v1/attention/backends/flash_attn.py:818-820). Its CUDA graph support is ALWAYS with FlashAttention 3 or 4 (or on XPU), else UNIFORM_BATCH (vllm/v1/attention/backends/flash_attn.py:612-621).
Deep diveAttentionCGSupport of common backends
| Builder | Support | Source |
|---|---|---|
| FlashAttention | ALWAYS with FA3/FA4 or XPU, else UNIFORM_BATCH | vllm/v1/attention/backends/flash_attn.py:617-621 |
| FlashInfer | UNIFORM_BATCH when every layer can use the TRT-LLM decode path (with extra rules on SM90 and SM12x), else UNIFORM_SINGLE_TOKEN_DECODE; always the latter with DCP | vllm/v1/attention/backends/flashinfer.py:1145-1189 |
| Triton | ALWAYS | vllm/v1/attention/backends/triton_attn.py:96 |
| FlashAttention MLA | UNIFORM_BATCH | vllm/v1/attention/backends/mla/flashattn_mla.py:127 |
| FlashMLA | UNIFORM_BATCH | vllm/v1/attention/backends/mla/flashmla.py:110 |
| base class | NEVER | vllm/v1/attention/backend.py:593-595 |
Example: Llama-3-8B on an H100 with FlashAttention 3 reports ALWAYS, so the default FULL_AND_PIECEWISE stays. With FlashInfer at UNIFORM_SINGLE_TOKEN_DECODE and EAGLE drafts (decode query length 1 + k), rule 3 of Step 6 lowers the mode to PIECEWISE (derived). The design document has a similar table (docs/design/cuda_graphs.md:173-187).
Deep diveOther builder hooks
reorder_batch_threshold: the query length up to which the builder treats a request as a decode._init_reorder_batch_thresholdraises it to1 + num_speculative_tokensfor backends that run speculative verification as decodes (vllm/v1/attention/backend.py:658-684,vllm/v1/attention/backend.py:572-589).update_block_table: reuse metadata with another block table; only whensupports_update_block_tableis true (vllm/v1/attention/backend.py:706-718).build_for_drafting: metadata for a draft model, withfast_build=True(vllm/v1/attention/backend.py:731-751).update_draft_decode_metadata: capture-safe update between draft steps, for the fused multi-step draft graph (vllm/v1/attention/backend.py:753-761,docs/design/model_runner_v2.md:194-200).get_varlen_cudagraph_max_query_len: the largest per-request query length that a FULL graph of this builder can replay in a variable-length decode batch (vllm/v1/attention/backend.py:635-656).
What can go wrong#
- A backend with no CUDA graph support and
-cc.cudagraph_mode=FULL. Start-up stops with "CUDAGraphMode.FULL is not supported with ... backend (support: NEVER); please try cudagraph_mode=PIECEWISE" (vllm/config/compilation.py:1400-1410). - Silent mode changes. A weaker backend lowers
FULL_AND_PIECEWISEtoPIECEWISEorNONEwith only a warning. Decode then loses its full graph and gets slower. Look for "is not supported with" in the log. - Batch invariance. With
VLLM_BATCH_INVARIANT=1, a backend withoutsupports_batch_invariance()raisesRuntimeErrorininit_attn_backend(vllm/v1/worker/gpu/attn_utils.py:235-242). - A layout that some backend cannot read.
VLLM_KV_CACHE_LAYOUToutside the supported set raisesValueError; backends with no common layout raise too (vllm/v1/attention/backends/utils.py:361-367,vllm/v1/attention/backends/utils.py:271-276). - Fused output quantization on FlashAttention. Its forward raises
NotImplementedErrorwhen it gets an output scale (vllm/v1/attention/backends/flash_attn.py:1219-1222).
Why it matters. A backend plugin implements these three classes. The builder decides the CPU cost of each step and whether decode can use a FULL CUDA graph; the impl decides the GPU cost. When a deployment is slower than expected, check which backend each layer got and which CUDA graph mode survived start-up.
Check yourself#
- 1How many FlashAttention metadata objects does Llama-3-8B build per step, and how many impl objects exist?easy
Show answer
One metadata object per step: one KV cache group, one attention group, stored under all 32 layer names (
vllm/v1/worker/gpu/attn_utils.py:579-580). There are 32 impl objects, one perAttentionlayer (vllm/model_executor/layers/attention/attention.py:419-432). - 2Which class decides which CUDA graph modes are possible, and through which method?medium
Show answer
The metadata builder, through
get_cudagraph_support(), which returns anAttentionCGSupport(vllm/v1/attention/backend.py:626-633). The runner takes the minimum over all groups and passes it toresolve_cudagraph_mode_and_sizes(vllm/v1/worker/gpu/model_runner.py:754-763). - 3What is inmedium
seq_lensofCommonAttentionMetadatafor a decode request with 40 computed tokens?Show answer
41: computed tokens plus the 1 new query token. Model Runner V2 writes
num_computed_tokens + query_len(vllm/v1/worker/gpu/input_batch.py:375-376), although the docstring says "computed tokens" (vllm/v1/attention/backend.py:401-402). - 4A model runs FlashInfer with supporthard
UNIFORM_SINGLE_TOKEN_DECODE, with EAGLE and 3 speculative tokens, at the default O2. Which CUDA graph mode does it get, and why?Show answer
PIECEWISE. O2 setsFULL_AND_PIECEWISE, whose decode mode is FULL. The decode query length is 1 + 3 = 4 > 1, and the support is belowUNIFORM_BATCH. Attention is a splitting op by default, so the mode becomesPIECEWISE(vllm/config/compilation.py:1448-1465). - 5Where does FlashAttention write the new keys, and what memory layout does it see?hard
Show answer
In
do_kv_cache_update, called by the separate update op becauseforward_includes_kv_cache_update = False. It views the cache[B, H, N, 2D]as(B, N, H, 2D)and splits it into a key half and a value half. Then it scatters the new rows by slot mapping (vllm/v1/attention/backends/flash_attn.py:1430-1464).