vLLM Explained

How vLLM chooses an attention backend

AdvancedvLLM conceptsabout 17 min read + 7 min deep dives

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

In one sentence

When the worker builds the model, each attention layer asks the platform for a backend; CUDA takes the first valid entry of a priority list.

The idea#

Attention computes softmax(QKT)V for every head; MHA, GQA and MLA differ in how many key and value heads they store (Attention basics). vLLM has many implementations of this step. Each one is an attention backend: FlashAttention, FlashInfer, Triton, FlashMLA and more. A backend is fast only for some GPUs, head sizes and KV cache formats, so vLLM must choose one for each kind of layer.

The choice happens in the worker, while it builds the model. Each attention layer calls one function, the selector, with its own facts. Examples are the head size, the data type, the KV cache data type and MLA or not. The selector adds facts from the config and asks the platform. The platform method get_attn_backend_cls is a component picker: it returns the dotted name of a backend class.

On NVIDIA GPUs, CudaPlatform has a backend priority list list for each case: MLA or not, and the GPU generation. It asks each backend in the list whether it can serve this layer. The first backend that says yes wins. If the user names a backend with --attention-backend, vLLM checks only that one, and an invalid choice stops start-up.

All backend names are members of one enum, the AttentionBackendEnum. A plugin can replace the class behind a member, or add a new member, with register_backend. An out-of-tree platform can return any class name.

An everyday comparison. Think of a hospital that needs a surgeon for one operation. The head nurse has a ranked list of surgeons for each kind of operation. She asks each surgeon in order whether they can do this one. Each surgeon checks a fixed list of conditions and answers yes or no, with reasons. The first yes gets the job. If a patient asks for one surgeon by name, the nurse asks only that surgeon. The comparison stops here: vLLM asks every surgeon on the list, even after a yes, and it remembers the answer for the same question.

The picture#

Figure: the selection path of one attention layer on CUDA (panel 1), and the winner for four common cases (panel 2).
Open full size ↗

Read panel 1 in two rows. The blue steps run on every platform. The violet steps are the CUDA implementation of the platform hook; ROCm, CPU and XPU have their own. Panel 2 shows that the same model gets a different backend on a different GPU generation, because the lists differ.

The interactive visual below combines this choice with the CustomOp choice of CustomOp, out-of-tree ops and IR op providers. Pick a platform and the flags, and read which forward_* method RMSNorm runs and which attention backend a layer gets.

InteractiveWhich kernel path? RMSNorm method and attention backend

This interactive visual needs JavaScript. Turn on JavaScript in your browser to use it. The text on this page explains the same idea.

Press a button to change a setting (Tab, then Enter or Space). The rule tags (C1 ... C6, A1 ... A12) refer to the list below. acme_hw is a made-up out-of-tree platform.

In the code#

Step 1: AttentionBackendEnum names every backend#

The enum value is the default dotted class path. The docstring says that register_backend() can override it:

class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
    """Enumeration of all supported attention backends.

    The enum value is the default class path, but this can be overridden
    at runtime using register_backend().

    To get the actual backend class (respecting overrides), use:
        backend.get_class()
    """

    FLASH_ATTN = "vllm.v1.attention.backends.flash_attn.FlashAttentionBackend"
    FLASH_ATTN_DIFFKV = (
        "vllm.v1.attention.backends.flash_attn_diffkv.FlashAttentionDiffKVBackend"
    )
    TRITON_ATTN = "vllm.v1.attention.backends.triton_attn.TritonAttentionBackend"

At this commit the enum has 49 members (vllm/v1/attention/backends/registry.py:44-166, counted with git grep). Two members are placeholders. TORCH_SDPA has an empty value and is "only used for ViT". CUSTOM has the value None and "must be registered before use" (vllm/v1/attention/backends/registry.py:70, vllm/v1/attention/backends/registry.py:164-166). Mamba-style layers use a second enum, MambaAttentionBackendEnum, with 6 members (vllm/v1/attention/backends/registry.py:288-295). The first deep dive groups all members.

