vLLM Explained

"v1/attention/backend.py and selector.py: the backend interfaces and the selector"

AdvancedCode mapabout 6 min read + 2 min deep dives

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

Purpose#

These two files define what an attention backend must provide (backend.py) and how a layer gets one (selector.py).

Filesvllm/v1/attention/backend.py, 1,150 lines; vllm/v1/attention/selector.py, 250 lines.
Main classesAttentionBackend (vllm/v1/attention/backend.py:59-379), CommonAttentionMetadata (vllm/v1/attention/backend.py:389-554), AttentionMetadataBuilder (vllm/v1/attention/backend.py:592-761), AttentionImpl (vllm/v1/attention/backend.py:890-1031), MLAAttentionImpl (vllm/v1/attention/backend.py:1034-1128); get_attn_backend (vllm/v1/attention/selector.py:109-203).
Runs inEach worker: the selector while the model is built; the builders at KV cache init and in every step; the impls in every step.
Called byAttention and MLAAttention constructors (selector), attn_utils.py of Model Runner V2 (builders), the unified attention ops (impls).
Implemented byThe backend modules in vllm/v1/attention/backends/, for example flash_attn.py, and out-of-tree backends.
Not in this fileThe backend enum and its registry (backends/registry.py), the platform priority lists (How vLLM chooses an attention backend), the KV cache layout resolver (backends/utils.py), kernels.

Where it sits#

Figure: how the abstract classes of backend.py are used at start-up, per step and per layer.
Open full size ↗

Who calls it#

What it calls#

Key classes and functions#

NameLinesRole
AttentionTypevllm/v1/attention/backend.py:36-49decoder, encoder, encoder_only, encoder_decoder; strings for torch.compile.
MultipleOfvllm/v1/attention/backend.py:52-56Kernel block sizes "any multiple of N".
AttentionBackendvllm/v1/attention/backend.py:59-379Static description: names, classes, supports_*, validate_configuration, layouts.
CommonAttentionMetadatavllm/v1/attention/backend.py:389-554Backend-neutral batch description per KV cache group.
AttentionCGSupportvllm/v1/attention/backend.py:557-569ALWAYS, UNIFORM_BATCH, UNIFORM_SINGLE_TOKEN_DECODE, NEVER.
max_decode_query_lenvllm/v1/attention/backend.py:572-589Widest query that counts as a decode: 1 + num_speculative_tokens.
AttentionMetadataBuildervllm/v1/attention/backend.py:592-761build, capture and drafting variants, CUDA graph support.
AttentionLayervllm/v1/attention/backend.py:764-782Protocol: the scales and forward that an impl may read from its layer.
AttentionImplBasevllm/v1/attention/backend.py:785-887Shared attributes; DCP/PCP ranks set in __new__.
AttentionImplvllm/v1/attention/backend.py:890-1031forward plus optional fused-op hooks.
MLAAttentionImplvllm/v1/attention/backend.py:1034-1128forward_mha (prefill), forward_mqa (decode), shared cache write.
subclass_attention_backend(_with_overrides)vllm/v1/attention/backend.py:1131-1150Make a backend subclass with another builder or other members.
AttentionSelectorConfigvllm/v1/attention/selector.py:25-65The hashable set of facts that the platform sees.
get_attn_spec_kindvllm/v1/attention/selector.py:68-106Map a layer to a KVCacheSpecKind for per-kind overrides.
get_attn_backend, _cached_get_attn_backendvllm/v1/attention/selector.py:109-228Collect facts, pick the requested backend, ask the platform once per fact set.
get_mamba_attn_backendvllm/v1/attention/selector.py:231-250The same for Mamba-style layers, from their own enum.

Walk through the main methods#

AttentionBackend.validate_configuration#

    ) -> list[str]:
        invalid_reasons = []
        if not cls.supports_head_size(head_size):
            invalid_reasons.append("head_size not supported")
        if not cls.supports_dtype(dtype):
            invalid_reasons.append("dtype not supported")
        if not cls.supports_kv_cache_dtype(kv_cache_dtype):
            invalid_reasons.append("kv_cache_dtype not supported")
        if not cls.supports_block_size(block_size):
            invalid_reasons.append("block_size not supported")
        if use_mm_prefix and not cls.supports_mm_prefix():
            invalid_reasons.append(
                "partial multimodal token full attention not supported"
            )
        if use_mla != cls.is_mla():
            if use_mla:
                invalid_reasons.append("MLA not supported")
            else:
                invalid_reasons.append("non-MLA not supported")
        if has_sink and not cls.supports_sink():
            invalid_reasons.append("attention sinks not supported")
        if use_sparse != cls.is_sparse():
            if use_sparse:
                invalid_reasons.append("sparse not supported")
            else:
                invalid_reasons.append("non-sparse not supported")
        if use_per_head_quant_scales and not cls.supports_per_head_quant_scales():
            invalid_reasons.append("per-head quant scales not supported")
        if not cls.supports_compute_capability(device_capability):
            invalid_reasons.append("compute capability not supported")
        if not cls.supports_attn_type(attn_type):
            invalid_reasons.append(f"attention type {attn_type} not supported")

