main
py 542 lines 19.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for MLA prefill backend selector."""
4
5 from unittest.mock import MagicMock, patch
6
7 import pytest
8 import torch
9
10 from vllm.config import AttentionConfig, ModelConfig, VllmConfig
11 from vllm.platforms import current_platform
12 from vllm.platforms.interface import DeviceCapability
13 from vllm.v1.attention.backends.mla.prefill.base import MLADimensions
14 from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
15 from vllm.v1.attention.backends.mla.prefill.selector import (
16 MLAPrefillSelectorConfig,
17 _auto_select_mla_prefill_backend,
18 _get_mla_prefill_backend_priorities,
19 get_mla_prefill_backend,
20 )
21
22
23 @pytest.fixture(autouse=True)
24 def clear_cache():
25 """Clear lru cache to ensure each test case runs without caching."""
26 _auto_select_mla_prefill_backend.cache_clear()
27
28
29 def _make_mock_model_config(
30 qk_nope_head_dim: int = 128,
31 qk_rope_head_dim: int = 64,
32 v_head_dim: int = 128,
33 dtype: torch.dtype = torch.bfloat16,
34 ) -> ModelConfig:
35 mock_config = MagicMock(spec=ModelConfig)
36 mock_config.dtype = dtype
37 mock_config.hf_text_config = MagicMock()
38 mock_config.hf_text_config.qk_nope_head_dim = qk_nope_head_dim
39 mock_config.hf_text_config.qk_rope_head_dim = qk_rope_head_dim
40 mock_config.hf_text_config.v_head_dim = v_head_dim
41 return mock_config
42
43
44 def _make_vllm_config(
45 model_config: ModelConfig | None = None,
46 mla_prefill_backend: MLAPrefillBackendEnum | None = None,
47 ) -> VllmConfig:
48 if model_config is None:
49 model_config = _make_mock_model_config()
50
51 attention_config = AttentionConfig(mla_prefill_backend=mla_prefill_backend)
52 mock_vllm_config = MagicMock(spec=VllmConfig)
53 mock_vllm_config.model_config = model_config
54 mock_vllm_config.attention_config = attention_config
55 return mock_vllm_config
56
57
58 class TestGetMLAPrefillBackend:
59 """Tests for get_mla_prefill_backend (public API)."""
60
61 def test_cpu_uses_sdpa_prefill(self):
62 vllm_config = _make_vllm_config()
63 zen_cls = MLAPrefillBackendEnum.ZEN_CPU.get_class()
64
65 # A Zen CPU with zentorch selects the Zen backend, so expect whichever
66 # one this host offers.
67 expected = (
68 zen_cls if zen_cls.is_available() else MLAPrefillBackendEnum.CPU.get_class()
69 )
70
71 with patch("vllm.platforms.current_platform") as mock_platform:
72 mock_platform.is_cpu.return_value = True
73
74 backend = get_mla_prefill_backend(vllm_config)
75 assert backend is expected
76
77 def test_zen_cpu_prefers_zentorch_prefill(self):
78 vllm_config = _make_vllm_config()
79 zen_cls = MLAPrefillBackendEnum.ZEN_CPU.get_class()
80
81 with (
82 patch("vllm.platforms.current_platform") as mock_platform,
83 patch.object(zen_cls, "is_available", return_value=True),
84 ):
85 mock_platform.is_cpu.return_value = True
86
87 backend = get_mla_prefill_backend(vllm_config)
88 assert backend is zen_cls
89
90 def test_zen_cpu_falls_back_when_zentorch_unavailable(self):
91 vllm_config = _make_vllm_config()
92 zen_cls = MLAPrefillBackendEnum.ZEN_CPU.get_class()
93
94 with (
95 patch("vllm.platforms.current_platform") as mock_platform,
96 patch.object(zen_cls, "is_available", return_value=False),
97 ):
98 mock_platform.is_cpu.return_value = True
99
100 backend = get_mla_prefill_backend(vllm_config)
101 assert backend is MLAPrefillBackendEnum.CPU.get_class()
102
103 def test_no_device_capability_returns_flash_attn(self):
104 vllm_config = _make_vllm_config()
105
106 class FlashAttnBackend:
107 @staticmethod
108 def get_name():
109 return "FLASH_ATTN"
110
111 with (
112 patch("vllm.platforms.current_platform") as mock_platform,
113 patch.object(
114 MLAPrefillBackendEnum.FLASH_ATTN,
115 "get_class",
116 return_value=FlashAttnBackend,
117 ),
118 ):
119 mock_platform.get_device_capability.return_value = None
120 mock_platform.is_cpu.return_value = False
121
122 backend = get_mla_prefill_backend(vllm_config)
123 assert backend.get_name() == "FLASH_ATTN"
124
125 def test_explicit_flash_attn_selection(self):
126 try:
127 flash_attn_cls = MLAPrefillBackendEnum.FLASH_ATTN.get_class()
128 except ImportError:
129 pytest.skip("FLASH_ATTN backend not available")
130 return
131
132 vllm_config = _make_vllm_config(
133 mla_prefill_backend=MLAPrefillBackendEnum.FLASH_ATTN,
134 )
135
136 with patch("vllm.platforms.current_platform") as mock_platform:
137 mock_platform.is_cpu.return_value = False
138 mock_platform.get_device_capability.return_value = DeviceCapability(
139 major=9, minor=0
140 )
141
142 with patch.object(
143 flash_attn_cls,
144 "validate_configuration",
145 return_value=[],
146 ):
147 backend = get_mla_prefill_backend(vllm_config)
148 assert backend.get_name() == "FLASH_ATTN"
149
150 def test_explicit_backend_invalid_raises_error(self):
151 vllm_config = _make_vllm_config(
152 mla_prefill_backend=MLAPrefillBackendEnum.FLASHINFER,
153 )
154
155 with patch("vllm.platforms.current_platform") as mock_platform:
156 mock_platform.is_cpu.return_value = False
157 mock_platform.get_device_capability.return_value = DeviceCapability(
158 major=9, minor=0
159 )
160
161 with pytest.raises(ValueError, match="is not valid"):
162 get_mla_prefill_backend(vllm_config)
163
164 def test_explicit_backend_import_error_raises(self):
165 vllm_config = _make_vllm_config(
166 mla_prefill_backend=MLAPrefillBackendEnum.TRTLLM_RAGGED,
167 )
168
169 with patch("vllm.platforms.current_platform") as mock_platform:
170 mock_platform.is_cpu.return_value = False
171 mock_platform.get_device_capability.return_value = DeviceCapability(
172 major=10, minor=0
173 )
174
175 with (
176 patch.object(
177 MLAPrefillBackendEnum.TRTLLM_RAGGED,
178 "get_class",
179 side_effect=ImportError("trtllm not installed"),
180 ),
181 pytest.raises(ValueError, match="is not valid"),
182 ):
183 get_mla_prefill_backend(vllm_config)
184
185 @pytest.mark.parametrize(
186 ("qk_nope_head_dim", "v_head_dim"),
187 [(128, 128), (192, 256), (64, 128)],
188 ids=["deepseek", "glm", "mistral_s4"],
189 )
190 def test_auto_selection_on_hopper(self, qk_nope_head_dim: int, v_head_dim: int):
191 try:
192 flash_attn_cls = MLAPrefillBackendEnum.FLASH_ATTN.get_class()
193 except ImportError:
194 pytest.skip("FLASH_ATTN backend not available")
195 return
196
197 vllm_config = _make_vllm_config(
198 _make_mock_model_config(
199 qk_nope_head_dim=qk_nope_head_dim,
200 v_head_dim=v_head_dim,
201 )
202 )
203
204 with (
205 patch("vllm.platforms.current_platform") as mock_platform,
206 patch.object(flash_attn_cls, "is_available", return_value=True),
207 patch(
208 "vllm.v1.attention.backends.mla.prefill.flash_attn."
209 "get_flash_attn_version",
210 return_value=3,
211 ),
212 ):
213 mock_platform.is_cpu.return_value = False
214 mock_platform.get_device_capability.return_value = DeviceCapability(
215 major=9, minor=0
216 )
217 mock_platform.is_rocm.return_value = False
218
219 backend = get_mla_prefill_backend(vllm_config)
220 assert backend.get_name() == "FLASH_ATTN"
221
222
223 class TestAutoSelectMLAPrefillBackend:
224 """Tests for fallback and error paths in auto-selection."""
225
226 def test_blackwell_glm_dimensions_use_trtllm(self):
227 capability = DeviceCapability(major=10, minor=0)
228 selector_config = MLAPrefillSelectorConfig(
229 dtype=torch.bfloat16,
230 mla_dimensions=MLADimensions(
231 qk_nope_head_dim=192,
232 qk_rope_head_dim=64,
233 v_head_dim=256,
234 ),
235 )
236
237 try:
238 flash_attn_cls = MLAPrefillBackendEnum.FLASH_ATTN.get_class()
239 trtllm_cls = MLAPrefillBackendEnum.TRTLLM_RAGGED.get_class()
240 except ImportError:
241 pytest.skip("MLA prefill backend not available")
242 return
243
244 with (
245 patch("vllm.platforms.current_platform") as mock_platform,
246 patch.object(flash_attn_cls, "is_available", return_value=True),
247 patch.object(trtllm_cls, "validate_configuration", return_value=[]),
248 ):
249 # Force the non-ROCm priority on the Blackwell.
250 mock_platform.is_rocm.return_value = False
251 backend = _auto_select_mla_prefill_backend(
252 capability,
253 selector_config,
254 )
255 assert backend.get_name() == "TRTLLM_RAGGED"
256
257 def test_all_fail_raises_error(self):
258 capability = DeviceCapability(major=10, minor=0)
259 selector_config = MLAPrefillSelectorConfig(
260 dtype=torch.bfloat16,
261 mla_dimensions=MLADimensions(
262 qk_nope_head_dim=128,
263 qk_rope_head_dim=64,
264 v_head_dim=128,
265 ),
266 )
267
268 def mock_get_class(backend_enum): # noqa: ARG001
269 cls = MagicMock()
270 cls.validate_configuration.return_value = ["not available"]
271 return cls
272
273 with patch.object(MLAPrefillBackendEnum, "get_class", mock_get_class):
274 _auto_select_mla_prefill_backend.cache_clear()
275 with pytest.raises(ValueError, match="No valid MLA"):
276 _auto_select_mla_prefill_backend(
277 capability,
278 selector_config,
279 )
280
281
282 class TestBackendValidation:
283 """Tests for backend validation logic."""
284
285 def test_backend_supported_dimension_validation(self):
286 try:
287 from vllm.v1.attention.backends.mla.prefill.flashinfer import (
288 FlashInferPrefillBackend,
289 )
290 from vllm.v1.attention.backends.mla.prefill.trtllm_ragged import (
291 TrtllmRaggedPrefillBackend,
292 )
293 except ImportError:
294 pytest.skip("MLA prefill backend not available")
295 return
296
297 capability = DeviceCapability(major=10, minor=0)
298 selector_config = MLAPrefillSelectorConfig(
299 dtype=torch.bfloat16,
300 mla_dimensions=MLADimensions(
301 qk_nope_head_dim=128,
302 qk_rope_head_dim=64,
303 v_head_dim=128,
304 ),
305 )
306
307 with patch.object(FlashInferPrefillBackend, "is_available", return_value=True):
308 invalid_reasons = FlashInferPrefillBackend.validate_configuration(
309 capability,
310 selector_config,
311 )
312 assert len(invalid_reasons) == 0
313
314 selector_config_invalid = MLAPrefillSelectorConfig(
315 dtype=torch.bfloat16,
316 mla_dimensions=MLADimensions(
317 qk_nope_head_dim=64,
318 qk_rope_head_dim=64,
319 v_head_dim=128,
320 ),
321 )
322
323 with patch.object(FlashInferPrefillBackend, "is_available", return_value=True):
324 invalid_reasons = FlashInferPrefillBackend.validate_configuration(
325 capability,
326 selector_config_invalid,
327 )
328 assert len(invalid_reasons) == 1
329 assert "supported MLA dimensions" in invalid_reasons[0]
330
331 selector_config_glm5 = MLAPrefillSelectorConfig(
332 dtype=torch.bfloat16,
333 mla_dimensions=MLADimensions(
334 qk_nope_head_dim=192,
335 qk_rope_head_dim=64,
336 v_head_dim=256,
337 ),
338 )
339
340 with patch.object(
341 TrtllmRaggedPrefillBackend, "is_available", return_value=True
342 ):
343 invalid_reasons = TrtllmRaggedPrefillBackend.validate_configuration(
344 capability,
345 selector_config_glm5,
346 )
347 assert invalid_reasons == []
348
349 def test_flash_attn_accepts_glm53_flash_nope_dimensions(self):
350 """(256, 0, 256) runs the same FA kernels as GLM-5's (192, 64, 256); a
351 RoPE-carrying 320-wide query does not."""
352 try:
353 from vllm.v1.attention.backends.mla.prefill.flash_attn import (
354 FlashAttnPrefillBackend,
355 )
356 except ImportError:
357 pytest.skip("MLA prefill backend not available")
358 return
359
360 capability = DeviceCapability(major=10, minor=0)
361
362 def validate(qk_rope_head_dim: int) -> list[str]:
363 selector_config = MLAPrefillSelectorConfig(
364 dtype=torch.bfloat16,
365 mla_dimensions=MLADimensions(
366 qk_nope_head_dim=256,
367 qk_rope_head_dim=qk_rope_head_dim,
368 v_head_dim=256,
369 ),
370 )
371 with patch.object(
372 FlashAttnPrefillBackend, "is_available", return_value=True
373 ):
374 return FlashAttnPrefillBackend.validate_configuration(
375 capability, selector_config
376 )
377
378 assert validate(qk_rope_head_dim=0) == []
379 invalid_reasons = validate(qk_rope_head_dim=64)
380 assert len(invalid_reasons) == 1
381 assert "supported MLA dimensions" in invalid_reasons[0]
382
383
384 @pytest.mark.skipif(
385 not current_platform.is_cuda_alike(),
386 reason="Imports vllm.platforms.rocm, whose module init requires a CUDA or "
387 "ROCm torch build; not importable on XPU/CPU/TPU.",
388 )
389 class TestROCmAiterFAPrefillSelection:
390 """Tests for the ROCm AITER FlashAttention MLA prefill backend."""
391
392 def test_rocm_priorities_prefer_aiter_fa(self):
393 """On ROCm, ROCM_AITER_FA is tried first, FLASH_ATTN as fallback."""
394 with patch("vllm.platforms.current_platform") as mock_platform:
395 mock_platform.is_rocm.return_value = True
396 priorities = _get_mla_prefill_backend_priorities(
397 DeviceCapability(major=9, minor=5),
398 MLADimensions(
399 qk_nope_head_dim=128,
400 qk_rope_head_dim=64,
401 v_head_dim=128,
402 ),
403 )
404
405 assert priorities == [
406 MLAPrefillBackendEnum.ROCM_AITER_FA,
407 MLAPrefillBackendEnum.FLASH_ATTN,
408 ]
409
410 def test_supported_dtypes_are_fp16_bf16_only(self):
411 from vllm.v1.attention.backends.mla.prefill.aiter_flash_attn import (
412 AiterFlashAttnPrefillBackend,
413 )
414
415 assert AiterFlashAttnPrefillBackend.supports_dtype(torch.bfloat16)
416 assert AiterFlashAttnPrefillBackend.supports_dtype(torch.float16)
417 # FP8 is served by the separate AITER ASM backend, not this one.
418 assert not AiterFlashAttnPrefillBackend.supports_dtype(torch.float8_e4m3fn)
419
420 def test_supports_compute_capability_on_rocm(self):
421 from vllm.v1.attention.backends.mla.prefill import aiter_flash_attn as mod
422
423 # Gating is decided by on_mi3xx(), not by capability
424 capability = MagicMock()
425
426 with patch.object(mod.current_platform, "is_rocm", return_value=False):
427 assert not mod.AiterFlashAttnPrefillBackend.supports_compute_capability(
428 capability
429 )
430
431 with (
432 patch.object(mod.current_platform, "is_rocm", return_value=True),
433 patch("vllm.platforms.rocm.on_mi3xx", return_value=False),
434 ):
435 assert not mod.AiterFlashAttnPrefillBackend.supports_compute_capability(
436 capability
437 )
438
439 with (
440 patch.object(mod.current_platform, "is_rocm", return_value=True),
441 patch("vllm.platforms.rocm.on_mi3xx", return_value=True),
442 ):
443 assert mod.AiterFlashAttnPrefillBackend.supports_compute_capability(
444 capability
445 )
446
447 def test_is_available_delegates_to_rocm_aiter_ops(self):
448 from vllm._aiter_ops import rocm_aiter_ops
449 from vllm.v1.attention.backends.mla.prefill import aiter_flash_attn as mod
450
451 with patch.object(rocm_aiter_ops, "is_enabled", return_value=False):
452 assert not mod.AiterFlashAttnPrefillBackend.is_available()
453
454 with patch.object(rocm_aiter_ops, "is_enabled", return_value=True):
455 assert mod.AiterFlashAttnPrefillBackend.is_available()
456
457 def test_auto_select_prefers_aiter_fa_on_rocm(self):
458 from vllm.v1.attention.backends.mla.prefill.aiter_flash_attn import (
459 AiterFlashAttnPrefillBackend,
460 )
461
462 # gfx gating is simulated via the mocked validate_configuration,
463 # not the capability.
464 capability = MagicMock()
465 selector_config = MLAPrefillSelectorConfig(dtype=torch.bfloat16)
466
467 with (
468 patch("vllm.platforms.current_platform") as mock_platform,
469 patch.object(
470 AiterFlashAttnPrefillBackend,
471 "validate_configuration",
472 return_value=[],
473 ),
474 ):
475 mock_platform.is_rocm.return_value = True
476 backend = _auto_select_mla_prefill_backend(capability, selector_config)
477 assert backend.get_name() == "ROCM_AITER_FA"
478
479 def test_auto_select_falls_back_to_flash_attn_when_aiter_invalid(self):
480 from vllm.v1.attention.backends.mla.prefill.aiter_flash_attn import (
481 AiterFlashAttnPrefillBackend,
482 )
483
484 try:
485 flash_attn_cls = MLAPrefillBackendEnum.FLASH_ATTN.get_class()
486 except ImportError:
487 pytest.skip("FLASH_ATTN backend not available")
488 return
489
490 # the fallback is forced by the mocked validate_configuration,
491 # not the capability.
492 capability = MagicMock()
493 selector_config = MLAPrefillSelectorConfig(dtype=torch.bfloat16)
494
495 with (
496 patch("vllm.platforms.current_platform") as mock_platform,
497 patch.object(
498 AiterFlashAttnPrefillBackend,
499 "validate_configuration",
500 return_value=["compute capability not supported"],
501 ),
502 patch.object(flash_attn_cls, "validate_configuration", return_value=[]),
503 ):
504 mock_platform.is_rocm.return_value = True
505 backend = _auto_select_mla_prefill_backend(capability, selector_config)
506 assert backend.get_name() == "FLASH_ATTN"
507
508
509 class TestMLAPrefillBackendParsing:
510 """Tests for string-based mla_prefill_backend parsing from CLI args."""
511
512 def test_valid_string_parses_to_enum(self):
513 config = AttentionConfig(
514 mla_prefill_backend="FLASH_ATTN",
515 )
516 assert config.mla_prefill_backend == MLAPrefillBackendEnum.FLASH_ATTN
517
518 def test_invalid_string_raises_error(self):
519 with pytest.raises(ValueError, match="Unknown MLA prefill backend"):
520 AttentionConfig(
521 mla_prefill_backend="NONEXISTENT",
522 )
523
524
525 class TestMLAPrefillBackendConfig:
526 """Tests for mla_prefill_backend configuration in AttentionConfig."""
527
528 def test_default_backend_is_none(self):
529 config = AttentionConfig()
530 assert config.mla_prefill_backend is None
531
532 def test_explicit_flash_attn_backend(self):
533 config = AttentionConfig(
534 mla_prefill_backend=MLAPrefillBackendEnum.FLASH_ATTN,
535 )
536 assert config.mla_prefill_backend == MLAPrefillBackendEnum.FLASH_ATTN
537
538 def test_explicit_trtllm_ragged_backend(self):
539 config = AttentionConfig(
540 mla_prefill_backend=MLAPrefillBackendEnum.TRTLLM_RAGGED,
541 )
542 assert config.mla_prefill_backend == MLAPrefillBackendEnum.TRTLLM_RAGGED