get_path() reads an override first, then the enum value. get_class() imports the class:

    def get_path(self, include_classname: bool = True) -> str:
        """Get the class path for this backend (respects overrides).

        Returns:
            The fully qualified class path string

        Raises:
            ValueError: If Backend.CUSTOM is used without being registered

        """
        path = _ATTN_OVERRIDES.get(self, self.value)
        if not path:
            raise ValueError(
                f"Backend {self.name} must be registered before use. "
                f"Use register_backend(Backend.{self.name}, 'your.module.YourClass')"
            )
        if not include_classname:
            path = path.rsplit(".", 1)[0]
        return path

    def get_class(self) -> "type[AttentionBackend]":
        """Get the backend class (respects overrides).

        Returns:
            The backend class

        Raises:
            ImportError: If the backend class cannot be imported
            ValueError: If Backend.CUSTOM is used without being registered

        """
        return resolve_obj_by_qualname(self.get_path())

Step 2: register_backend changes the table#

    # Handle dynamic enum creation for string backend names
    if isinstance(backend, str):
        if is_mamba:
            backend = MambaAttentionBackendEnum.register(backend, class_path or "")
        else:
            backend = AttentionBackendEnum.register(backend, class_path or "")

    def decorator(cls: type) -> type:
        if is_mamba:
            _MAMBA_ATTN_OVERRIDES[backend] = f"{cls.__module__}.{cls.__qualname__}"  # type: ignore[index]
        else:
            _ATTN_OVERRIDES[backend] = f"{cls.__module__}.{cls.__qualname__}"  # type: ignore[index]
        return cls

    if class_path is not None:
        if is_mamba:
            _MAMBA_ATTN_OVERRIDES[backend] = class_path  # type: ignore[index]
        else:
            _ATTN_OVERRIDES[backend] = class_path  # type: ignore[index]
        return lambda x: x

    return decorator
A string makes a new enum member first. Then the decorator or the explicit class_path writes the override.
  1. Lines 400-404. A string name such as "CUSTOM_MLA" calls AttentionBackendEnum.register. That method raises ValueError if the name exists already, or if it is not a valid identifier (vllm/v1/attention/backends/registry.py:180-187). It logs "Registered new attention backend" at INFO level (vllm/v1/attention/backends/registry.py:197).
  2. Lines 406-411. As a decorator, the override is the module and qualified name of the decorated class.
  3. Lines 413-418. With class_path, the override is that string. An enum member that has an override already gets the new one with no log and no error.

The overrides live in the module dict _ATTN_OVERRIDES (vllm/v1/attention/backends/registry.py:344). So register_backend must run in each process that selects backends: a general plugin is the normal place. A test shows the CUSTOM form:

def test_register_custom_backend_with_class_path():
    # Register with explicit class path
    register_backend(
        backend=AttentionBackendEnum.CUSTOM,
        class_path="tests.test_attention_backend_registry.CustomAttentionBackend",
        is_mamba=False,
    )

    # Check that CUSTOM backend is registered
    assert AttentionBackendEnum.CUSTOM.is_overridden(), (
        "CUSTOM should be overridden after registration"
    )

    # Get the registered class path
    class_path = AttentionBackendEnum.CUSTOM.get_path()
    assert class_path == "tests.test_attention_backend_registry.CustomAttentionBackend"

    # Get the backend class
    backend_cls = AttentionBackendEnum.CUSTOM.get_class()
    assert backend_cls.get_name() == "CUSTOM"
    assert backend_cls.get_impl_cls() == CustomAttentionImpl

Step 3: the layer calls the selector#

The Attention layer asks in its constructor, with use_mla=False:

        # During model initialization, the default dtype is set as the model
        # weight and activation dtype.
        dtype = torch.get_default_dtype()
        if attn_backend is None:
            self.attn_backend = get_attn_backend(
                head_size,
                dtype,
                kv_cache_dtype,
                use_mla=False,
                has_sink=self.has_sink,
                use_mm_prefix=self.use_mm_prefix,
                use_per_head_quant_scales=use_per_head_quant_scales,
                attn_type=attn_type,
                has_sliding_window=sliding_window is not None,
            )
        else:
            self.attn_backend = attn_backend