Each failed check adds a human-readable reason; an empty list means "valid". The rest of the method checks sliding window, R-SWA, non-causal attention, batch invariance, the KV connector, PCP, DCP and adaptive verification, then the backend's own supports_combination (vllm/v1/attention/backend.py:332-369). The page How vLLM chooses an attention backend shows how CUDA uses the reasons.

AttentionMetadataBuilder.__init__ and build#

class AttentionMetadataBuilder(ABC, Generic[M]):
    # Does this backend/builder support CUDA Graphs for attention (default: no).
    # Do not access directly. Call get_cudagraph_support() instead.
    _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.NEVER
    # Does this backend/builder reorder the batch?
    # If not, set this to None. Otherwise set it to the query
    # length that will be pulled into the front of the batch.
    reorder_batch_threshold: int | None = None
    # Does this backend/builder support updating the block table in existing
    # metadata
    supports_update_block_table: bool = False
    # Whether the builder constructor requires the block-table width.
    requires_block_table_width: ClassVar[bool] = False
    # Whether update_draft_decode_metadata() can regenerate all decode metadata
    # from persistent device buffers with capture-safe ops only. Lets a
    # speculator record the refresh inside its draft decode CUDA graph, so no
    # eager build() is needed between steps or between the batches that replay
    # it, whether the graph holds one draft forward or a multi-step loop.
    supports_draft_decode_metadata_update: bool = False

    @abstractmethod
    def __init__(
        self,
        kv_cache_spec: "KVCacheSpec",
        layer_names: list[str],
        vllm_config: "VllmConfig",
        device: torch.device,
    ):
        self.kv_cache_spec = kv_cache_spec
        self.layer_names = layer_names
        self.vllm_config = vllm_config
        self.device = device
  1. Line 595. CUDA graph support defaults to NEVER; read it through get_cudagraph_support() (line 594).
  2. Line 599. reorder_batch_threshold: the query length up to which the builder treats a request as a decode.
  3. Lines 602, 604, 610. Feature flags: block-table update, block-table width, capture-safe draft updates.
  4. Lines 613-624. The constructor gets the KV cache spec, the layer names of the group, the config and the device.

build(common_prefix_len, common_attn_metadata, fast_build=False) is abstract (vllm/v1/attention/backend.py:686-704). build_for_cudagraph_capture and build_for_drafting call it by default (lines 720-751).

AttentionImpl.forward#

    @abstractmethod
    def __init__(
        self,
        num_heads: int,
        head_size: int,
        scale: float,
        num_kv_heads: int | None = None,
        alibi_slopes: list[float] | None = None,
        sliding_window: int | None = None,
        kv_cache_dtype: str = "auto",
        logits_soft_cap: float | None = None,
        attn_type: str = AttentionType.DECODER,
        kv_sharing_target_layer_name: str | None = None,
    ) -> None:
        raise NotImplementedError

    @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 NotImplementedError

The constructor takes the same ten arguments that Attention.__init__ passes. forward gets the layer (for its scales), the tensors, the KV cache view, the metadata and the output buffer. The fused hooks (do_rope_and_kv_cache_update and others, lines 973-1031) are used only by Inductor fusion passes when the impl says it supports them.

