Files
vllm-project--vllm/tests/v1/attention/test_mla_prefill_selector.py
T
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

428 lines
15 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for MLA prefill backend selector."""
from unittest.mock import MagicMock, patch
import pytest
import torch
from vllm.config import AttentionConfig, ModelConfig, VllmConfig
from vllm.platforms.interface import DeviceCapability
from vllm.v1.attention.backends.mla.prefill.base import MLADimensions
from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
from vllm.v1.attention.backends.mla.prefill.selector import (
MLAPrefillSelectorConfig,
_auto_select_mla_prefill_backend,
_get_mla_prefill_backend_priorities,
get_mla_prefill_backend,
)
@pytest.fixture(autouse=True)
def clear_cache():
"""Clear lru cache to ensure each test case runs without caching."""
_auto_select_mla_prefill_backend.cache_clear()
def _make_mock_model_config(
qk_nope_head_dim: int = 128,
qk_rope_head_dim: int = 64,
v_head_dim: int = 128,
dtype: torch.dtype = torch.bfloat16,
) -> ModelConfig:
mock_config = MagicMock(spec=ModelConfig)
mock_config.dtype = dtype
mock_config.hf_text_config = MagicMock()
mock_config.hf_text_config.qk_nope_head_dim = qk_nope_head_dim
mock_config.hf_text_config.qk_rope_head_dim = qk_rope_head_dim
mock_config.hf_text_config.v_head_dim = v_head_dim
return mock_config
def _make_vllm_config(
model_config: ModelConfig | None = None,
mla_prefill_backend: MLAPrefillBackendEnum | None = None,
) -> VllmConfig:
if model_config is None:
model_config = _make_mock_model_config()
attention_config = AttentionConfig(mla_prefill_backend=mla_prefill_backend)
mock_vllm_config = MagicMock(spec=VllmConfig)
mock_vllm_config.model_config = model_config
mock_vllm_config.attention_config = attention_config
return mock_vllm_config
class TestGetMLAPrefillBackend:
"""Tests for get_mla_prefill_backend (public API)."""
def test_no_device_capability_returns_flash_attn(self):
vllm_config = _make_vllm_config()
with patch("vllm.platforms.current_platform") as mock_platform:
mock_platform.get_device_capability.return_value = None
backend = get_mla_prefill_backend(vllm_config)
assert backend.get_name() == "FLASH_ATTN"
def test_explicit_flash_attn_selection(self):
try:
flash_attn_cls = MLAPrefillBackendEnum.FLASH_ATTN.get_class()
except ImportError:
pytest.skip("FLASH_ATTN backend not available")
return
vllm_config = _make_vllm_config(
mla_prefill_backend=MLAPrefillBackendEnum.FLASH_ATTN,
)
with patch("vllm.platforms.current_platform") as mock_platform:
mock_platform.get_device_capability.return_value = DeviceCapability(
major=9, minor=0
)
with patch.object(
flash_attn_cls,
"validate_configuration",
return_value=[],
):
backend = get_mla_prefill_backend(vllm_config)
assert backend.get_name() == "FLASH_ATTN"
def test_explicit_backend_invalid_raises_error(self):
vllm_config = _make_vllm_config(
mla_prefill_backend=MLAPrefillBackendEnum.FLASHINFER,
)
with patch("vllm.platforms.current_platform") as mock_platform:
mock_platform.get_device_capability.return_value = DeviceCapability(
major=9, minor=0
)
with pytest.raises(ValueError, match="is not valid"):
get_mla_prefill_backend(vllm_config)
def test_explicit_backend_import_error_raises(self):
vllm_config = _make_vllm_config(
mla_prefill_backend=MLAPrefillBackendEnum.TRTLLM_RAGGED,
)
with patch("vllm.platforms.current_platform") as mock_platform:
mock_platform.get_device_capability.return_value = DeviceCapability(
major=10, minor=0
)
with (
patch.object(
MLAPrefillBackendEnum.TRTLLM_RAGGED,
"get_class",
side_effect=ImportError("trtllm not installed"),
),
pytest.raises(ValueError, match="is not valid"),
):
get_mla_prefill_backend(vllm_config)
def test_auto_selection_on_hopper(self):
try:
flash_attn_cls = MLAPrefillBackendEnum.FLASH_ATTN.get_class()
except ImportError:
pytest.skip("FLASH_ATTN backend not available")
return
vllm_config = _make_vllm_config()
with patch("vllm.platforms.current_platform") as mock_platform:
mock_platform.get_device_capability.return_value = DeviceCapability(
major=9, minor=0
)
with patch.object(
flash_attn_cls,
"validate_configuration",
return_value=[],
):
backend = get_mla_prefill_backend(vllm_config)
assert backend.get_name() == "FLASH_ATTN"
class TestAutoSelectMLAPrefillBackend:
"""Tests for fallback and error paths in auto-selection."""
def test_blackwell_falls_back_to_trtllm(self):
capability = DeviceCapability(major=10, minor=0)
selector_config = MLAPrefillSelectorConfig(
dtype=torch.bfloat16,
mla_dimensions=MLADimensions(
qk_nope_head_dim=128,
qk_rope_head_dim=64,
v_head_dim=128,
),
)
try:
trtllm_cls = MLAPrefillBackendEnum.TRTLLM_RAGGED.get_class()
except ImportError:
pytest.skip("TRTLLM_RAGGED backend not available")
return
with (
patch("vllm.platforms.current_platform") as mock_platform,
patch.object(
MLAPrefillBackendEnum.FLASH_ATTN,
"get_class",
side_effect=ImportError("FLASH_ATTN not available"),
),
patch.object(trtllm_cls, "validate_configuration", return_value=[]),
):
# Force the non-ROCm priority on the Blackwell.
mock_platform.is_rocm.return_value = False
backend = _auto_select_mla_prefill_backend(
capability,
selector_config,
)
assert backend.get_name() == "TRTLLM_RAGGED"
def test_all_fail_raises_error(self):
capability = DeviceCapability(major=10, minor=0)
selector_config = MLAPrefillSelectorConfig(
dtype=torch.bfloat16,
mla_dimensions=MLADimensions(
qk_nope_head_dim=128,
qk_rope_head_dim=64,
v_head_dim=128,
),
)
def mock_get_class(backend_enum): # noqa: ARG001
cls = MagicMock()
cls.validate_configuration.return_value = ["not available"]
return cls
with patch.object(MLAPrefillBackendEnum, "get_class", mock_get_class):
_auto_select_mla_prefill_backend.cache_clear()
with pytest.raises(ValueError, match="No valid MLA"):
_auto_select_mla_prefill_backend(
capability,
selector_config,
)
class TestBackendValidation:
"""Tests for backend validation logic."""
def test_backend_supported_dimension_validation(self):
try:
from vllm.v1.attention.backends.mla.prefill.flashinfer import (
FlashInferPrefillBackend,
)
from vllm.v1.attention.backends.mla.prefill.trtllm_ragged import (
TrtllmRaggedPrefillBackend,
)
except ImportError:
pytest.skip("MLA prefill backend not available")
return
capability = DeviceCapability(major=10, minor=0)
selector_config = MLAPrefillSelectorConfig(
dtype=torch.bfloat16,
mla_dimensions=MLADimensions(
qk_nope_head_dim=128,
qk_rope_head_dim=64,
v_head_dim=128,
),
)
with patch.object(FlashInferPrefillBackend, "is_available", return_value=True):
invalid_reasons = FlashInferPrefillBackend.validate_configuration(
capability,
selector_config,
)
assert len(invalid_reasons) == 0
selector_config_invalid = MLAPrefillSelectorConfig(
dtype=torch.bfloat16,
mla_dimensions=MLADimensions(
qk_nope_head_dim=64,
qk_rope_head_dim=64,
v_head_dim=128,
),
)
with patch.object(FlashInferPrefillBackend, "is_available", return_value=True):
invalid_reasons = FlashInferPrefillBackend.validate_configuration(
capability,
selector_config_invalid,
)
assert len(invalid_reasons) == 1
assert "supported MLA dimensions" in invalid_reasons[0]
selector_config_glm5 = MLAPrefillSelectorConfig(
dtype=torch.bfloat16,
mla_dimensions=MLADimensions(
qk_nope_head_dim=192,
qk_rope_head_dim=64,
v_head_dim=256,
),
)
with patch.object(
TrtllmRaggedPrefillBackend, "is_available", return_value=True
):
invalid_reasons = TrtllmRaggedPrefillBackend.validate_configuration(
capability,
selector_config_glm5,
)
assert invalid_reasons == []
class TestROCmAiterFAPrefillSelection:
"""Tests for the ROCm AITER FlashAttention MLA prefill backend."""
def test_rocm_priorities_prefer_aiter_fa(self):
"""On ROCm, ROCM_AITER_FA is tried first, FLASH_ATTN as fallback."""
with patch("vllm.platforms.current_platform") as mock_platform:
mock_platform.is_rocm.return_value = True
priorities = _get_mla_prefill_backend_priorities(
DeviceCapability(major=9, minor=5)
)
assert priorities == [
MLAPrefillBackendEnum.ROCM_AITER_FA,
MLAPrefillBackendEnum.FLASH_ATTN,
]
def test_supported_dtypes_are_fp16_bf16_only(self):
from vllm.v1.attention.backends.mla.prefill.aiter_flash_attn import (
AiterFlashAttnPrefillBackend,
)
assert AiterFlashAttnPrefillBackend.supports_dtype(torch.bfloat16)
assert AiterFlashAttnPrefillBackend.supports_dtype(torch.float16)
# FP8 is served by the separate AITER ASM backend, not this one.
assert not AiterFlashAttnPrefillBackend.supports_dtype(torch.float8_e4m3fn)
def test_supports_compute_capability_on_rocm(self):
from vllm.v1.attention.backends.mla.prefill import aiter_flash_attn as mod
# Gating is decided by on_mi3xx(), not by capability
capability = MagicMock()
with patch.object(mod.current_platform, "is_rocm", return_value=False):
assert not mod.AiterFlashAttnPrefillBackend.supports_compute_capability(
capability
)
with (
patch.object(mod.current_platform, "is_rocm", return_value=True),
patch("vllm.platforms.rocm.on_mi3xx", return_value=False),
):
assert not mod.AiterFlashAttnPrefillBackend.supports_compute_capability(
capability
)
with (
patch.object(mod.current_platform, "is_rocm", return_value=True),
patch("vllm.platforms.rocm.on_mi3xx", return_value=True),
):
assert mod.AiterFlashAttnPrefillBackend.supports_compute_capability(
capability
)
def test_is_available_delegates_to_rocm_aiter_ops(self):
from vllm._aiter_ops import rocm_aiter_ops
from vllm.v1.attention.backends.mla.prefill import aiter_flash_attn as mod
with patch.object(rocm_aiter_ops, "is_enabled", return_value=False):
assert not mod.AiterFlashAttnPrefillBackend.is_available()
with patch.object(rocm_aiter_ops, "is_enabled", return_value=True):
assert mod.AiterFlashAttnPrefillBackend.is_available()
def test_auto_select_prefers_aiter_fa_on_rocm(self):
from vllm.v1.attention.backends.mla.prefill.aiter_flash_attn import (
AiterFlashAttnPrefillBackend,
)
# gfx gating is simulated via the mocked validate_configuration,
# not the capability.
capability = MagicMock()
selector_config = MLAPrefillSelectorConfig(dtype=torch.bfloat16)
with (
patch("vllm.platforms.current_platform") as mock_platform,
patch.object(
AiterFlashAttnPrefillBackend,
"validate_configuration",
return_value=[],
),
):
mock_platform.is_rocm.return_value = True
backend = _auto_select_mla_prefill_backend(capability, selector_config)
assert backend.get_name() == "ROCM_AITER_FA"
def test_auto_select_falls_back_to_flash_attn_when_aiter_invalid(self):
from vllm.v1.attention.backends.mla.prefill.aiter_flash_attn import (
AiterFlashAttnPrefillBackend,
)
try:
flash_attn_cls = MLAPrefillBackendEnum.FLASH_ATTN.get_class()
except ImportError:
pytest.skip("FLASH_ATTN backend not available")
return
# the fallback is forced by the mocked validate_configuration,
# not the capability.
capability = MagicMock()
selector_config = MLAPrefillSelectorConfig(dtype=torch.bfloat16)
with (
patch("vllm.platforms.current_platform") as mock_platform,
patch.object(
AiterFlashAttnPrefillBackend,
"validate_configuration",
return_value=["compute capability not supported"],
),
patch.object(flash_attn_cls, "validate_configuration", return_value=[]),
):
mock_platform.is_rocm.return_value = True
backend = _auto_select_mla_prefill_backend(capability, selector_config)
assert backend.get_name() == "FLASH_ATTN"
class TestMLAPrefillBackendParsing:
"""Tests for string-based mla_prefill_backend parsing from CLI args."""
def test_valid_string_parses_to_enum(self):
config = AttentionConfig(
mla_prefill_backend="FLASH_ATTN", # type: ignore[arg-type]
)
assert config.mla_prefill_backend == MLAPrefillBackendEnum.FLASH_ATTN
def test_invalid_string_raises_error(self):
with pytest.raises(ValueError, match="Unknown MLA prefill backend"):
AttentionConfig(
mla_prefill_backend="NONEXISTENT", # type: ignore[arg-type]
)
class TestMLAPrefillBackendConfig:
"""Tests for mla_prefill_backend configuration in AttentionConfig."""
def test_default_backend_is_none(self):
config = AttentionConfig()
assert config.mla_prefill_backend is None
def test_explicit_flash_attn_backend(self):
config = AttentionConfig(
mla_prefill_backend=MLAPrefillBackendEnum.FLASH_ATTN,
)
assert config.mla_prefill_backend == MLAPrefillBackendEnum.FLASH_ATTN
def test_explicit_trtllm_ragged_backend(self):
config = AttentionConfig(
mla_prefill_backend=MLAPrefillBackendEnum.TRTLLM_RAGGED,
)
assert config.mla_prefill_backend == MLAPrefillBackendEnum.TRTLLM_RAGGED