The MLA layer does the same with use_mla=True and num_heads (vllm/model_executor/layers/attention/mla_attention.py:514-521). Its head size is kv_lora_rank + qk_rope_head_dim (vllm/model_executor/layers/attention/mla_attention.py:480). A layer can also bring its own backend in attn_backend; then the selector does not run (lines 361-362). The model is built in the worker, so the selection runs there, or in the engine core process with the uni executor.

The selector adds facts from VllmConfig. Two details matter. First, it passes the block size only when the user set --block-size:

    vllm_config = get_current_vllm_config()

    cache_config = vllm_config.cache_config
    block_size: int | None
    if cache_config is not None and cache_config.user_specified_block_size:
        block_size = cache_config.block_size
    else:
        block_size = None

    kv_transfer_config = vllm_config.kv_transfer_config
    use_kv_connector = (
        kv_transfer_config is not None and kv_transfer_config.is_kv_transfer_instance
    )

Second, it chooses the requested backend. A per-kind override wins over the global value:

    # 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
        ),
    )

The kinds are the values of KVCacheSpecKind, for example full_attention, mla_attention and sliding_window (vllm/v1/kv_cache_interface.py:168-178). get_attn_spec_kind derives the kind from the layer (vllm/v1/attention/selector.py:68-106).

Step 4: one cached call to the platform#

@cache
def _cached_get_attn_backend(
    backend,
    attn_selector_config: AttentionSelectorConfig,
    num_heads: int | None = None,
    *,
    _run_kv_cache_dtype: CacheDType | None = None,
) -> type[AttentionBackend]:
    # Some platform selectors inspect the run-wide cache dtype. Keep it in the
    # cache key even though they read the value from the current config.
    from vllm.platforms import current_platform

    attention_cls = current_platform.get_attn_backend_cls(
        backend,
        attn_selector_config=attn_selector_config,
        num_heads=num_heads,
    )
    if not attention_cls:
        raise ValueError(
            f"Invalid attention backend for {current_platform.device_name}"
        )
    backend = resolve_obj_by_qualname(attention_cls)
    return backend
  1. Line 206. @cache keys on all arguments. The 32 decoder layers of a Llama model ask the same question, so the platform runs once. The run-wide KV dtype is in the key too (lines 214-215).
  2. Lines 218-222. The platform gets the requested backend (an enum member or None), the AttentionSelectorConfig and the head count.
  3. Lines 223-226. An empty answer raises "Invalid attention backend for ...". The base Platform returns "" (vllm/platforms/interface.py:388-396).
  4. Line 227. The dotted name is imported. The layer keeps the class.

Step 5: CUDA checks an explicit backend alone#

        device_capability = cls.get_device_capability()
        assert device_capability is not None

        # First try checking just the selected backend, if there is one.
        if selected_backend is not None:
            try:
                backend_class = _get_attn_backend_class(selected_backend)
                invalid_reasons = backend_class.validate_configuration(
                    device_capability=device_capability,
                    **attn_selector_config._asdict(),
                )
            except (ImportError, OSError) as e:
                raise ValueError(
                    f"Selected backend {selected_backend} is not valid for "
                    f"this configuration. Reason: [{type(e).__name__}: {e}]"
                ) from e
            if invalid_reasons:
                raise ValueError(
                    f"Selected backend {selected_backend} is not valid for "
                    f"this configuration. Reason: {invalid_reasons}"
                )
            else:
                logger.info("Using %s backend.", selected_backend)
                return _backend_cls_path(backend_class)

The flag --attention-backend fills attention_config.backend (vllm/engine/arg_utils.py:1103-1105, vllm/engine/arg_utils.py:2716-2726). The validator turns "auto" into None and any other string into an enum member, case-insensitive (vllm/config/attention.py:208-220). An unknown name raises "Unknown attention backend: ... Valid options are: ..." (vllm/v1/attention/backends/registry.py:21-31). The comment at line 451 says "or the selected backend is invalid", but the code above raises in that case and does not fall back.

Step 6: the priority lists#