get_attn_backend (selector.py)#

    attn_type = attn_type or AttentionType.DECODER
    attn_selector_config = AttentionSelectorConfig(
        head_size=head_size,
        dtype=dtype,
        kv_cache_dtype=cast(CacheDType | None, kv_cache_dtype),
        block_size=block_size,
        use_mla=use_mla,
        has_sink=has_sink,
        use_sparse=use_sparse,
        use_mm_prefix=use_mm_prefix,
        use_per_head_quant_scales=use_per_head_quant_scales,
        attn_type=attn_type,
        has_sliding_window=has_sliding_window,
        use_non_causal=vllm_config.attention_config.use_non_causal,
        use_batch_invariant=envs.VLLM_BATCH_INVARIANT,
        use_kv_connector=use_kv_connector,
        use_pcp=vllm_config.parallel_config.prefill_context_parallel_size > 1,
        use_adaptive_verification=use_adaptive_verification,
        use_dcp=vllm_config.parallel_config.decode_context_parallel_size > 1,
        use_rswa=(
            vllm_config.model_config is not None
            and vllm_config.model_config.rswa_window is not None
        ),
    )

    # A per-KV-group override (keyed by KVCacheSpecKind) takes precedence over
    # the global backend; kinds not present in the map fall back to it.
    attention_config = vllm_config.attention_config
    backend = attention_config.backend
    if attention_config.backend_per_kind:
        kind = get_attn_spec_kind(
            use_mla=use_mla,
            has_sliding_window=has_sliding_window,
            attn_type=attn_type,
        )
        backend = attention_config.backend_per_kind.get(kind.value, backend)

    # The KV cache layout is resolved across all of the model's backends at once
    # in get_kv_cache_spec(); a single selection cannot see its peers.
    return _cached_get_attn_backend(
        backend=backend,
        attn_selector_config=attn_selector_config,
        num_heads=num_heads,
        _run_kv_cache_dtype=(
            cache_config.cache_dtype if cache_config is not None else None
        ),
    )
  1. Lines 158-180. The selector config mixes the layer's facts with run-wide facts: KV connector, PCP, DCP, batch invariance, adaptive verification and R-SWA.
  2. Lines 184-192. A per-kind override wins over the global backend.
  3. Lines 196-203. The cached call. The run-wide KV cache dtype is part of the cache key.

The block size goes into the config only when the user set --block-size (vllm/v1/attention/selector.py:134-138). _cached_get_attn_backend is decorated with @cache, so each set of facts asks the platform once per process (vllm/v1/attention/selector.py:206-228).

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

CommonAttentionMetadata helpers

Deep dive

AttentionImplBase.__new__ and context parallelism

Every impl gets its DCP and PCP rank and world size in __new__, so subclasses need not call a base constructor (vllm/v1/attention/backend.py:853-878). need_to_return_lse_for_decode is true only with DCP > 1 and an impl that can return the softmax LSE. The comment on lse_base_on_e warns that a wrong base "silently corrupts the cross-shard softmax denominator" (vllm/v1/attention/backend.py:808-818).

Deep dive

Writing a backend: the minimum

  1. A subclass of AttentionBackend with get_name, get_impl_cls and get_builder_cls (vllm/v1/attention/backend.py:81-94), plus the supports_* answers that differ from the defaults.
  2. A metadata dataclass and a builder with __init__ and build; set _cudagraph_support if graphs are safe.
  3. An impl with __init__ and forward; if forward_includes_kv_cache_update = False, also do_kv_cache_update.
  4. Registration through register_backend and selection by the platform or --attention-backend (How vLLM chooses an attention backend).

(Derived from the abstract members of this file.)

Deep dive

Error paths

SituationResultWhere
unknown KV cache dtype stringAssertionError "Invalid kv_cache_dtype"vllm/v1/attention/selector.py:123-127
the platform returns no class nameValueError "Invalid attention backend for ..."vllm/v1/attention/selector.py:223-226
Mamba backend without batch invariance under VLLM_BATCH_INVARIANTRuntimeErrorvllm/v1/attention/selector.py:245-249
a builder without update_block_table asked to updateNotImplementedErrorvllm/v1/attention/backend.py:706-718

Check yourself#

  1. 1
    Which three static methods must every backend implement?
    easy
    Show answer

    get_name, get_impl_cls and get_builder_cls (vllm/v1/attention/backend.py:81-94).

  2. 2
    Why does the selector pass _run_kv_cache_dtype to the cached function although the platform can read the config?
    medium
    Show answer

    So the cache key changes when the run-wide KV cache dtype changes. The comment says some platform selectors inspect it (vllm/v1/attention/selector.py:214-215).

  3. 3
    What does max_decode_query_len return with 4 speculative tokens on Model Runner V2, and who uses it?
    hard
    Show answer

    5 (1 + 4); the extra term for parallel drafting applies only to Model Runner V1 (vllm/v1/attention/backend.py:584-589). Builders that run spec decoding as decodes raise their reorder_batch_threshold to it (vllm/v1/attention/backend.py:666-678).