[Attention] Require explicit DCP support from attention implementations (#55780)
Signed-off-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Andreas Karatzas committed
Sep 8, 2026 at 16:04 UTC
db3814a4f215e666098ffb74a5c21bf46c60bae9
15 files changed
+199
-19
tests/kernels/attention/test_attention_selector.py
+94
-16
@@ -10,6 +10,7 @@ import torch
10
from vllm.config import (
11
AttentionConfig,
12
CacheConfig,
13
+ ParallelConfig,
14
VllmConfig,
15
set_current_vllm_config,
16
)
@@ -29,7 +30,11 @@ else:
30
31
from vllm.v1.attention.backend import AttentionType
32
from vllm.v1.attention.backends.registry import AttentionBackendEnum
32
-from vllm.v1.attention.selector import _cached_get_attn_backend, get_attn_backend
33
+from vllm.v1.attention.selector import (
34
+ AttentionSelectorConfig,
35
+ _cached_get_attn_backend,
36
+ get_attn_backend,
37
+)
38
39
40
@pytest.fixture(autouse=True)
@@ -77,33 +82,54 @@ def generate_params():
82
else DEVICE_REGULAR_ATTN_BACKENDS[device]
83
)
84
for name in backends:
80
- block_sizes = DEVICE_MLA_BLOCK_SIZES[device] if use_mla else [16]
85
+ block_sizes = (
86
+ [128]
87
+ if name == "CUTLASS_MLA"
88
+ else DEVICE_MLA_BLOCK_SIZES[device]
89
+ if use_mla
90
+ else [16]
91
+ )
92
for block_size in block_sizes:
82
- params.append(
83
- pytest.param(
84
- device,
85
- name,
86
- use_mla,
87
- block_size,
88
- id=f"{device}_{name}_mla_{str(use_mla)[0]}_blks{block_size}",
93
+ for use_dcp in [False, True] if device != "cpu" else [False]:
94
+ params.append(
95
+ pytest.param(
96
+ device,
97
+ name,
98
+ use_mla,
99
+ block_size,
100
+ use_dcp,
101
+ id=(
102
+ f"{device}_{name}_mla_{str(use_mla)[0]}"
103
+ f"_blks{block_size}_dcp{use_dcp}"
104
+ ),
105
+ )
106
)
90
- )
107
return params
108
109
94
-@pytest.mark.parametrize("device, name, use_mla, block_size", generate_params())
110
+@pytest.mark.parametrize(
111
+ "device, name, use_mla, block_size, use_dcp", generate_params()
112
+)
113
def test_backend_selection(
114
device: str,
115
name: str,
116
use_mla: bool,
117
block_size: int,
118
+ use_dcp: bool,
119
):
101
- """Test attention backend selection with valid device-backend pairs."""
120
+ """Supported GPU backends remain selectable when DCP is enabled."""
121
# Create AttentionConfig with the specified backend
122
attention_config = AttentionConfig(backend=AttentionBackendEnum[name])
123
cache_config = CacheConfig(block_size=block_size)
124
vllm_config = VllmConfig(
106
- attention_config=attention_config, cache_config=cache_config
125
+ attention_config=attention_config,
126
+ cache_config=cache_config,
127
+ parallel_config=ParallelConfig(
128
+ # Skip GPU-count autodetection: selection does not launch workers.
129
+ distributed_executor_backend="mp" if use_dcp else None,
130
+ tensor_parallel_size=2 if use_dcp else 1,
131
+ decode_context_parallel_size=2 if use_dcp else 1,
132
+ ),
133
)
134
135
with set_current_vllm_config(vllm_config):
@@ -135,9 +161,15 @@ def test_backend_selection(
161
expected = name
162
assert backend.get_name() == expected
163
else:
138
- backend = get_attn_backend(32, torch.float16, None, use_mla=use_mla)
139
- expected = "ROCM_ATTN"
140
- assert backend.get_name() == expected
164
+ if use_dcp:
165
+ with pytest.raises(ValueError, match="DCP not supported"):
166
+ get_attn_backend(32, torch.float16, None, use_mla=use_mla)
167
+ else:
168
+ backend = get_attn_backend(
169
+ 32, torch.float16, None, use_mla=use_mla
170
+ )
171
+ expected = "ROCM_ATTN"
172
+ assert backend.get_name() == expected
173
174
elif device == "cuda":
175
if CudaPlatform is None:
@@ -230,6 +262,52 @@ def test_backend_selection(
262
assert backend.get_name() == expected
263
264
265
+@pytest.mark.skipif(
266
+ not current_platform.is_cuda_alike(), reason="GPU attention backends"
267
+)
268
+@pytest.mark.parametrize(
269
+ "backend_name",
270
+ [
271
+ "FLASH_ATTN",
272
+ "FLASH_ATTN_DIFFKV",
273
+ "FLASHINFER",
274
+ "TRITON_MLA",
275
+ "ROCM_AITER_MLA",
276
+ "ROCM_AITER_TRITON_MLA",
277
+ "CUTLASS_MLA",
278
+ "FLASHMLA",
279
+ "FLASHINFER_MLA",
280
+ "TOKENSPEED_MLA",
281
+ "FLASH_ATTN_MLA",
282
+ "FLASHMLA_SPARSE",
283
+ "FLASHINFER_MLA_SPARSE",
284
+ ],
285
+)
286
+def test_supported_backend_preserves_dcp_eligibility(backend_name, default_vllm_config):
287
+ """DCP opt-ins preserve eligibility and unrelated configuration restrictions."""
288
+ if backend_name.startswith("ROCM_") and not current_platform.is_rocm():
289
+ pytest.skip("ROCm-specific backend")
290
+ try:
291
+ backend_cls = AttentionBackendEnum[backend_name].get_class()
292
+ except ImportError as error:
293
+ pytest.skip(f"Optional backend dependency unavailable: {error}")
294
+
295
+ config = AttentionSelectorConfig(
296
+ head_size=576 if backend_cls.is_mla() else 128,
297
+ dtype=torch.bfloat16,
298
+ kv_cache_dtype="auto",
299
+ block_size=backend_cls.get_preferred_block_size(64),
300
+ use_mla=backend_cls.is_mla(),
301
+ use_sparse=backend_cls.is_sparse(),
302
+ )
303
+ kwargs = dict(
304
+ device_capability=current_platform.get_device_capability(), **config._asdict()
305
+ )
306
+ invalid_without_dcp = backend_cls.validate_configuration(**kwargs)
307
+ kwargs["use_dcp"] = True
308
+ assert backend_cls.validate_configuration(**kwargs) == invalid_without_dcp
309
+
310
+
311
@pytest.mark.parametrize("device", ["cpu", "cuda", "hip"])
312
def test_fp32_fallback(device: str):
313
"""Test attention backend selection with fp32."""
tests/test_attention_backend_registry.py
+21
@@ -1,5 +1,8 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import torch
4
+
5
+from vllm.platforms.interface import DeviceCapability
6
from vllm.v1.attention.backend import (
7
AttentionBackend,
8
AttentionImpl,
@@ -9,6 +12,7 @@ from vllm.v1.attention.backends.registry import (
12
MambaAttentionBackendEnum,
13
register_backend,
14
)
15
+from vllm.v1.attention.selector import AttentionSelectorConfig
16
17
18
class CustomAttentionImpl(AttentionImpl):
@@ -67,6 +71,23 @@ class CustomMambaAttentionBackend(AttentionBackend):
71
return None
72
73
74
+def test_custom_attention_backend_requires_dcp_opt_in():
75
+ """A new implementation remains usable without implicitly opting into DCP."""
76
+ config = AttentionSelectorConfig(
77
+ head_size=64,
78
+ dtype=torch.float16,
79
+ kv_cache_dtype="auto",
80
+ block_size=16,
81
+ )
82
+ kwargs = dict(device_capability=DeviceCapability(9, 0), **config._asdict())
83
+ assert CustomAttentionBackend.validate_configuration(**kwargs) == []
84
+
85
+ kwargs["use_dcp"] = True
86
+ assert CustomAttentionBackend.validate_configuration(**kwargs) == [
87
+ "DCP not supported"
88
+ ]
89
+
90
+
91
def test_custom_is_not_alias_of_any_backend():
92
# Get all members of AttentionBackendEnum
93
all_backends = list(AttentionBackendEnum)
tests/v1/attention/test_rocm_attention_backends_selection.py
+72
-2
@@ -74,6 +74,7 @@ def test_aiter_unified_attention_capture_preserves_query_start_locations():
74
assert torch.equal(actual.query_start_loc, expected_query_start_loc)
75
76
77
+@pytest.mark.parametrize("use_dcp", [False, True])
78
@pytest.mark.parametrize(
79
"env_vars, selected_backend, expected_backend_path",
80
[
@@ -143,11 +144,12 @@ def test_standard_attention_backend_selection(
144
env_vars,
145
selected_backend,
146
expected_backend_path,
147
+ use_dcp,
148
mock_vllm_config,
149
mock_get_cdna_version,
150
monkeypatch,
151
):
150
- """Test standard attention backend selection with various configurations."""
152
+ """Standard ROCm backends remain selectable without DCP and reject DCP."""
153
# Set environment variables
154
for key, value in env_vars.items():
155
monkeypatch.setenv(key, value)
@@ -176,8 +178,14 @@ def test_standard_attention_backend_selection(
178
use_mla=False,
179
has_sink=False,
180
use_sparse=False,
181
+ use_dcp=use_dcp,
182
)
183
184
+ if use_dcp:
185
+ with pytest.raises(ValueError, match="DCP not supported"):
186
+ RocmPlatform.get_attn_backend_cls(backend_enum, attn_selector_config)
187
+ return
188
+
189
backend_path = RocmPlatform.get_attn_backend_cls(
190
selected_backend=backend_enum, attn_selector_config=attn_selector_config
191
)
@@ -185,6 +193,7 @@ def test_standard_attention_backend_selection(
193
assert backend_path == expected_backend_path
194
195
196
+@pytest.mark.parametrize("use_dcp", [False, True])
197
@pytest.mark.parametrize(
198
"env_vars, selected_backend, block_size, expected_backend_path, should_raise",
199
[
@@ -261,10 +270,11 @@ def test_mla_backend_selection(
270
block_size,
271
expected_backend_path,
272
should_raise,
273
+ use_dcp,
274
mock_vllm_config,
275
monkeypatch,
276
):
267
- """Test MLA backend selection with various configurations."""
277
+ """Dense MLA remains selectable with DCP for valid block sizes."""
278
# Set environment variables
279
for key, value in env_vars.items():
280
monkeypatch.setenv(key, value)
@@ -303,6 +313,7 @@ def test_mla_backend_selection(
313
use_mla=True,
314
has_sink=False,
315
use_sparse=False,
316
+ use_dcp=use_dcp,
317
)
318
attn_selector_config = AttentionSelectorConfig(
319
head_size=128,
@@ -312,6 +323,7 @@ def test_mla_backend_selection(
323
use_mla=True,
324
has_sink=False,
325
use_sparse=False,
326
+ use_dcp=use_dcp,
327
)
328
backend_path = RocmPlatform.get_attn_backend_cls(
329
selected_backend=backend_enum,
@@ -327,6 +339,7 @@ def test_mla_backend_selection(
339
use_mla=True,
340
has_sink=False,
341
use_sparse=False,
342
+ use_dcp=use_dcp,
343
)
344
345
backend_path = RocmPlatform.get_attn_backend_cls(
@@ -336,6 +349,63 @@ def test_mla_backend_selection(
349
assert backend_path == expected_backend_path
350
351
352
+@pytest.mark.parametrize("use_dcp", [False, True])
353
+@pytest.mark.parametrize(
354
+ "selected_backend", [None, AttentionBackendEnum.ROCM_AITER_MLA_SPARSE]
355
+)
356
+def test_sparse_mla_backend_rejects_dcp(selected_backend, use_dcp):
357
+ """Sparse MLA remains selectable without DCP and fails early with DCP."""
358
+ from vllm.platforms.rocm import RocmPlatform
359
+
360
+ selector_config = AttentionSelectorConfig(
361
+ head_size=576,
362
+ dtype=torch.bfloat16,
363
+ kv_cache_dtype="auto",
364
+ block_size=16,
365
+ use_mla=True,
366
+ use_sparse=True,
367
+ use_dcp=use_dcp,
368
+ )
369
+ if use_dcp:
370
+ with pytest.raises(ValueError, match="DCP not supported"):
371
+ RocmPlatform.get_attn_backend_cls(selected_backend, selector_config)
372
+ else:
373
+ assert RocmPlatform.get_attn_backend_cls(selected_backend, selector_config) == (
374
+ AttentionBackendEnum.ROCM_AITER_MLA_SPARSE.get_path()
375
+ )
376
+
377
+
378
+@pytest.mark.parametrize("use_dcp", [False, True])
379
+@pytest.mark.parametrize(
380
+ "selected_backend, head_size, kv_cache_dtype",
381
+ [
382
+ (AttentionBackendEnum.TRITON_ATTN_DIFFKV, 192, "bfloat16"),
383
+ # A 128-dimensional head uses 118 packed bytes, or 59 fp16 elements.
384
+ (AttentionBackendEnum.TURBOQUANT, 59, "turboquant_k3v4_nc"),
385
+ ],
386
+)
387
+def test_specialized_attention_backends_reject_dcp(
388
+ selected_backend, head_size, kv_cache_dtype, use_dcp
389
+):
390
+ """Valid DiffKV and compressed-cache configurations must reject DCP."""
391
+ from vllm.platforms.rocm import RocmPlatform
392
+
393
+ selector_config = AttentionSelectorConfig(
394
+ head_size=head_size,
395
+ dtype=torch.bfloat16,
396
+ kv_cache_dtype=kv_cache_dtype,
397
+ block_size=16,
398
+ use_dcp=use_dcp,
399
+ )
400
+ if use_dcp:
401
+ with pytest.raises(ValueError, match="DCP not supported"):
402
+ RocmPlatform.get_attn_backend_cls(selected_backend, selector_config)
403
+ else:
404
+ assert RocmPlatform.get_attn_backend_cls(selected_backend, selector_config) == (
405
+ selected_backend.get_path()
406
+ )
407
+
408
+
409
def test_aiter_fa_requires_mi3xx(mock_vllm_config):
410
"""Test that ROCM_AITER_FA requires CDNA3+ architecture."""
411
from vllm.platforms.rocm import RocmPlatform
vllm/v1/attention/backend.py
+1
-1
@@ -819,7 +819,7 @@ class AttentionImplBase(ABC, Generic[T]):
819
# Whether the attention impl supports Prefill Context Parallelism.
820
supports_pcp: bool = False
821
# Whether the attention impl supports Decode Context Parallelism.
822
- supports_dcp: bool = True
822
+ supports_dcp: bool = False
823
# Whether the attention impl(or ops) supports MTP
824
# when cp_kv_cache_interleave_size > 1
825
supports_mtp_with_cp_non_trivial_interleave_size: bool = False
vllm/v1/attention/backends/flash_attn.py
+1
@@ -832,6 +832,7 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
832
833
class FlashAttentionImpl(AttentionImpl):
834
can_return_lse_for_decode: bool = True
835
+ supports_dcp: bool = True
836
837
def __init__(
838
self,
vllm/v1/attention/backends/flashinfer.py
+1
@@ -1779,6 +1779,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1779
1780
class FlashInferImpl(AttentionImpl):
1781
can_return_lse_for_decode: bool = True
1782
+ supports_dcp: bool = True
1783
1784
def __init__(
1785
self,
vllm/v1/attention/backends/mla/cutlass_mla.py
+1
@@ -103,6 +103,7 @@ MAX_HEADS = 128
103
104
class CutlassMLAImpl(MLACommonImpl[MLACommonMetadata]):
105
can_return_lse_for_decode: bool = True
106
+ supports_dcp: bool = True
107
108
def __init__(
109
self,
vllm/v1/attention/backends/mla/flashattn_mla.py
+1
@@ -255,6 +255,7 @@ class FlashAttnMLAMetadataBuilder(MLACommonMetadataBuilder[FlashAttnMLAMetadata]
255
256
class FlashAttnMLAImpl(MLACommonImpl[FlashAttnMLAMetadata]):
257
can_return_lse_for_decode: bool = True
258
+ supports_dcp: bool = True
259
260
def __init__(
261
self,
vllm/v1/attention/backends/mla/flashinfer_mla.py
+1
@@ -210,6 +210,7 @@ class FlashInferMLABackend(MLACommonBackend):
210
211
class FlashInferMLAImpl(MLACommonImpl[MLACommonMetadata]):
212
can_return_lse_for_decode: bool = True
213
+ supports_dcp: bool = True
214
# DCP is the only path that consumes LSE. It uses monolithic CuTeDSL,
215
# whose public LSE contract is natural-log.
216
lse_base_on_e: bool = True
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py
+1
@@ -319,6 +319,7 @@ class FlashInferMLASparseImpl(SparseMLACommonImpl[FlashInferMLASparseMetadata]):
319
"""
320
321
can_return_lse_for_decode: bool = True
322
+ supports_dcp: bool = True
323
lse_base_on_e: bool = False
324
325
def __init__(
vllm/v1/attention/backends/mla/flashmla.py
+1
@@ -207,6 +207,7 @@ class FlashMLAMetadataBuilder(MLACommonMetadataBuilder[FlashMLAMetadata]):
207
208
class FlashMLAImpl(MLACommonImpl[FlashMLAMetadata]):
209
can_return_lse_for_decode: bool = True
210
+ supports_dcp: bool = True
211
212
def __init__(
213
self,
vllm/v1/attention/backends/mla/flashmla_sparse.py
+1
@@ -582,6 +582,7 @@ class FlashMLASparseMetadataBuilder(
582
583
class FlashMLASparseImpl(SparseMLACommonImpl[FlashMLASparseMetadata]):
584
can_return_lse_for_decode: bool = True
585
+ supports_dcp: bool = True
586
587
@staticmethod
588
def _compute_fp8_decode_padded_heads(num_heads: int) -> int:
vllm/v1/attention/backends/mla/rocm_aiter_mla.py
+1
@@ -1421,6 +1421,7 @@ class AiterMLAHelper:
1421
class AiterMLAImpl(MLACommonImpl[AiterMLAMetadata]):
1422
# DCP decode paths return natural-log softmax LSE for the cross-rank merge.
1423
can_return_lse_for_decode: bool = True
1424
+ supports_dcp: bool = True
1425
# Measured on gfx950: aiter.mla.mla_decode_fwd(return_lse=True) matches
1426
# logsumexp to fp32 exactly, and merge_mla_segments_triton converts AITER's
1427
# base-2 segment statistics with LOGE2. Stated rather than inherited because
vllm/v1/attention/backends/mla/tokenspeed_mla.py
+1
@@ -154,6 +154,7 @@ class TokenspeedMLABackend(MLACommonBackend):
154
155
class TokenspeedMLAImpl(MLACommonImpl[MLACommonMetadata]):
156
can_return_lse_for_decode: bool = True
157
+ supports_dcp: bool = True
158
# tokenspeed_mla_decode returns LSE in log2 units; its own DCP test merges
159
# partial outputs with exp2(lse).
160
lse_base_on_e: bool = False
vllm/v1/attention/backends/mla/triton_mla.py
+1
@@ -181,6 +181,7 @@ class TritonMLABackend(MLACommonBackend):
181
182
class TritonMLAImpl(MLACommonImpl[MLACommonMetadata]):
183
can_return_lse_for_decode: bool = True
184
+ supports_dcp: bool = True
185
186
def __init__(
187
self,