_get_backend_priorities builds the list. The non-MLA branch:

    else:
        # SM100f defaults to FlashInfer for TRTLLM causal attention, but its non-causal
        # cutlass path (used for dflash attention) is known to have problems.
        # So prefer FlashAttention when non-causal on SM100f.
        if device_capability.major == 10 and not use_non_causal:
            return [
                *([AttentionBackendEnum.TRITON_FLASHINFER] if use_mm_prefix else []),
                AttentionBackendEnum.FLASHINFER,
                AttentionBackendEnum.FLASH_ATTN,
                AttentionBackendEnum.TRITON_ATTN,
                AttentionBackendEnum.FLEX_ATTENTION,
                AttentionBackendEnum.TURBOQUANT,
            ]
        else:
            return [
                *(
                    [AttentionBackendEnum.TRITON_FLASH_ATTN]
                    if device_capability.major == 9 and use_mm_prefix
                    else []
                ),
                AttentionBackendEnum.FLASH_ATTN,
                AttentionBackendEnum.FLASHINFER,
                AttentionBackendEnum.TRITON_ATTN,
                AttentionBackendEnum.FLEX_ATTENTION,
                AttentionBackendEnum.TURBOQUANT,
            ]

Blackwell (capability major 10) puts FlashInfer first, unless the model uses non-causal attention. Other GPUs put FlashAttention first. A multimodal prefix model gets a Triton variant in front. The major 10 causal list adds TRITON_FLASHINFER; the other list adds TRITON_FLASH_ATTN only on major 9 (lines 162, 171-175). The MLA branch has three cases: major 10, major 12 and all others (vllm/platforms/cuda.py:96-155). The third deep dive lists every case.

Step 7: each candidate validates itself#

get_valid_backends asks every entry, not only until the first success:

        backend_priorities = _get_backend_priorities(
            use_mla=attn_selector_config.use_mla,
            device_capability=device_capability,
            num_heads=num_heads,
            kv_cache_dtype=attn_selector_config.kv_cache_dtype,
            use_non_causal=attn_selector_config.use_non_causal,
            head_size=attn_selector_config.head_size,
            use_mm_prefix=attn_selector_config.use_mm_prefix,
        )
        for priority, backend in enumerate(backend_priorities):
            try:
                backend_class = _get_attn_backend_class(backend)
                invalid_reasons_i = backend_class.validate_configuration(
                    device_capability=device_capability,
                    **attn_selector_config._asdict(),
                )
            except (ImportError, OSError) as e:
                logger.debug(
                    "Attention backend %s is unavailable", backend.name, exc_info=True
                )
                invalid_reasons_i = [f"{type(e).__name__}: {e}"]
            if invalid_reasons_i:
                invalid_reasons[backend] = (priority, invalid_reasons_i)
            else:
                valid_backends_priorities.append(
                    _BackendCandidate(backend_class, backend, priority)
                )

        return valid_backends_priorities, invalid_reasons

An entry whose module fails to import gets the reason "ImportError: ..." or "OSError: ..." and a DEBUG log. So a missing optional library only removes that backend. Each backend class answers with validate_configuration, which collects reasons:

        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")

The rest of the method checks sliding window, R-SWA, non-causal attention, batch invariance and the KV connector. It also checks PCP, DCP and adaptive verification, then calls supports_combination for backend-specific rules (vllm/v1/attention/backend.py:332-369). A backend class changes the answer by overriding the small supports_* methods. For example, FlashAttention accepts capability 8.0 and higher (vllm/v1/attention/backends/flash_attn.py:456-457). ROCM_ATTN returns False from supports_kv_connector (vllm/v1/attention/backends/rocm_attn.py:184-188).

Step 8: pick the winner, or fail with every reason#

        valid_backends_priorities, all_invalid_reasons = cls.get_valid_backends(
            device_capability=device_capability,
            attn_selector_config=attn_selector_config,
            num_heads=num_heads,
        )
        reasons_str = (
            "{"
            + ", ".join(
                f"{backend.name}: [{', '.join(reasons)}]"
                for backend, (_, reasons) in all_invalid_reasons.items()
            )
            + "}"
        )
        config_str = attn_selector_config.__repr__()
        logger.debug_once(
            f"Some attention backends are not valid for {cls.device_name} with "
            f"{config_str}. Reasons: {reasons_str}."
        )
        if len(valid_backends_priorities) == 0:
            raise ValueError(
                f"No valid attention backend found for {cls.device_name} "
                f"with {config_str}. Reasons: {reasons_str}."
            )

        # We have found some valid backends. Select the one with the
        # highest priority.
        selected_candidate = min(
            valid_backends_priorities,
            key=lambda candidate: candidate.priority,
        )
        selected_backend_class = selected_candidate.backend_class
        selected_backend = selected_candidate.backend
        selected_priority = selected_candidate.priority

