main
py 894 lines 32.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from contextlib import contextmanager
5 from typing import Literal
6 from unittest.mock import MagicMock, patch
7
8 import pytest
9 import torch
10
11 from vllm.config import (
12 AttentionConfig,
13 CacheConfig,
14 ParallelConfig,
15 VllmConfig,
16 set_current_vllm_config,
17 )
18 from vllm.config.cache import CacheDType
19 from vllm.platforms import current_platform
20 from vllm.platforms.cpu import CpuPlatform
21 from vllm.platforms.interface import DeviceCapability
22
23 if current_platform.is_cuda():
24 from vllm.platforms.cuda import CudaPlatform
25 else:
26 CudaPlatform = None
27
28 if current_platform.is_rocm():
29 from vllm.platforms.rocm import RocmPlatform
30 else:
31 RocmPlatform = None # type: ignore[misc] # Unavailable platform import.
32
33 from vllm.v1.attention.backend import AttentionType
34 from vllm.v1.attention.backends.registry import AttentionBackendEnum
35 from vllm.v1.attention.selector import (
36 AttentionSelectorConfig,
37 _cached_get_attn_backend,
38 get_attn_backend,
39 )
40
41
42 @pytest.fixture(autouse=True)
43 def clear_cache():
44 """Clear lru cache to ensure each test case runs without caching."""
45 _cached_get_attn_backend.cache_clear()
46
47
48 # Define MLA and non-MLA backends separately
49 DEVICE_MLA_BACKENDS = {
50 "cuda": [
51 "TRITON_MLA",
52 "FLASHMLA",
53 "FLASHINFER_MLA",
54 "FLASH_ATTN_MLA",
55 "CUTLASS_MLA",
56 ],
57 "hip": ["TRITON_MLA", "ROCM_AITER_MLA"],
58 "cpu": [],
59 }
60
61 DEVICE_REGULAR_ATTN_BACKENDS = {
62 "cuda": ["FLASHINFER", "FLASH_ATTN"],
63 "hip": ["ROCM_ATTN"],
64 "cpu": ["CPU_ATTN"],
65 }
66
67 DEVICE_MLA_BLOCK_SIZES = {
68 "cuda": [16, 64], # CUDA supports both standard and extended block sizes
69 "hip": [16, 1], # HIP requires special handling for block_size=1
70 # "cpu": [16] # CPU uses fixed block size from test cases
71 "cpu": [], # FIXME(woosuk): Temporarily disable CPU tests
72 }
73
74
75 def generate_params():
76 is_rocm = current_platform.is_rocm()
77 params = []
78 device_list = ["cuda", "cpu"] if not is_rocm else ["hip", "cpu"]
79 for use_mla in [True, False]:
80 for device in device_list:
81 backends = (
82 DEVICE_MLA_BACKENDS[device]
83 if use_mla
84 else DEVICE_REGULAR_ATTN_BACKENDS[device]
85 )
86 for name in backends:
87 block_sizes = (
88 [128]
89 if name == "CUTLASS_MLA"
90 else DEVICE_MLA_BLOCK_SIZES[device]
91 if use_mla
92 else [16]
93 )
94 for block_size in block_sizes:
95 for use_dcp in [False, True] if device != "cpu" else [False]:
96 params.append(
97 pytest.param(
98 device,
99 name,
100 use_mla,
101 block_size,
102 use_dcp,
103 id=(
104 f"{device}_{name}_mla_{str(use_mla)[0]}"
105 f"_blks{block_size}_dcp{use_dcp}"
106 ),
107 )
108 )
109 return params
110
111
112 @pytest.mark.parametrize(
113 "device, name, use_mla, block_size, use_dcp", generate_params()
114 )
115 def test_backend_selection(
116 device: str,
117 name: str,
118 use_mla: bool,
119 block_size: int,
120 use_dcp: bool,
121 ):
122 """Supported GPU backends remain selectable when DCP is enabled."""
123 # Create AttentionConfig with the specified backend
124 attention_config = AttentionConfig(backend=AttentionBackendEnum[name])
125 cache_config = CacheConfig(block_size=block_size)
126 vllm_config = VllmConfig(
127 attention_config=attention_config,
128 cache_config=cache_config,
129 parallel_config=ParallelConfig(
130 # Skip GPU-count autodetection: selection does not launch workers.
131 distributed_executor_backend="mp" if use_dcp else None,
132 tensor_parallel_size=2 if use_dcp else 1,
133 decode_context_parallel_size=2 if use_dcp else 1,
134 ),
135 )
136
137 with set_current_vllm_config(vllm_config):
138 if device == "cpu":
139 with patch("vllm.platforms.current_platform", CpuPlatform()):
140 backend = get_attn_backend(16, torch.float16, None)
141 assert backend.get_name() == "CPU_ATTN"
142
143 elif device == "hip":
144 if RocmPlatform is None:
145 pytest.skip("RocmPlatform not available")
146 with patch("vllm.platforms.current_platform", RocmPlatform()):
147 if use_mla:
148 # ROCm MLA backend logic:
149 # - TRITON_MLA: supported when block_size != 1
150 # - ROCM_AITER_MLA: supported when block_size == 1
151 # If backend is forced but doesn't match block_size,
152 # should raise ValueError
153
154 if name == "TRITON_MLA" and block_size == 1:
155 # TRITON_MLA doesn't support block_size == 1
156 with pytest.raises(ValueError):
157 get_attn_backend(576, torch.float16, None, use_mla=use_mla)
158 else:
159 # Valid backend-block_size combination
160 backend = get_attn_backend(
161 576, torch.float16, None, use_mla=use_mla
162 )
163 expected = name
164 assert backend.get_name() == expected
165 else:
166 if use_dcp:
167 with pytest.raises(ValueError, match="DCP not supported"):
168 get_attn_backend(32, torch.float16, None, use_mla=use_mla)
169 else:
170 backend = get_attn_backend(
171 32, torch.float16, None, use_mla=use_mla
172 )
173 expected = "ROCM_ATTN"
174 assert backend.get_name() == expected
175
176 elif device == "cuda":
177 if CudaPlatform is None:
178 pytest.skip("CudaPlatform not available")
179 with patch("vllm.platforms.current_platform", CudaPlatform()):
180 capability = torch.cuda.get_device_capability()
181 if use_mla:
182 # CUDA MLA backend logic:
183 # - CUTLASS_MLA: only supported with block_size == 128
184 # and Blackwell GPUs (SM 10.x), V1 only
185 # - FLASHINFER_MLA: only supported on Blackwell GPUs
186 # (SM 10.x), V1 only
187 # - FLASHMLA: only supported with block_size == 64
188 # - FLASH_ATTN_MLA: V1 only
189 # - TRITON_MLA: fallback for other cases
190
191 if name == "CUTLASS_MLA":
192 if block_size != 128:
193 # CUTLASS_MLA only supports block_size == 128
194 pytest.skip("CUTLASS_MLA only supports block_size 128")
195 if capability[0] != 10:
196 pytest.skip("CUTLASS MLA is not supported on this platform")
197 backend = get_attn_backend(
198 576, torch.float16, None, use_mla=use_mla
199 )
200 expected = "CUTLASS_MLA"
201 assert backend.get_name() == expected
202 elif name == "FLASHINFER_MLA":
203 if capability[0] != 10:
204 pytest.skip(
205 "FlashInfer MLA is not supported on this platform"
206 )
207 if block_size not in [32, 64]:
208 # FlashInfer MLA only supports block_size 32 or 64
209 pytest.skip(
210 "FlashInfer MLA only supports block_size 32 or 64"
211 )
212 backend = get_attn_backend(
213 576, torch.float16, None, use_mla=use_mla
214 )
215 expected = "FLASHINFER_MLA"
216 assert backend.get_name() == expected
217 elif name == "FLASHMLA":
218 if block_size != 64:
219 # FlashMLA only supports block_size == 64
220 pytest.skip("FlashMLA only supports block_size 64")
221 from vllm.v1.attention.backends.mla.flashmla import (
222 is_flashmla_dense_supported,
223 )
224
225 is_supported, _ = is_flashmla_dense_supported()
226 if not is_supported:
227 pytest.skip("FlashMLA not supported on this platform")
228 backend = get_attn_backend(
229 576,
230 torch.float16,
231 None,
232 use_mla=use_mla,
233 )
234 expected = name
235 assert backend.get_name() == expected
236 elif name == "FLASH_ATTN_MLA":
237 from vllm.v1.attention.backends.fa_utils import (
238 flash_attn_supports_mla,
239 )
240
241 if not flash_attn_supports_mla():
242 pytest.skip(
243 "FlashAttention MLA not supported on this platform"
244 )
245 backend = get_attn_backend(
246 576, torch.float16, None, use_mla=use_mla
247 )
248 expected = "FLASH_ATTN_MLA"
249 assert backend.get_name() == expected
250 else:
251 # TRITON_MLA or other fallback
252 backend = get_attn_backend(
253 576, torch.float16, None, use_mla=use_mla
254 )
255 expected = "TRITON_MLA"
256 assert backend.get_name() == expected
257 elif name == "FLASHINFER":
258 backend = get_attn_backend(64, torch.float16, None, use_mla=use_mla)
259 expected = "FLASHINFER"
260 assert backend.get_name() == expected
261 elif name == "FLASH_ATTN":
262 backend = get_attn_backend(32, torch.float16, None, use_mla=use_mla)
263 expected = "FLASH_ATTN"
264 assert backend.get_name() == expected
265
266
267 @pytest.mark.skipif(
268 not current_platform.is_cuda_alike(), reason="GPU attention backends"
269 )
270 @pytest.mark.parametrize(
271 "backend_name",
272 [
273 "FLASH_ATTN",
274 "FLASH_ATTN_DIFFKV",
275 "FLASHINFER",
276 "TRITON_MLA",
277 "ROCM_AITER_MLA",
278 "ROCM_AITER_TRITON_MLA",
279 "CUTLASS_MLA",
280 "FLASHMLA",
281 "FLASHINFER_MLA",
282 "TOKENSPEED_MLA",
283 "FLASH_ATTN_MLA",
284 "FLASHMLA_SPARSE",
285 "FLASHINFER_MLA_SPARSE",
286 ],
287 )
288 def test_supported_backend_preserves_dcp_eligibility(backend_name, default_vllm_config):
289 """DCP opt-ins preserve eligibility and unrelated configuration restrictions."""
290 if backend_name.startswith("ROCM_") and not current_platform.is_rocm():
291 pytest.skip("ROCm-specific backend")
292 try:
293 backend_cls = AttentionBackendEnum[backend_name].get_class()
294 except ImportError as error:
295 pytest.skip(f"Optional backend dependency unavailable: {error}")
296
297 config = AttentionSelectorConfig(
298 head_size=576 if backend_cls.is_mla() else 128,
299 dtype=torch.bfloat16,
300 kv_cache_dtype="auto",
301 block_size=backend_cls.get_preferred_block_size(64),
302 use_mla=backend_cls.is_mla(),
303 use_sparse=backend_cls.is_sparse(),
304 )
305 kwargs = dict(
306 device_capability=current_platform.get_device_capability(), **config._asdict()
307 )
308 invalid_without_dcp = backend_cls.validate_configuration(**kwargs)
309 kwargs["use_dcp"] = True
310 assert backend_cls.validate_configuration(**kwargs) == invalid_without_dcp
311
312
313 @pytest.mark.parametrize("device", ["cpu", "cuda", "hip"])
314 def test_fp32_fallback(device: str):
315 """Test attention backend selection with fp32."""
316 # Use default config (no backend specified)
317 vllm_config = VllmConfig()
318
319 with set_current_vllm_config(vllm_config):
320 if device == "cpu":
321 with patch("vllm.platforms.current_platform", CpuPlatform()):
322 backend = get_attn_backend(16, torch.float32, None)
323 assert backend.get_name() == "CPU_ATTN"
324
325 elif device == "cuda":
326 if CudaPlatform is None:
327 pytest.skip("CudaPlatform not available")
328 with patch("vllm.platforms.current_platform", CudaPlatform()):
329 backend = get_attn_backend(16, torch.float32, None)
330 assert backend.get_name() == "FLEX_ATTENTION"
331
332 elif device == "hip":
333 if RocmPlatform is None:
334 pytest.skip("RocmPlatform not available")
335 # ROCm backends do not support head_size=16 (minimum is 32).
336 # No known HuggingFace transformer model uses head_size=16.
337 # Revisit if a real model with this head size is identified
338 # and accuracy-tested.
339 with (
340 patch("vllm.platforms.current_platform", RocmPlatform()),
341 pytest.raises(ValueError, match="No valid attention backend"),
342 ):
343 get_attn_backend(16, torch.float32, None)
344
345
346 def test_invalid_backend():
347 """Test that invalid attention backend names raise ValueError."""
348 with (
349 pytest.raises(ValueError),
350 ):
351 # Invalid backend name should raise ValueError when creating enum
352 AttentionConfig(backend=AttentionBackendEnum["INVALID"])
353
354
355 @pytest.mark.parametrize("auto_value", ["auto", "AUTO", "Auto"])
356 def test_auto_backend_string(auto_value: str):
357 """Test that 'auto' string value triggers automatic backend selection."""
358 # Using "auto" should result in backend=None (automatic selection)
359 attention_config = AttentionConfig(backend=auto_value)
360 assert attention_config.backend is None
361
362
363 def test_auto_backend_selection_behavior():
364 """Test that 'auto' backend behaves same as None (automatic selection)."""
365 # Create config with explicit "auto"
366 auto_config = AttentionConfig(backend="auto")
367
368 # Create config with None (default)
369 none_config = AttentionConfig(backend=None)
370
371 # Both should have backend=None
372 assert auto_config.backend is None
373 assert none_config.backend is None
374
375 # Both configs should result in the same automatic backend selection
376 vllm_config_auto = VllmConfig(attention_config=auto_config)
377 vllm_config_none = VllmConfig(attention_config=none_config)
378
379 with (
380 set_current_vllm_config(vllm_config_auto),
381 patch("vllm.platforms.current_platform", CpuPlatform()),
382 ):
383 backend_auto = get_attn_backend(16, torch.float16, None)
384
385 _cached_get_attn_backend.cache_clear()
386
387 with (
388 set_current_vllm_config(vllm_config_none),
389 patch("vllm.platforms.current_platform", CpuPlatform()),
390 ):
391 backend_none = get_attn_backend(16, torch.float16, None)
392
393 # Both should select the same backend
394 assert backend_auto.get_name() == backend_none.get_name()
395
396
397 @pytest.mark.parametrize(
398 "backend_name,flash_attn_version,should_succeed",
399 [
400 ("FLASH_ATTN", 3, True), # FA3 supports per-head quant scales
401 ("FLASH_ATTN", 2, False), # FA2 does not support per-head quant scales
402 ("FLASHINFER", None, False), # FlashInfer does not support
403 ("FLEX_ATTENTION", None, False), # Flex does not support
404 ],
405 )
406 @pytest.mark.skipif(
407 current_platform.is_rocm(),
408 reason="Attention backend FA3 is not supported on ROCm. This test can't succeed.",
409 )
410 def test_per_head_quant_scales_backend_selection(
411 backend_name: str, flash_attn_version: Literal[2, 3, 4] | None, should_succeed: bool
412 ):
413 """Test backend selection when use_per_head_quant_scales=True."""
414 # Clear cache to ensure fresh backend selection
415 _cached_get_attn_backend.cache_clear()
416
417 attention_config = AttentionConfig(
418 backend=AttentionBackendEnum[backend_name],
419 flash_attn_version=flash_attn_version,
420 )
421 cache_config = CacheConfig(block_size=64)
422 vllm_config = VllmConfig(
423 attention_config=attention_config, cache_config=cache_config
424 )
425
426 if CudaPlatform is None:
427 pytest.skip("CudaPlatform not available")
428 with (
429 set_current_vllm_config(vllm_config),
430 patch("vllm.platforms.current_platform", CudaPlatform()),
431 ):
432 if backend_name == "FLASH_ATTN" and flash_attn_version == 3:
433 if not torch.cuda.is_available():
434 pytest.skip("FA3 requires CUDA")
435 capability = torch.cuda.get_device_capability()
436 if capability[0] != 9:
437 pytest.skip("FA3 is only supported on Hopper (SM 9.x) GPUs")
438
439 if should_succeed:
440 backend = get_attn_backend(
441 head_size=128,
442 dtype=torch.float16,
443 kv_cache_dtype="fp8",
444 use_per_head_quant_scales=True,
445 )
446 assert backend.get_name() == backend_name
447 else:
448 with pytest.raises(ValueError) as exc_info:
449 get_attn_backend(
450 head_size=128,
451 dtype=torch.float16,
452 kv_cache_dtype="fp8",
453 use_per_head_quant_scales=True,
454 )
455 assert backend_name in str(exc_info.value)
456
457
458 @pytest.mark.parametrize(
459 "backend_name,use_non_causal,should_succeed",
460 [
461 ("FLASH_ATTN", True, True), # FlashAttn supports non-causal
462 ("FLASH_ATTN", False, True), # FlashAttn also works with causal
463 ]
464 + (
465 [
466 ("FLASHINFER", True, True), # FlashInfer supports non-causal
467 ("FLASHINFER", False, True), # FlashInfer works with causal
468 ]
469 if CudaPlatform is not None
470 else []
471 ),
472 )
473 def test_non_causal_backend_selection(
474 backend_name: str, use_non_causal: bool, should_succeed: bool
475 ):
476 """Test that use_non_causal on AttentionConfig controls backend filtering.
477
478 DFlashProposer sets use_non_causal=True on the draft model's
479 AttentionConfig so only non-causal-capable backends are selected.
480 The target model keeps use_non_causal=False (default) and can use
481 any backend.
482 """
483 _cached_get_attn_backend.cache_clear()
484
485 attention_config = AttentionConfig(
486 backend=AttentionBackendEnum[backend_name],
487 use_non_causal=use_non_causal,
488 )
489 cache_config = CacheConfig(block_size=16)
490 vllm_config = VllmConfig(
491 attention_config=attention_config, cache_config=cache_config
492 )
493
494 platform = CudaPlatform or RocmPlatform
495 if platform is None:
496 pytest.skip("CudaPlatform and RocmPlatform are not available")
497 with (
498 set_current_vllm_config(vllm_config),
499 patch("vllm.platforms.current_platform", platform()),
500 ):
501 if should_succeed:
502 backend = get_attn_backend(
503 head_size=128,
504 dtype=torch.float16,
505 kv_cache_dtype=None,
506 )
507 assert backend.get_name() == backend_name
508 else:
509 with pytest.raises(ValueError) as exc_info:
510 get_attn_backend(
511 head_size=128,
512 dtype=torch.float16,
513 kv_cache_dtype=None,
514 )
515 assert "non-causal" in str(exc_info.value).lower()
516
517
518 def test_non_causal_autoselect_backend():
519 """Test that when backend=None with use_non_causal=True, auto-selection
520 picks a compatible backend.
521
522 This simulates the DFlash scenario where the user doesn't specify
523 --attention-backend or --speculative-config.attention_backend.
524 The drafter inherits backend=None and auto-selects a backend that
525 supports non-causal attention.
526 """
527 _cached_get_attn_backend.cache_clear()
528
529 attention_config = AttentionConfig(
530 backend=None,
531 use_non_causal=True,
532 )
533 cache_config = CacheConfig(block_size=16)
534 vllm_config = VllmConfig(
535 attention_config=attention_config, cache_config=cache_config
536 )
537
538 if CudaPlatform is None:
539 pytest.skip("CudaPlatform not available")
540 with (
541 set_current_vllm_config(vllm_config),
542 patch("vllm.platforms.current_platform", CudaPlatform()),
543 ):
544 backend = get_attn_backend(
545 head_size=128,
546 dtype=torch.float16,
547 kv_cache_dtype=None,
548 )
549 assert backend.supports_non_causal()
550
551
552 @pytest.mark.parametrize(
553 "kv_cache_dtype",
554 [
555 "fp8_e5m2",
556 "fp8_ds_mla",
557 "fp8_inc",
558 "nvfp4",
559 "nvfp4_4over6",
560 "fp8_per_token_head",
561 "int8_per_token_head",
562 ],
563 )
564 def test_flash_attn_rejects_unhandled_kv_cache_dtypes(kv_cache_dtype: CacheDType):
565 """FlashAttentionBackend must not claim support for kv_cache dtypes
566 that it cannot handle."""
567 from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
568
569 assert not FlashAttentionBackend.supports_kv_cache_dtype(kv_cache_dtype)
570
571
572 @pytest.mark.parametrize("kv_cache_dtype", ["fp8", "fp8_e4m3"])
573 def test_flash_attn_accepts_handled_fp8_variants(
574 kv_cache_dtype: CacheDType, monkeypatch: pytest.MonkeyPatch
575 ):
576 """FlashAttentionBackend must accept the two fp8 dtypes it can actually
577 handle: 'fp8' (alias for fp8_e4m3fn) and 'fp8_e4m3'."""
578 import vllm.v1.attention.backends.fa_utils as fa_utils_mod
579 from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
580
581 # The fp8 decision is made in fa_utils, using its own current_platform
582 # binding, so patch is_xpu there (not on flash_attn's) to stay robust to
583 # import order across earlier tests that patch vllm.platforms.current_platform.
584 monkeypatch.setattr(fa_utils_mod.current_platform, "is_xpu", lambda: True)
585 assert FlashAttentionBackend.supports_kv_cache_dtype(kv_cache_dtype)
586
587
588 blackwell_only = pytest.mark.skipif(
589 not current_platform.is_cuda(), reason="FA4 is CUDA-only"
590 )
591
592
593 @contextmanager
594 def _blackwell(vllm_config=None):
595 platform = MagicMock()
596 platform.is_xpu.return_value = False
597 platform.is_rocm.return_value = False
598 platform.get_device_capability.return_value = DeviceCapability(10, 0)
599 with (
600 patch("vllm.v1.attention.backends.fa_utils.current_platform", platform),
601 patch(
602 "vllm.vllm_flash_attn.flash_attn_interface.is_fa_version_supported",
603 return_value=True,
604 ),
605 patch("vllm.config.get_current_vllm_config_or_none", return_value=vllm_config),
606 patch(
607 "vllm.v1.attention.backends.flash_attn.get_current_vllm_config_or_none",
608 return_value=vllm_config,
609 ),
610 ):
611 yield
612
613
614 def _hd256_config(
615 *,
616 is_mm_prefix_lm=False,
617 rswa_window=None,
618 dcp_size=1,
619 softcap=None,
620 head_size=256,
621 cache_dtype="auto",
622 ):
623 vllm_config = MagicMock()
624 vllm_config.attention_config.flash_attn_version = None
625 vllm_config.model_config.is_mm_prefix_lm = is_mm_prefix_lm
626 vllm_config.model_config.rswa_window = rswa_window
627 vllm_config.model_config.hf_text_config.attn_logit_softcapping = softcap
628 vllm_config.model_config.get_head_size.return_value = head_size
629 vllm_config.cache_config.cache_dtype = cache_dtype
630 vllm_config.parallel_config.decode_context_parallel_size = dcp_size
631 return vllm_config
632
633
634 @blackwell_only
635 @pytest.mark.parametrize(
636 "kwargs,config_kwargs,expected",
637 [
638 ({}, {}, 4),
639 ({"supports_fa4_hd256": False}, {}, 2),
640 ({"has_sinks": True}, {}, 2),
641 ({"requires_softcap": True}, {}, 2),
642 # Larger block sizes normalize to 128.
643 ({"kv_cache_block_size": 16}, {}, 2),
644 ({"kv_cache_block_size": 64}, {}, 2),
645 ({"kv_cache_block_size": 256}, {}, 4),
646 ({}, {"is_mm_prefix_lm": True}, 2),
647 ({}, {"rswa_window": 512}, 2),
648 ({}, {"dcp_size": 2}, 2),
649 ({}, {"softcap": 50.0}, 2),
650 ({}, {"cache_dtype": "fp8"}, 2),
651 ({"head_size": 128, "kv_cache_block_size": 16}, {}, 4),
652 ({"head_size": 192, "head_size_v": 128, "kv_cache_block_size": 16}, {}, 4),
653 ({"head_size": 256, "head_size_v": 128, "kv_cache_block_size": 16}, {}, 2),
654 ({"head_size": 256, "head_size_v": 64}, {}, 2),
655 ],
656 )
657 def test_fa4_hd256_fallback_matrix(kwargs, config_kwargs, expected):
658 from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
659
660 kwargs = {
661 "head_size": 256,
662 "kv_cache_block_size": 128,
663 "supports_fa4_hd256": True,
664 **kwargs,
665 }
666 with _blackwell(_hd256_config(head_size=kwargs["head_size"], **config_kwargs)):
667 assert get_flash_attn_version(**kwargs) == expected
668
669
670 @blackwell_only
671 @pytest.mark.parametrize(
672 "backend_name,config_kwargs,expected",
673 [
674 ("FLASH_ATTN", {}, 128),
675 ("FLASH_ATTN", {"softcap": 50.0}, 16),
676 ("FLASH_ATTN", {"head_size": 128}, 16),
677 ("FLASH_ATTN", {"cache_dtype": "fp8"}, 16),
678 ("FLASH_ATTN_DIFFKV", {}, 16),
679 ],
680 )
681 def test_fa4_hd256_block_size_advertisement(
682 backend_name: str, config_kwargs: dict, expected: int
683 ):
684 from vllm.v1.attention.backend import MultipleOf
685 from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
686 from vllm.v1.attention.backends.flash_attn_diffkv import (
687 FlashAttentionDiffKVBackend,
688 )
689
690 backend = {
691 "FLASH_ATTN": FlashAttentionBackend,
692 "FLASH_ATTN_DIFFKV": FlashAttentionDiffKVBackend,
693 }[backend_name]
694 vllm_config = _hd256_config(**config_kwargs)
695 with _blackwell(vllm_config):
696 (size,) = backend.get_supported_kernel_block_sizes()
697 preferred = backend.get_preferred_block_size(16)
698 assert preferred == expected
699 if expected == 128:
700 assert size == 128
701 else:
702 assert isinstance(size, MultipleOf) and size.base == expected
703
704
705 @blackwell_only
706 def test_fa4_hd256_mm_prefix_deselects_flash_attn():
707 from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
708
709 with _blackwell(_hd256_config(is_mm_prefix_lm=True)):
710 assert (
711 FlashAttentionBackend.supports_combination(
712 head_size=256,
713 dtype=torch.bfloat16,
714 kv_cache_dtype=None,
715 block_size=128,
716 use_mla=False,
717 has_sink=False,
718 use_sparse=False,
719 use_mm_prefix=True,
720 device_capability=DeviceCapability(10, 0),
721 )
722 is not None
723 )
724
725
726 @pytest.mark.skipif(
727 not current_platform.is_cuda()
728 or not current_platform.is_device_capability_family(100),
729 reason="requires a Blackwell GPU",
730 )
731 @pytest.mark.parametrize(
732 "attn_type,sliding_window,expected",
733 [
734 (AttentionType.DECODER, None, 4),
735 (AttentionType.DECODER, 512, 4),
736 # Local encoder attention lacks the required seqused tensors.
737 (AttentionType.ENCODER_ONLY, None, 4),
738 (AttentionType.ENCODER_ONLY, 512, 2),
739 ],
740 )
741 def test_fa4_hd256_impl_selection(attn_type, sliding_window, expected):
742 from vllm.v1.attention.backends.flash_attn import FlashAttentionImpl
743
744 # Implementations are constructed before the platform updates the block size.
745 with set_current_vllm_config(VllmConfig()):
746 impl = FlashAttentionImpl(
747 num_heads=8,
748 head_size=256,
749 scale=0.0625,
750 num_kv_heads=8,
751 alibi_slopes=None,
752 sliding_window=sliding_window,
753 kv_cache_dtype="auto",
754 attn_type=attn_type,
755 )
756 assert impl.vllm_flash_attn_version == expected
757 assert impl.fa4_hd256 == (expected == 4)
758
759
760 @pytest.fixture
761 def blackwell_selection():
762 with (
763 patch.object(
764 type(current_platform),
765 "get_device_capability",
766 return_value=DeviceCapability(10, 0),
767 ),
768 patch(
769 "vllm.v1.attention.backends.fa_utils.is_fa_version_supported",
770 return_value=True,
771 ),
772 ):
773 yield
774
775
776 @pytest.fixture
777 def hopper_selection():
778 with (
779 patch.object(
780 type(current_platform),
781 "get_device_capability",
782 return_value=DeviceCapability(9, 0),
783 ),
784 patch(
785 "vllm.v1.attention.backends.fa_utils.is_fa_version_supported",
786 return_value=True,
787 ),
788 ):
789 yield
790
791
792 @blackwell_only
793 @pytest.mark.parametrize("use_mm_prefix", [False, True])
794 @pytest.mark.parametrize("flash_attn_version", [None, 3, 4])
795 def test_hopper_mm_prefix_selects_triton_flash_attn(
796 use_mm_prefix, flash_attn_version, hopper_selection
797 ):
798 """Hopper selects the composite when its causal route resolves FA3 or FA4."""
799 from vllm.engine.arg_utils import EngineArgs
800
801 config = EngineArgs(
802 model="google/gemma-4-31B-it",
803 dtype="bfloat16",
804 attention_config=AttentionConfig(flash_attn_version=flash_attn_version),
805 ).create_engine_config()
806 with set_current_vllm_config(config):
807 backend = get_attn_backend(
808 256, torch.bfloat16, None, use_mm_prefix=use_mm_prefix
809 )
810 expected = "TRITON_FLASH_ATTN" if use_mm_prefix else "FLASH_ATTN"
811 assert backend.get_name() == expected
812
813
814 @blackwell_only
815 @pytest.mark.parametrize("use_mm_prefix", [False, True])
816 @pytest.mark.parametrize("kv_cache_dtype", [None, "fp8_e4m3"])
817 def test_mm_prefix_selects_composite_without_changing_causal_default(
818 use_mm_prefix, kv_cache_dtype, blackwell_selection
819 ):
820 """The image-mask requirement must reach CUDA's automatic backend priority."""
821 from vllm.engine.arg_utils import EngineArgs
822
823 config = EngineArgs(
824 model="google/gemma-4-31B-it", dtype="bfloat16"
825 ).create_engine_config()
826 with set_current_vllm_config(config):
827 backend = get_attn_backend(
828 256, torch.bfloat16, kv_cache_dtype, use_mm_prefix=use_mm_prefix
829 )
830 expected = "TRITON_FLASHINFER" if use_mm_prefix else "FLASHINFER"
831 assert backend.get_name() == expected
832
833
834 @blackwell_only
835 @pytest.mark.parametrize(
836 "feature_kwargs,reason",
837 [
838 ({"use_rswa": True}, "R-SWA not supported"),
839 ({"kv_cache_dtype": "int8_per_token_head"}, "kv_cache_dtype not supported"),
840 ({"block_size": 16}, "block_size not supported"),
841 ({"use_dcp": True}, "DCP not supported"),
842 ({"use_pcp": True}, "PCP not supported"),
843 ({"use_adaptive_verification": True}, "device-cpu query lens mismatch"),
844 ],
845 )
846 def test_composite_rejects_features_not_shared_by_both_routes(feature_kwargs, reason):
847 """Automatic selection must not admit features only one child can execute."""
848 from vllm.engine.arg_utils import EngineArgs
849 from vllm.v1.attention.backends.triton_flashinfer import TritonFlashInferBackend
850
851 config = EngineArgs(
852 model="google/gemma-4-31B-it", dtype="bfloat16"
853 ).create_engine_config()
854 args = dict(
855 head_size=256,
856 dtype=torch.bfloat16,
857 kv_cache_dtype="auto",
858 block_size=128,
859 use_mla=False,
860 has_sink=False,
861 use_sparse=False,
862 use_mm_prefix=True,
863 use_per_head_quant_scales=False,
864 device_capability=DeviceCapability(10, 0),
865 attn_type=AttentionType.DECODER,
866 )
867 args.update(feature_kwargs)
868 with set_current_vllm_config(config):
869 reasons = TritonFlashInferBackend.validate_configuration(**args)
870 assert any(reason in message for message in reasons), reasons
871
872
873 @blackwell_only
874 def test_rswa_selection_does_not_reuse_causal_result(blackwell_selection):
875 """An R-SWA requirement must invalidate a previously cached causal selection."""
876 from vllm.engine.arg_utils import EngineArgs
877
878 config = EngineArgs(
879 model="google/gemma-4-31B-it",
880 dtype="bfloat16",
881 attention_config=AttentionConfig(
882 backend=AttentionBackendEnum.TRITON_FLASHINFER
883 ),
884 ).create_engine_config()
885 with set_current_vllm_config(config):
886 assert (
887 get_attn_backend(256, torch.bfloat16, None).get_name()
888 == "TRITON_FLASHINFER"
889 )
890 config.model_config.model_arch_config.rswa_window = 128
891 with pytest.raises(ValueError, match="R-SWA"):
892 get_attn_backend(256, torch.bfloat16, None)
893 config.attention_config.backend = AttentionBackendEnum.TRITON_ATTN
894 assert get_attn_backend(256, torch.bfloat16, None).get_name() == "TRITON_ATTN"