How vLLM chooses an attention backend
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#
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.
This interactive visual needs JavaScript. Turn on JavaScript in your browser to use it. The text on this page explains the same idea.
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 decoratorclass_path writes the override.- Lines 400-404. A string name such as
"CUSTOM_MLA"callsAttentionBackendEnum.register. That method raisesValueErrorif 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). - Lines 406-411. As a decorator, the override is the module and qualified name of the decorated class.
- 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() == CustomAttentionImplStep 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_backendThe 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- Line 206.
@cachekeys 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). - Lines 218-222. The platform gets the requested backend (an enum member or
None), theAttentionSelectorConfigand the head count. - Lines 223-226. An empty answer raises "Invalid attention backend for ...". The base
Platformreturns""(vllm/platforms/interface.py:388-396). - 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_reasonsAn 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.priorityAll 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:
- ROCm has the same shape: an explicit backend first, then a list (
vllm/platforms/rocm.py:731-836). Its list depends on AITER and on the KV connector (vllm/platforms/rocm.py:489-526). It tolerates an invalid explicit backend only in TurboQuant and UltraQuant runs. Even there, the one reason must be the KV dtype or the packed layout. Then it uses the list (vllm/platforms/rocm.py:765-795). - CPU has no list. It returns
CPU_ATTN, orAMX_MLA/CPU_MLAfor MLA. It logs an explicit backend that it cannot use and goes on (vllm/platforms/cpu.py:143-192). - An out-of-tree platform overrides
get_attn_backend_clsand returns any dotted class path. The test platform does exactly this (tests/plugins/vllm_add_dummy_platform/vllm_add_dummy_platform/dummy_platform.py:25-32).
So there are three ways to bring your own backend:
- Replace a built-in member.
register_backend(AttentionBackendEnum.FLASH_ATTN, "my_pkg.attn.MyFlash"). CUDA reads members throughget_class()(vllm/platforms/cuda.py:189-190), so the priority list uses your class in that slot. Your class then validates itself. - Fill
CUSTOM.CUSTOMis in no priority list, so the user must select it:--attention-backend CUSTOM. - 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 diveThe 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.
| Group | Members | Count |
|---|---|---|
| Standard attention (MHA/GQA) on GPUs | FLASH_ATTN, FLASH_ATTN_DIFFKV, TRITON_ATTN, TRITON_ATTN_DIFFKV, TRITON_FLASH_ATTN, FLASHINFER, TRITON_FLASHINFER, FLEX_ATTENTION, B12X, HPC_ATTN, TURBOQUANT, ULTRAQUANT | 12 |
| Dense MLA | FLASHINFER_MLA, TOKENSPEED_MLA, TRITON_MLA, CUTLASS_MLA, FLASHMLA, FLASH_ATTN_MLA | 6 |
| Sparse MLA | FLASHINFER_MLA_SPARSE, FLASHINFER_MLA_SPARSE_SM120, FLASHINFER_MLA_SPARSE_SM90, FLASHMLA_SPARSE, FLASH_ATTN_MLA_SPARSE, FLASH_ATTN_MLA_SPARSE_FA4 | 6 |
Model-specific (value in vllm.models.*) | 3 for DeepSeek V4, 4 for DeepSeek V4.1, MINIMAX_M3_SPARSE, CUTLASS_MSA, TRITON_MSA, STEP5_DSA | 11 |
| ROCm | ROCM_ATTN, ROCM_SEGMENTED_ATTN, ROCM_AITER_FA, ROCM_AITER_UNIFIED_ATTN, ROCM_AITER_MLA, ROCM_AITER_TRITON_MLA, ROCM_AITER_MLA_SPARSE | 7 |
| XPU | XPU_MLA_SPARSE | 1 |
| CPU | CPU_ATTN, CPU_MLA, AMX_MLA | 3 |
| Special | TORCH_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 diveAttentionSelectorConfig: 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).
| Field | Type | Set from |
|---|---|---|
head_size | int | the layer; 128 for Llama-3-8B, 576 for a DeepSeek MLA layer (512 + 64, vllm/v1/attention/backends/fa_utils.py:338-339) |
dtype | torch.dtype | torch.get_default_dtype() during model build (vllm/model_executor/layers/attention/attention.py:348) |
kv_cache_dtype | str | None | cache_config.cache_dtype, after per-layer skips (vllm/model_executor/layers/attention/attention.py:276-326) |
block_size | int | None | only with --block-size (vllm/v1/attention/selector.py:135-138) |
use_mla, use_sparse | bool | the layer class |
has_sink, use_mm_prefix, use_per_head_quant_scales, has_sliding_window | bool | the layer and the model config (vllm/model_executor/layers/attention/attention.py:294-297, vllm/model_executor/layers/attention/attention.py:340-344) |
attn_type | str | default AttentionType.DECODER (vllm/v1/attention/selector.py:157) |
use_non_causal | bool | attention_config.use_non_causal (vllm/v1/attention/selector.py:170) |
use_batch_invariant | bool | VLLM_BATCH_INVARIANT, default 0 (vllm/envs.py:637) |
use_kv_connector | bool | a KV transfer config with a connector and a role (vllm/v1/attention/selector.py:140-143) |
use_pcp, use_dcp | bool | prefill or decode context parallel size above 1 (vllm/v1/attention/selector.py:173-175) |
use_adaptive_verification, use_rswa | bool | speculative 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 diveEvery 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.
| Case | List, 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 10 | FLASHINFER_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 12 | TRITON_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 diveWorked 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.)
- 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_ATTNaccepts head size 128 (a multiple of 8, at most 256), BF16, KVauto, 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. - 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. - 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 KVauto. It wins. With--kv-cache-dtype fp8it says "kv_cache_dtype not supported", andFLASHMLA(index 1) wins. - DeepSeek-V3, B200. The list starts with
FLASHINFER_MLA. It needs capability major 10 andqk_nope_head_dimin 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 diveFlags, config fields and environment variables
| Setting | Default | Read at | Effect |
|---|---|---|---|
--attention-backend | not set (auto) | vllm/engine/arg_utils.py:2716-2726 | Fills attention_config.backend; raises if --attention-config also sets backend. |
--attention-config '{"backend_per_kind": {...}}' | {} | vllm/v1/attention/selector.py:186-192 | A backend per KV cache kind; unknown kinds raise (vllm/config/attention.py:230-254). |
attention_config.flash_attn_version | None | vllm/v1/attention/backends/fa_utils.py:113-117 | Forces FA 2, 3 or 4; SM90 prefers 3, SM100 prefers 4 (vllm/v1/attention/backends/fa_utils.py:99-107). |
--kv-cache-dtype | auto | vllm/model_executor/layers/attention/attention.py:276-279 | An fp8 value removes backends that do not accept it. |
--block-size | not set | vllm/v1/attention/selector.py:135-138 | Only a user value reaches the selector; it can exclude backends, with a warning. |
VLLM_BATCH_INVARIANT | 0 | vllm/v1/attention/selector.py:171 | Backends without batch invariance say no. |
VLLM_LOGGING_LEVEL=DEBUG | INFO | vllm/platforms/cuda.py:467-470 | Shows every rejected backend and its reasons, also when one is valid. |
Deep diveEdge cases of register_backend and the cache
- Re-registration. An enum member gets the new override silently. A string name that exists raises
ValueErrorand suggests the enum form (vllm/v1/attention/backends/registry.py:180-185). So two general plugins that overrideFLASH_ATTNdo not fail; the last one wins. - Timing.
_cached_get_attn_backendkeeps 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_NAMEbecomes 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 pickledVllmConfigin the engine core and the workers is not traced here (open question). TheCUSTOMmember exists in every process. CUSTOMwithout 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 askget_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.
CUSTOMand 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#
- 1In 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_backendin its constructor (vllm/model_executor/layers/attention/attention.py:349-360). - 2You 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 putsFLASH_ATTNbeforeFLASHINFER(vllm/platforms/cuda.py:170-181), and the valid entry with the smallest index wins (vllm/platforms/cuda.py:479-482). - 3You run a DeepSeek-V3-style MLA model on H100 withmedium
--kv-cache-dtype fp8. Which backend wins, and what block size follows?Show answer
FLASHMLA.FLASH_ATTN_MLAis 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), soupdate_block_size_for_backendsets 64 when you did not set--block-size(vllm/platforms/interface.py:721-722). - 4A general plugin callsmedium
register_backend(AttentionBackendEnum.CUSTOM, "acme.attn.Fast"). Will vLLM use it on CUDA with no other flag?Show answer
No.
CUSTOMis 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 asFLASH_ATTN. - 5You sethard
--attention-backend FLASH_ATTNfor a model whose layers are MLA. What is the error, and why is there no fallback?Show answer
ValueError: "Selected backend AttentionBackendEnum.FLASH_ATTN is not valid for this configuration. Reason: [...]". The list has "MLA not supported", becauseFlashAttentionBackend.is_mla()isFalsewhileuse_mlaisTrue(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.