All reasons go into one string, so the error message tells why each backend said no. The winner is the valid entry with the smallest index. vLLM logs it once at INFO level: "Using FLASH_ATTN attention backend out of potential backends: ['FLASH_ATTN', 'FLASHINFER', ...]" (vllm/platforms/cuda.py:508-516). If the user set --block-size and that excluded a better backend, vLLM warns (vllm/platforms/cuda.py:487-506).

Step 9: other platforms, and how to add a backend#

Each platform implements the hook in its own way:

So there are three ways to bring your own backend:

  1. Replace a built-in member. register_backend(AttentionBackendEnum.FLASH_ATTN, "my_pkg.attn.MyFlash"). CUDA reads members through get_class() (vllm/platforms/cuda.py:189-190), so the priority list uses your class in that slot. Your class then validates itself.
  2. Fill CUSTOM. CUSTOM is in no priority list, so the user must select it: --attention-backend CUSTOM.
  3. Own the platform. A platform plugin returns its class from get_attn_backend_cls, as the test platform does. The page The Platform interface explains the hook.

Choices 1 and 2 need the register call in every process that builds the model, so put it in a general plugin.

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

The 49 enum members, grouped

Counted from vllm/v1/attention/backends/registry.py:44-166. The groups are this site's; the code has one flat list.

GroupMembersCount
Standard attention (MHA/GQA) on GPUsFLASH_ATTN, FLASH_ATTN_DIFFKV, TRITON_ATTN, TRITON_ATTN_DIFFKV, TRITON_FLASH_ATTN, FLASHINFER, TRITON_FLASHINFER, FLEX_ATTENTION, B12X, HPC_ATTN, TURBOQUANT, ULTRAQUANT12
Dense MLAFLASHINFER_MLA, TOKENSPEED_MLA, TRITON_MLA, CUTLASS_MLA, FLASHMLA, FLASH_ATTN_MLA6
Sparse MLAFLASHINFER_MLA_SPARSE, FLASHINFER_MLA_SPARSE_SM120, FLASHINFER_MLA_SPARSE_SM90, FLASHMLA_SPARSE, FLASH_ATTN_MLA_SPARSE, FLASH_ATTN_MLA_SPARSE_FA46
Model-specific (value in vllm.models.*)3 for DeepSeek V4, 4 for DeepSeek V4.1, MINIMAX_M3_SPARSE, CUTLASS_MSA, TRITON_MSA, STEP5_DSA11
ROCmROCM_ATTN, ROCM_SEGMENTED_ATTN, ROCM_AITER_FA, ROCM_AITER_UNIFIED_ATTN, ROCM_AITER_MLA, ROCM_AITER_TRITON_MLA, ROCM_AITER_MLA_SPARSE7
XPUXPU_MLA_SPARSE1
CPUCPU_ATTN, CPU_MLA, AMX_MLA3
SpecialTORCH_SDPA (ViT tag, empty value), NO_ATTENTION, CUSTOM (placeholder)3

A comment says that the DeepSeek V4 and V4.1 members are "model-driven; selected via the V4 layer" (vllm/v1/attention/backends/registry.py:99). Another says that HPC_ATTN runs only on Hopper with the Hy3 model and block size 64 (vllm/v1/attention/backends/registry.py:149-154). The model-specific layers bring their own backend through get_attn_backend() methods, for example in vllm/models/deepseek_v4/attention.py:852. This site does not describe the per-model code.

Deep dive

AttentionSelectorConfig: every input of the decision

The selector packs the facts into a NamedTuple (vllm/v1/attention/selector.py:25-43). The platform passes all fields to validate_configuration as keyword arguments (vllm/platforms/cuda.py:401-404).

