| 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 | ) |
| 10 | from vllm.v1.attention.backends.registry import ( |
| 11 | AttentionBackendEnum, |
| 12 | MambaAttentionBackendEnum, |
| 13 | register_backend, |
| 14 | ) |
| 15 | from vllm.v1.attention.selector import AttentionSelectorConfig |
| 16 | |
| 17 | |
| 18 | class CustomAttentionImpl(AttentionImpl): |
| 19 | """Mock custom attention implementation for testing.""" |
| 20 | |
| 21 | def __init__(self, *args, **kwargs): |
| 22 | super().__init__() |
| 23 | |
| 24 | def forward(self, *args, **kwargs): |
| 25 | """Mock forward pass.""" |
| 26 | pass |
| 27 | |
| 28 | |
| 29 | class CustomAttentionBackend(AttentionBackend): |
| 30 | """Mock custom attention backend for testing.""" |
| 31 | |
| 32 | @staticmethod |
| 33 | def get_name(): |
| 34 | return "CUSTOM" |
| 35 | |
| 36 | @staticmethod |
| 37 | def get_impl_cls(): |
| 38 | return CustomAttentionImpl |
| 39 | |
| 40 | @staticmethod |
| 41 | def get_builder_cls(): |
| 42 | """Mock builder class.""" |
| 43 | return None |
| 44 | |
| 45 | |
| 46 | class CustomMambaAttentionImpl(AttentionImpl): |
| 47 | """Mock custom mamba attention implementation for testing.""" |
| 48 | |
| 49 | def __init__(self, *args, **kwargs): |
| 50 | super().__init__() |
| 51 | |
| 52 | def forward(self, *args, **kwargs): |
| 53 | """Mock forward pass.""" |
| 54 | pass |
| 55 | |
| 56 | |
| 57 | class CustomMambaAttentionBackend(AttentionBackend): |
| 58 | """Mock custom mamba attention backend for testing.""" |
| 59 | |
| 60 | @staticmethod |
| 61 | def get_name(): |
| 62 | return "CUSTOM_MAMBA" |
| 63 | |
| 64 | @staticmethod |
| 65 | def get_impl_cls(): |
| 66 | return CustomMambaAttentionImpl |
| 67 | |
| 68 | @staticmethod |
| 69 | def get_builder_cls(): |
| 70 | """Mock builder class.""" |
| 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) |
| 94 | |
| 95 | # Find any aliases of CUSTOM |
| 96 | aliases = [] |
| 97 | for backend in all_backends: |
| 98 | if backend.name != "CUSTOM" and backend is AttentionBackendEnum.CUSTOM: |
| 99 | aliases.append(backend.name) |
| 100 | |
| 101 | # CUSTOM should not be an alias of any other backend |
| 102 | assert len(aliases) == 0, ( |
| 103 | f"BUG! CUSTOM is an alias of: {', '.join(aliases)}!\n" |
| 104 | f"CUSTOM.value = {repr(AttentionBackendEnum.CUSTOM.value)}\n" |
| 105 | f"This happens when CUSTOM has the same value as another backend.\n" |
| 106 | f"When you register to CUSTOM, you're actually registering to {aliases[0]}!\n" |
| 107 | f"All backend values:\n" |
| 108 | + "\n".join(f" {b.name}: {repr(b.value)}" for b in all_backends) |
| 109 | ) |
| 110 | |
| 111 | # Verify CUSTOM has its own unique identity |
| 112 | assert AttentionBackendEnum.CUSTOM.name == "CUSTOM", ( |
| 113 | f"CUSTOM.name should be 'CUSTOM', but got '{AttentionBackendEnum.CUSTOM.name}'" |
| 114 | ) |
| 115 | |
| 116 | |
| 117 | def test_register_custom_backend_with_class_path(): |
| 118 | # Register with explicit class path |
| 119 | register_backend( |
| 120 | backend=AttentionBackendEnum.CUSTOM, |
| 121 | class_path="tests.test_attention_backend_registry.CustomAttentionBackend", |
| 122 | is_mamba=False, |
| 123 | ) |
| 124 | |
| 125 | # Check that CUSTOM backend is registered |
| 126 | assert AttentionBackendEnum.CUSTOM.is_overridden(), ( |
| 127 | "CUSTOM should be overridden after registration" |
| 128 | ) |
| 129 | |
| 130 | # Get the registered class path |
| 131 | class_path = AttentionBackendEnum.CUSTOM.get_path() |
| 132 | assert class_path == "tests.test_attention_backend_registry.CustomAttentionBackend" |
| 133 | |
| 134 | # Get the backend class |
| 135 | backend_cls = AttentionBackendEnum.CUSTOM.get_class() |
| 136 | assert backend_cls.get_name() == "CUSTOM" |
| 137 | assert backend_cls.get_impl_cls() == CustomAttentionImpl |
| 138 | |
| 139 | |
| 140 | def test_mamba_custom_is_not_alias_of_any_backend(): |
| 141 | # Get all mamba backends |
| 142 | all_backends = list(MambaAttentionBackendEnum) |
| 143 | |
| 144 | # Find any aliases of CUSTOM |
| 145 | aliases = [] |
| 146 | for backend in all_backends: |
| 147 | if backend.name != "CUSTOM" and backend is MambaAttentionBackendEnum.CUSTOM: |
| 148 | aliases.append(backend.name) |
| 149 | |
| 150 | # CUSTOM should not be an alias of any other backend |
| 151 | assert len(aliases) == 0, ( |
| 152 | f"BUG! MambaAttentionBackendEnum.CUSTOM is an alias of: {', '.join(aliases)}!\n" |
| 153 | f"CUSTOM.value = {repr(MambaAttentionBackendEnum.CUSTOM.value)}\n" |
| 154 | f"All mamba backend values:\n" |
| 155 | + "\n".join(f" {b.name}: {repr(b.value)}" for b in all_backends) |
| 156 | ) |
| 157 | |
| 158 | |
| 159 | def test_register_custom_mamba_backend_with_class_path(): |
| 160 | # Register with explicit class path |
| 161 | register_backend( |
| 162 | backend=MambaAttentionBackendEnum.CUSTOM, |
| 163 | class_path="tests.test_attention_backend_registry.CustomMambaAttentionBackend", |
| 164 | is_mamba=True, |
| 165 | ) |
| 166 | |
| 167 | # Check that the backend is registered |
| 168 | assert MambaAttentionBackendEnum.CUSTOM.is_overridden() |
| 169 | |
| 170 | # Get the registered class path |
| 171 | class_path = MambaAttentionBackendEnum.CUSTOM.get_path() |
| 172 | assert ( |
| 173 | class_path |
| 174 | == "tests.test_attention_backend_registry.CustomMambaAttentionBackend" |
| 175 | ) |
| 176 | |
| 177 | # Get the backend class |
| 178 | backend_cls = MambaAttentionBackendEnum.CUSTOM.get_class() |
| 179 | assert backend_cls.get_name() == "CUSTOM_MAMBA" |
| 180 | assert backend_cls.get_impl_cls() == CustomMambaAttentionImpl |