FieldTypeSet from
head_sizeintthe layer; 128 for Llama-3-8B, 576 for a DeepSeek MLA layer (512 + 64, vllm/v1/attention/backends/fa_utils.py:338-339)
dtypetorch.dtypetorch.get_default_dtype() during model build (vllm/model_executor/layers/attention/attention.py:348)
kv_cache_dtypestr | Nonecache_config.cache_dtype, after per-layer skips (vllm/model_executor/layers/attention/attention.py:276-326)
block_sizeint | Noneonly with --block-size (vllm/v1/attention/selector.py:135-138)
use_mla, use_sparseboolthe layer class
has_sink, use_mm_prefix, use_per_head_quant_scales, has_sliding_windowboolthe layer and the model config (vllm/model_executor/layers/attention/attention.py:294-297, vllm/model_executor/layers/attention/attention.py:340-344)
attn_typestrdefault AttentionType.DECODER (vllm/v1/attention/selector.py:157)
use_non_causalboolattention_config.use_non_causal (vllm/v1/attention/selector.py:170)
use_batch_invariantboolVLLM_BATCH_INVARIANT, default 0 (vllm/envs.py:637)
use_kv_connectorboola KV transfer config with a connector and a role (vllm/v1/attention/selector.py:140-143)
use_pcp, use_dcpboolprefill or decode context parallel size above 1 (vllm/v1/attention/selector.py:173-175)
use_adaptive_verification, use_rswaboolspeculative config and model config (vllm/v1/attention/selector.py:145-155, vllm/v1/attention/selector.py:176-179)

The KV dtype string must be known, else an assert fails (vllm/v1/attention/selector.py:123-127). num_heads is not a field; only the MLA layer passes it, and only the sparse MLA lists use it (vllm/platforms/cuda.py:110).

Deep dive

Every CUDA priority list

From vllm/platforms/cuda.py:84-181. "Major" is the major compute capability: 9 for H100, 10 for B200, 12 for consumer Blackwell.

CaseList, best first
non-MLA, major 10, causal(TRITON_FLASHINFER if multimodal prefix), FLASHINFER, FLASH_ATTN, TRITON_ATTN, FLEX_ATTENTION, TURBOQUANT
non-MLA, all other cases(TRITON_FLASH_ATTN if major 9 and multimodal prefix), FLASH_ATTN, FLASHINFER, TRITON_ATTN, FLEX_ATTENTION, TURBOQUANT
MLA, major 10FLASHINFER_MLA, TOKENSPEED_MLA, CUTLASS_MLA, FLASH_ATTN_MLA, FLASHMLA, TRITON_MLA, then sparse: fp8 KV → FLASHINFER_MLA_SPARSE, FLASHMLA_SPARSE; BF16 and ≤ 16 heads → FLASH_ATTN_MLA_SPARSE_FA4, FLASHINFER_MLA_SPARSE, FLASHMLA_SPARSE; else FLASHMLA_SPARSE, FLASHINFER_MLA_SPARSE
MLA, major 12TRITON_MLA, FLASHINFER_MLA_SPARSE_SM120
MLA, other majors (9, 8 ...)FLASH_ATTN_MLA, FLASHMLA, FLASHINFER_MLA, TRITON_MLA, then FLASH_ATTN_MLA_SPARSE, FLASHMLA_SPARSE, with FLASHINFER_MLA_SPARSE_SM90 first if head size is 512, else last

Dense and sparse entries share one list. validate_configuration removes the wrong half, because use_sparse must equal is_sparse() (vllm/v1/attention/backend.py:321-325). The comment at vllm/platforms/cuda.py:157-159 gives the reason for the non-causal rule on SM100: FlashInfer's non-causal path "is known to have problems". The comment at vllm/platforms/cuda.py:98-100 links the benchmark issue for the sparse order.

Deep dive

Worked example: Llama-3-8B and DeepSeek-V3 on H100 and B200

All four cases use vllm serve defaults: --kv-cache-dtype auto, BF16 weights, no --block-size. They assume three things. vllm_flash_attn has FA3 for SM90 (case 3). FlashInfer imports (cases 2 and 4). The FlashMLA extension is built (the fp8 variant of case 3). An ImportError or a missing build only makes that backend invalid, and the next entry wins. (Model sizes come from the model configs, not from this repository.)

  1. Llama-3-8B, H100 (capability 9.0). 32 query heads of size 128, 8 KV heads. The list is FLASH_ATTN, FLASHINFER, TRITON_ATTN, FLEX_ATTENTION, TURBOQUANT. FLASH_ATTN accepts head size 128 (a multiple of 8, at most 256), BF16, KV auto, capability 9.0 (vllm/v1/attention/backends/flash_attn.py:423-427, vllm/v1/attention/backends/flash_attn.py:456-457). It has index 0, so it wins.
  2. Llama-3-8B, B200 (capability 10.0). The list starts with FLASHINFER. It accepts head size 128 and capability 10.0 (vllm/v1/attention/backends/flashinfer.py:662-675), so it wins.
  3. DeepSeek-V3, H100. MLA layers, head size 512 + 64 = 576. The list starts with FLASH_ATTN_MLA. It needs FA3 on SM90 (vllm/v1/attention/backends/fa_utils.py:334-336) and accepts KV auto. It wins. With --kv-cache-dtype fp8 it says "kv_cache_dtype not supported", and FLASHMLA (index 1) wins.
  4. DeepSeek-V3, B200. The list starts with FLASHINFER_MLA. It needs capability major 10 and qk_nope_head_dim in 64, 128 or 192 (vllm/v1/attention/backends/mla/flashinfer_mla.py:240-249). DeepSeek-V3 has 128, so it wins.

The block size follows. After load_model, update_block_size_for_backend picks a block size for the chosen backends, when the user did not set one (vllm/platforms/interface.py:771-781). With one backend, it is that backend's preferred size (vllm/platforms/interface.py:721-722). FLASHMLA supports only 64 (vllm/v1/attention/backends/mla/flashmla.py:57-58), so case 3 with fp8 runs with block size 64 instead of 16. FLASHINFER_MLA supports 32 and 64 (vllm/v1/attention/backends/mla/flashinfer_mla.py:204-205), so case 4 gets 32, the smallest one (vllm/v1/attention/backend.py:155-163). (Derived from these lines.)

Deep dive

Flags, config fields and environment variables

SettingDefaultRead atEffect
--attention-backendnot set (auto)vllm/engine/arg_utils.py:2716-2726Fills attention_config.backend; raises if --attention-config also sets backend.
--attention-config '{"backend_per_kind": {...}}'{}vllm/v1/attention/selector.py:186-192A backend per KV cache kind; unknown kinds raise (vllm/config/attention.py:230-254).
attention_config.flash_attn_versionNonevllm/v1/attention/backends/fa_utils.py:113-117Forces FA 2, 3 or 4; SM90 prefers 3, SM100 prefers 4 (vllm/v1/attention/backends/fa_utils.py:99-107).
--kv-cache-dtypeautovllm/model_executor/layers/attention/attention.py:276-279An fp8 value removes backends that do not accept it.
--block-sizenot setvllm/v1/attention/selector.py:135-138Only a user value reaches the selector; it can exclude backends, with a warning.
VLLM_BATCH_INVARIANT0vllm/v1/attention/selector.py:171Backends without batch invariance say no.
VLLM_LOGGING_LEVEL=DEBUGINFOvllm/platforms/cuda.py:467-470Shows every rejected backend and its reasons, also when one is valid.
Deep dive

Edge cases of register_backend and the cache

  • Re-registration. An enum member gets the new override silently. A string name that exists raises ValueError and suggests the enum form (vllm/v1/attention/backends/registry.py:180-185). So two general plugins that override FLASH_ATTN do not fail; the last one wins.
  • Timing. _cached_get_attn_backend keeps its first answer for the process (vllm/v1/attention/selector.py:206). A registration after the model build has no effect on that model. (Derived.)
  • The front end parses the name. --attention-backend MY_NAME becomes an enum member while the front end parses the config (vllm/config/attention.py:216-219). A string-registered name must therefore exist in the front end too. Whether such a dynamic member survives the pickled VllmConfig in the engine core and the workers is not traced here (open question). The CUSTOM member exists in every process.
  • CUSTOM without registration. get_path() raises "Backend CUSTOM must be registered before use" (vllm/v1/attention/backends/registry.py:211-215).
  • Mamba. register_backend(..., is_mamba=True) writes _MAMBA_ATTN_OVERRIDES (vllm/v1/attention/backends/registry.py:407-417). Mamba layers do not use the priority lists; they ask get_mamba_attn_backend (vllm/v1/attention/selector.py:231-250).

What can go wrong#

  • No backend fits. The message lists every backend and its reasons: "No valid attention backend found for cuda with AttentionSelectorConfig(...). Reasons: {...}" (vllm/platforms/cuda.py:471-475). Read the reasons: a wrong KV dtype, an unsupported head size or a missing library is the usual cause.
  • A library is missing. An import error makes only that backend invalid, with a DEBUG log (vllm/platforms/cuda.py:405-409). The server starts with a slower backend and says so only in the INFO line "Using X attention backend".
  • A block size excludes the best backend. vLLM warns "--block-size ... precluded higher-priority backend(s)" (vllm/platforms/cuda.py:498-506). Remove --block-size.
  • Your custom backend is never chosen. CUSTOM and new string names are in no priority list. Select them with --attention-backend, or override an existing member.
  • CPU ignores your choice. The CPU platform logs "Cannot use ... backend on CPU." and uses its own backend (vllm/platforms/cpu.py:183-184).

Why it matters. The attention backend sets the speed of every step and also the block size and the KV cache layout. The startup log line "Using X attention backend" is the first thing to check when a deployment is slower than expected.

Check yourself#

  1. 1
    In which process, and when, does vLLM choose the attention backend?
    easy
    Show answer

    In the worker (or in the engine core process with the uni executor), while the model is built. Each attention layer calls get_attn_backend in its constructor (vllm/model_executor/layers/attention/attention.py:349-360).

  2. 2
    You start Llama-3-8B on an H100 with no flags. Which backend runs, and why not FlashInfer?
    easy
    Show answer

    FLASH_ATTN. On capability major 9 the non-MLA list puts FLASH_ATTN before FLASHINFER (vllm/platforms/cuda.py:170-181), and the valid entry with the smallest index wins (vllm/platforms/cuda.py:479-482).

  3. 3
    You run a DeepSeek-V3-style MLA model on H100 with --kv-cache-dtype fp8. Which backend wins, and what block size follows?
    medium
    Show answer

    FLASHMLA. FLASH_ATTN_MLA is first but accepts only auto, float16 and bfloat16 KV (vllm/v1/attention/backends/mla/flashattn_mla.py:42-46). FlashMLA supports only block size 64 (vllm/v1/attention/backends/mla/flashmla.py:57-58), so update_block_size_for_backend sets 64 when you did not set --block-size (vllm/platforms/interface.py:721-722).

  4. 4
    A general plugin calls register_backend(AttentionBackendEnum.CUSTOM, "acme.attn.Fast"). Will vLLM use it on CUDA with no other flag?
    medium
    Show answer

    No. CUSTOM is in no CUDA priority list (vllm/platforms/cuda.py:84-181). Start with --attention-backend CUSTOM, which CUDA then validates alone (vllm/platforms/cuda.py:430-449). Or override a member that is in the list, such as FLASH_ATTN.

  5. 5
    You set --attention-backend FLASH_ATTN for a model whose layers are MLA. What is the error, and why is there no fallback?
    hard
    Show answer

    ValueError: "Selected backend AttentionBackendEnum.FLASH_ATTN is not valid for this configuration. Reason: [...]". The list has "MLA not supported", because FlashAttentionBackend.is_mla() is False while use_mla is True (vllm/v1/attention/backend.py:314-316). For a DeepSeek layer (head size 576) it starts with "head_size not supported", because FlashAttention accepts at most 512 (vllm/v1/attention/backends/flash_attn.py:423-433, vllm/v1/attention/backend.py:302-303). The explicit branch raises and returns before the priority list (vllm/platforms/cuda.py:442-449), although the comment at line 451 suggests a fallback.