@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / f3fef12350
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 4, 08:45 UTC

[Attention] Abstract the MLA prefill backends and eliminate cuDNN (#32623)

Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: Michael Goin <mgoin64@gmail.com> Co-authored-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Matthew Bonanni committed May 1, 2026 at 13:36 UTC f3fef123504db07b3ac83ad4ef677915b53e8386
16 files changed +1629 -708
docs/design/attention_backends.md
+12 -8
@@ -192,21 +192,25 @@ MLA uses separate backends for prefill and decode phases.
192
193 ### Prefill Backends
194
195 -The prefill backend is selected at runtime based on hardware and
196 -configuration.
195 +To explicitly select a prefill backend, use
196 +`-ac.mla_prefill_backend=<BACKEND>` (e.g., `FLASH_ATTN`, `FLASHINFER`).
197 +Otherwise, the prefill backend is selected automatically at runtime based on
198 +hardware and configuration.
199
198 -| Backend | Description | Compute Cap. | Enable | Disable | Notes |
199 -| ------- | ----------- | ------------ | ------ | ------- | ----- |
200 -| TRT-LLM Ragged‡ | TensorRT-LLM ragged attention | 10.x | Default on SM100 | `-ac.use_trtllm_ragged_deepseek_prefill=0` | DeepSeek R1 dims only |
201 -| FlashInfer | FlashInfer CUTLASS backend | 10.x | `-ac.disable_flashinfer_prefill=0` | `-ac.disable_flashinfer_prefill=1` | DeepSeek R1 dims only |
202 -| cuDNN | cuDNN-based attention | 10.x | `-ac.use_cudnn_prefill=1` | `-ac.use_cudnn_prefill=0` | |
203 -| FlashAttention | FlashAttention varlen (FA2/FA3) | Any | Default fallback | Use other backends | FA3 on SM90, FA2 otherwise |
200 +| Backend | Description | Dtypes | Compute Cap. | Notes |
201 +| ------- | ----------- | ------ | ------------ | ----- |
202 +| `FLASH_ATTN`‡ | FlashAttention varlen (FA2/FA3/FA4) | fp16, bf16 | Any | FA4 on SM100+, FA3 on SM90, FA2 otherwise |
203 +| `TRTLLM_RAGGED` | TensorRT-LLM ragged attention | fp16, bf16 | 10.x | DeepSeek R1 dims only |
204 +| `FLASHINFER` | FlashInfer CUTLASS backend | fp16, bf16 | 10.x | DeepSeek R1 dims only |
205
206 > **‡** TRT-LLM Ragged is the default on Blackwell (SM100).
207 > On other GPUs, FlashAttention is used as the default.
208
209 ### Decode Backends
210
211 +MLA decode backends are selected using the standard
212 +`-ac.backend=<BACKEND>` argument (e.g., `FLASHMLA`, `TRITON_MLA`).
213 +
214 | Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
215 | ------- | ------ | --------- | ----------- | ---------- | ---- | ------ | --------- | --- | --------------- | ------------ |
216 | `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
tests/engine/test_arg_utils.py
-3
@@ -333,8 +333,6 @@ def test_attention_config():
333 "true",
334 "--attention-config.flash_attn_max_num_splits_for_cuda_graph",
335 "16",
336 - "--attention-config.use_cudnn_prefill",
337 - "true",
336 "--attention-config.use_trtllm_ragged_deepseek_prefill",
337 "true",
338 "--attention-config.use_trtllm_attention",
@@ -352,7 +350,6 @@ def test_attention_config():
350 assert engine_args.attention_config.flash_attn_version == 3
351 assert engine_args.attention_config.use_prefill_decode_attention is True
352 assert engine_args.attention_config.flash_attn_max_num_splits_for_cuda_graph == 16
355 - assert engine_args.attention_config.use_cudnn_prefill is True
353 assert engine_args.attention_config.use_trtllm_ragged_deepseek_prefill is True
354 assert engine_args.attention_config.use_trtllm_attention is True
355 assert engine_args.attention_config.disable_flashinfer_prefill is True
tests/v1/attention/test_mla_backends.py
+1
@@ -672,6 +672,7 @@ def run_attention_backend(
672 def test_backend_correctness(
673 default_vllm_config,
674 dist_init,
675 + workspace_init,
676 batch_spec_name: str,
677 model: str,
678 tensor_parallel_size: int,
tests/v1/attention/test_mla_prefill_selector.py new
+304
@@ -0,0 +1,304 @@
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.interface import DeviceCapability
12 +from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
13 +from vllm.v1.attention.backends.mla.prefill.selector import (
14 + MLAPrefillSelectorConfig,
15 + _auto_select_mla_prefill_backend,
16 + get_mla_prefill_backend,
17 + is_deepseek_r1_mla_compatible,
18 +)
19 +
20 +
21 +@pytest.fixture(autouse=True)
22 +def clear_cache():
23 + """Clear lru cache to ensure each test case runs without caching."""
24 + _auto_select_mla_prefill_backend.cache_clear()
25 +
26 +
27 +def _make_mock_model_config(
28 + qk_nope_head_dim: int = 128,
29 + qk_rope_head_dim: int = 64,
30 + v_head_dim: int = 128,
31 + dtype: torch.dtype = torch.bfloat16,
32 +) -> ModelConfig:
33 + mock_config = MagicMock(spec=ModelConfig)
34 + mock_config.dtype = dtype
35 + mock_config.hf_text_config = MagicMock()
36 + mock_config.hf_text_config.qk_nope_head_dim = qk_nope_head_dim
37 + mock_config.hf_text_config.qk_rope_head_dim = qk_rope_head_dim
38 + mock_config.hf_text_config.v_head_dim = v_head_dim
39 + return mock_config
40 +
41 +
42 +def _make_vllm_config(
43 + model_config: ModelConfig | None = None,
44 + mla_prefill_backend: MLAPrefillBackendEnum | None = None,
45 +) -> VllmConfig:
46 + if model_config is None:
47 + model_config = _make_mock_model_config()
48 +
49 + attention_config = AttentionConfig(mla_prefill_backend=mla_prefill_backend)
50 + mock_vllm_config = MagicMock(spec=VllmConfig)
51 + mock_vllm_config.model_config = model_config
52 + mock_vllm_config.attention_config = attention_config
53 + return mock_vllm_config
54 +
55 +
56 +class TestGetMLAPrefillBackend:
57 + """Tests for get_mla_prefill_backend (public API)."""
58 +
59 + def test_no_device_capability_returns_flash_attn(self):
60 + vllm_config = _make_vllm_config()
61 +
62 + with patch("vllm.platforms.current_platform") as mock_platform:
63 + mock_platform.get_device_capability.return_value = None
64 +
65 + backend = get_mla_prefill_backend(vllm_config)
66 + assert backend.get_name() == "FLASH_ATTN"
67 +
68 + def test_explicit_flash_attn_selection(self):
69 + try:
70 + flash_attn_cls = MLAPrefillBackendEnum.FLASH_ATTN.get_class()
71 + except ImportError:
72 + pytest.skip("FLASH_ATTN backend not available")
73 + return
74 +
75 + vllm_config = _make_vllm_config(
76 + mla_prefill_backend=MLAPrefillBackendEnum.FLASH_ATTN,
77 + )
78 +
79 + with patch("vllm.platforms.current_platform") as mock_platform:
80 + mock_platform.get_device_capability.return_value = DeviceCapability(
81 + major=9, minor=0
82 + )
83 +
84 + with patch.object(
85 + flash_attn_cls,
86 + "validate_configuration",
87 + return_value=[],
88 + ):
89 + backend = get_mla_prefill_backend(vllm_config)
90 + assert backend.get_name() == "FLASH_ATTN"
91 +
92 + def test_explicit_backend_invalid_raises_error(self):
93 + vllm_config = _make_vllm_config(
94 + mla_prefill_backend=MLAPrefillBackendEnum.FLASHINFER,
95 + )
96 +
97 + with patch("vllm.platforms.current_platform") as mock_platform:
98 + mock_platform.get_device_capability.return_value = DeviceCapability(
99 + major=9, minor=0
100 + )
101 +
102 + with pytest.raises(ValueError, match="is not valid"):
103 + get_mla_prefill_backend(vllm_config)
104 +
105 + def test_explicit_backend_import_error_raises(self):
106 + vllm_config = _make_vllm_config(
107 + mla_prefill_backend=MLAPrefillBackendEnum.TRTLLM_RAGGED,
108 + )
109 +
110 + with patch("vllm.platforms.current_platform") as mock_platform:
111 + mock_platform.get_device_capability.return_value = DeviceCapability(
112 + major=10, minor=0
113 + )
114 +
115 + with (
116 + patch.object(
117 + MLAPrefillBackendEnum.TRTLLM_RAGGED,
118 + "get_class",
119 + side_effect=ImportError("trtllm not installed"),
120 + ),
121 + pytest.raises(ValueError, match="is not valid"),
122 + ):
123 + get_mla_prefill_backend(vllm_config)
124 +
125 + def test_auto_selection_on_hopper(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 +
134 + with patch("vllm.platforms.current_platform") as mock_platform:
135 + mock_platform.get_device_capability.return_value = DeviceCapability(
136 + major=9, minor=0
137 + )
138 +
139 + with patch.object(
140 + flash_attn_cls,
141 + "validate_configuration",
142 + return_value=[],
143 + ):
144 + backend = get_mla_prefill_backend(vllm_config)
145 + assert backend.get_name() == "FLASH_ATTN"
146 +
147 +
148 +class TestAutoSelectMLAPrefillBackend:
149 + """Tests for fallback and error paths in auto-selection."""
150 +
151 + def test_blackwell_falls_back_to_trtllm(self):
152 + vllm_config = _make_vllm_config()
153 + capability = DeviceCapability(major=10, minor=0)
154 + selector_config = MLAPrefillSelectorConfig(
155 + dtype=torch.bfloat16,
156 + is_r1_compatible=is_deepseek_r1_mla_compatible(vllm_config),
157 + )
158 +
159 + try:
160 + trtllm_cls = MLAPrefillBackendEnum.TRTLLM_RAGGED.get_class()
161 + except ImportError:
162 + pytest.skip("TRTLLM_RAGGED backend not available")
163 + return
164 +
165 + with (
166 + patch.object(
167 + MLAPrefillBackendEnum.FLASH_ATTN,
168 + "get_class",
169 + side_effect=ImportError("FLASH_ATTN not available"),
170 + ),
171 + patch.object(trtllm_cls, "validate_configuration", return_value=[]),
172 + ):
173 + backend = _auto_select_mla_prefill_backend(
174 + capability,
175 + selector_config,
176 + )
177 + assert backend.get_name() == "TRTLLM_RAGGED"
178 +
179 + def test_all_fail_raises_error(self):
180 + vllm_config = _make_vllm_config()
181 + capability = DeviceCapability(major=10, minor=0)
182 + selector_config = MLAPrefillSelectorConfig(
183 + dtype=torch.bfloat16,
184 + is_r1_compatible=is_deepseek_r1_mla_compatible(vllm_config),
185 + )
186 +
187 + def mock_get_class(backend_enum): # noqa: ARG001
188 + cls = MagicMock()
189 + cls.validate_configuration.return_value = ["not available"]
190 + return cls
191 +
192 + with patch.object(MLAPrefillBackendEnum, "get_class", mock_get_class):
193 + _auto_select_mla_prefill_backend.cache_clear()
194 + with pytest.raises(ValueError, match="No valid MLA"):
195 + _auto_select_mla_prefill_backend(
196 + capability,
197 + selector_config,
198 + )
199 +
200 +
201 +class TestBackendValidation:
202 + """Tests for backend validation logic."""
203 +
204 + def test_r1_dimension_requirement(self):
205 + try:
206 + from vllm.v1.attention.backends.mla.prefill.flashinfer import (
207 + FlashInferPrefillBackend,
208 + )
209 + except ImportError:
210 + pytest.skip("FlashInfer prefill backend not available")
211 + return
212 +
213 + assert FlashInferPrefillBackend.requires_r1_mla_dimensions is True
214 +
215 + vllm_config = _make_vllm_config(
216 + model_config=_make_mock_model_config(
217 + qk_nope_head_dim=128,
218 + qk_rope_head_dim=64,
219 + v_head_dim=128,
220 + )
221 + )
222 + capability = DeviceCapability(major=10, minor=0)
223 + selector_config = MLAPrefillSelectorConfig(
224 + dtype=torch.bfloat16,
225 + is_r1_compatible=is_deepseek_r1_mla_compatible(vllm_config),
226 + )
227 +
228 + with patch.object(FlashInferPrefillBackend, "is_available", return_value=True):
229 + invalid_reasons = FlashInferPrefillBackend.validate_configuration(
230 + capability,
231 + selector_config,
232 + )
233 + assert len(invalid_reasons) == 0
234 +
235 + vllm_config_invalid = _make_vllm_config(
236 + model_config=_make_mock_model_config(
237 + qk_nope_head_dim=64,
238 + qk_rope_head_dim=64,
239 + v_head_dim=128,
240 + )
241 + )
242 + selector_config_invalid = MLAPrefillSelectorConfig(
243 + dtype=torch.bfloat16,
244 + is_r1_compatible=is_deepseek_r1_mla_compatible(vllm_config_invalid),
245 + )
246 +
247 + with patch.object(FlashInferPrefillBackend, "is_available", return_value=True):
248 + invalid_reasons = FlashInferPrefillBackend.validate_configuration(
249 + capability,
250 + selector_config_invalid,
251 + )
252 + assert len(invalid_reasons) == 1
253 + assert "DeepSeek R1 MLA dimensions" in invalid_reasons[0]
254 +
255 +
256 +class TestMLAPrefillBackendParsing:
257 + """Tests for string-based mla_prefill_backend parsing from CLI args."""
258 +
259 + def test_valid_string_parses_to_enum(self):
260 + config = AttentionConfig(
261 + mla_prefill_backend="FLASH_ATTN", # type: ignore[arg-type]
262 + )
263 + assert config.mla_prefill_backend == MLAPrefillBackendEnum.FLASH_ATTN
264 +
265 + def test_invalid_string_raises_error(self):
266 + with pytest.raises(ValueError, match="Unknown MLA prefill backend"):
267 + AttentionConfig(
268 + mla_prefill_backend="NONEXISTENT", # type: ignore[arg-type]
269 + )
270 +
271 +
272 +class TestDeprecatedFlagMigration:
273 + """Tests for _migrate_deprecated_mla_prefill_flags in AttentionConfig."""
274 +
275 + def test_no_deprecated_flags_leaves_backend_none(self):
276 + config = AttentionConfig()
277 + assert config.mla_prefill_backend is None
278 +
279 + def test_use_trtllm_ragged_migrates_to_trtllm_ragged(self):
280 + config = AttentionConfig(use_trtllm_ragged_deepseek_prefill=True)
281 + assert config.mla_prefill_backend == MLAPrefillBackendEnum.TRTLLM_RAGGED
282 +
283 + def test_disable_flashinfer_prefill_migrates_to_flash_attn(self):
284 + config = AttentionConfig(disable_flashinfer_prefill=True)
285 + assert config.mla_prefill_backend == MLAPrefillBackendEnum.FLASH_ATTN
286 +
287 + def test_explicit_backend_ignores_deprecated_flags(self):
288 + config = AttentionConfig(
289 + mla_prefill_backend=MLAPrefillBackendEnum.FLASH_ATTN,
290 + use_cudnn_prefill=True,
291 + )
292 + assert config.mla_prefill_backend == MLAPrefillBackendEnum.FLASH_ATTN
293 +
294 + def test_cudnn_raises_error(self):
295 + match = "cuDNN MLA prefill backend has been removed"
296 + with pytest.raises(ValueError, match=match):
297 + AttentionConfig(use_cudnn_prefill=True)
298 +
299 + def test_trtllm_takes_priority_over_disable_flashinfer(self):
300 + config = AttentionConfig(
301 + use_trtllm_ragged_deepseek_prefill=True,
302 + disable_flashinfer_prefill=True,
303 + )
304 + assert config.mla_prefill_backend == MLAPrefillBackendEnum.TRTLLM_RAGGED
tools/pre_commit/generate_attention_backend_docs.py
+253 -88
@@ -30,7 +30,6 @@ REPO_ROOT = Path(__file__).parent.parent.parent
30 RELEVANT_PATTERNS = [
31 "vllm/v1/attention/backends/*.py",
32 "vllm/v1/attention/backends/**/*.py",
33 - "vllm/v1/attention/backends/fa_utils.py",
33 "vllm/model_executor/layers/attention/mla_attention.py",
34 "vllm/platforms/cuda.py",
35 "tools/pre_commit/generate_attention_backend_docs.py",
@@ -68,6 +67,11 @@ def is_relevant_file(filepath: str) -> bool:
67 return any(fnmatch.fnmatch(path_str, pattern) for pattern in RELEVANT_PATTERNS)
68
69
70 +MLA_PREFILL_DIR = BACKENDS_DIR / "mla" / "prefill"
71 +MLA_PREFILL_REGISTRY_FILE = MLA_PREFILL_DIR / "registry.py"
72 +MLA_PREFILL_SELECTOR_FILE = MLA_PREFILL_DIR / "selector.py"
73 +
74 +
75 # ---------------------------------------------------------------------------
76 # AST utility helpers
77 # ---------------------------------------------------------------------------
@@ -293,6 +297,242 @@ def get_file_from_class_path(class_path: str) -> Path | None:
297 return py_file if py_file.exists() else None
298
299
300 +def parse_mla_prefill_registry() -> dict[str, str]:
301 + """Parse MLAPrefillBackendEnum from the prefill registry.
302 +
303 + Returns:
304 + A dict mapping backend names to their class paths.
305 + """
306 + if not MLA_PREFILL_REGISTRY_FILE.exists():
307 + return {}
308 +
309 + try:
310 + tree = ast.parse(MLA_PREFILL_REGISTRY_FILE.read_text())
311 + except Exception:
312 + return {}
313 +
314 + for node in ast.walk(tree):
315 + if isinstance(node, ast.ClassDef) and node.name == "MLAPrefillBackendEnum":
316 + return _extract_enum_values(node)
317 + return {}
318 +
319 +
320 +def parse_mla_prefill_priorities() -> dict[str, list[str]]:
321 + """Parse MLA prefill backend priorities from selector.py.
322 +
323 + Returns:
324 + A dict with keys like 'blackwell' and 'default' containing
325 + lists of backend enum names in priority order.
326 + """
327 + if not MLA_PREFILL_SELECTOR_FILE.exists():
328 + return {}
329 +
330 + try:
331 + tree = ast.parse(MLA_PREFILL_SELECTOR_FILE.read_text())
332 + except Exception:
333 + return {}
334 +
335 + priorities: dict[str, list[str]] = {}
336 +
337 + for node in ast.walk(tree):
338 + if not isinstance(node, ast.FunctionDef):
339 + continue
340 + if node.name != "_get_mla_prefill_backend_priorities":
341 + continue
342 +
343 + # Look for if statements checking device_capability.major
344 + for stmt in ast.walk(node):
345 + if not isinstance(stmt, ast.If):
346 + continue
347 +
348 + # Check if it's a capability.major == 10 check (Blackwell)
349 + is_blackwell = (
350 + isinstance(stmt.test, ast.Compare)
351 + and isinstance(stmt.test.left, ast.Attribute)
352 + and stmt.test.left.attr == "major"
353 + and stmt.test.comparators
354 + and isinstance(stmt.test.comparators[0], ast.Constant)
355 + and stmt.test.comparators[0].value == 10
356 + )
357 +
358 + # Extract backends from return statements
359 + for body_stmt in stmt.body:
360 + if isinstance(body_stmt, ast.Return) and isinstance(
361 + body_stmt.value, ast.List
362 + ):
363 + backends = []
364 + for elt in body_stmt.value.elts:
365 + if isinstance(elt, ast.Attribute):
366 + backends.append(elt.attr)
367 + if is_blackwell:
368 + priorities["blackwell"] = backends
369 + else:
370 + priorities["default"] = backends
371 +
372 + # Extract from else branch
373 + for else_stmt in stmt.orelse:
374 + if isinstance(else_stmt, ast.Return) and isinstance(
375 + else_stmt.value, ast.List
376 + ):
377 + backends = []
378 + for elt in else_stmt.value.elts:
379 + if isinstance(elt, ast.Attribute):
380 + backends.append(elt.attr)
381 + priorities["default"] = backends
382 +
383 + return priorities
384 +
385 +
386 +def parse_mla_prefill_backend_file(class_path: str) -> dict[str, Any] | None:
387 + """Parse a single MLA prefill backend file to extract its properties.
388 +
389 + Args:
390 + class_path: The fully qualified class path.
391 +
392 + Returns:
393 + A dict with backend properties, or None if parsing fails.
394 + """
395 + file_path = get_file_from_class_path(class_path)
396 + if file_path is None:
397 + return None
398 +
399 + try:
400 + tree = ast.parse(file_path.read_text())
401 + except Exception:
402 + return None
403 +
404 + class_name = class_path.rsplit(".", 1)[1]
405 + class_node = find_class_in_ast(tree, class_name)
406 + if class_node is None:
407 + return None
408 +
409 + info: dict[str, Any] = {
410 + "compute_capability": "Any",
411 + "requires_r1_dims": False,
412 + "dtypes": "fp16, bf16", # Default from base class
413 + }
414 +
415 + # Parse class variables
416 + for item in class_node.body:
417 + if isinstance(item, ast.Assign):
418 + for target in item.targets:
419 + if (
420 + isinstance(target, ast.Name)
421 + and target.id == "requires_r1_mla_dimensions"
422 + and isinstance(item.value, ast.Constant)
423 + ):
424 + info["requires_r1_dims"] = item.value.value
425 +
426 + # Parse supported_dtypes class variable
427 + if (
428 + isinstance(item, ast.AnnAssign)
429 + and isinstance(item.target, ast.Name)
430 + and item.target.id == "supported_dtypes"
431 + and isinstance(item.value, ast.List)
432 + ):
433 + dtype_map = {"float16": "fp16", "bfloat16": "bf16", "float32": "fp32"}
434 + dtypes = []
435 + for elt in item.value.elts:
436 + if isinstance(elt, ast.Attribute):
437 + dtypes.append(dtype_map.get(elt.attr, elt.attr))
438 + if dtypes:
439 + info["dtypes"] = ", ".join(dtypes)
440 +
441 + # Parse get_name static method
442 + get_name_method = find_method(class_node, "get_name")
443 + if get_name_method:
444 + for n in ast.walk(get_name_method):
445 + if isinstance(n, ast.Return) and isinstance(n.value, ast.Constant):
446 + info["name"] = n.value.value
447 +
448 + # Parse supports_compute_capability classmethod
449 + cc_method = find_method(class_node, "supports_compute_capability")
450 + if cc_method:
451 + for n in ast.walk(cc_method):
452 + # Look for capability.major == 10 style checks
453 + if (
454 + isinstance(n, ast.Compare)
455 + and isinstance(n.left, ast.Attribute)
456 + and n.left.attr == "major"
457 + and n.comparators
458 + and isinstance(n.comparators[0], ast.Constant)
459 + ):
460 + major = n.comparators[0].value
461 + info["compute_capability"] = f"{major}.x"
462 +
463 + return info
464 +
465 +
466 +def parse_mla_prefill_backends() -> list[dict[str, Any]]:
467 + """Parse MLA prefill backend options from the prefill registry.
468 +
469 + MLA uses different backends for prefill vs decode. The decode backends are
470 + registered in the main registry, but prefill backends have their own
471 + registry at vllm/v1/attention/backends/mla/prefill/registry.py.
472 +
473 + Returns a list of prefill backend info dicts with their requirements.
474 + """
475 + registry = parse_mla_prefill_registry()
476 + priorities = parse_mla_prefill_priorities()
477 +
478 + if not registry:
479 + return []
480 +
481 + # Get the priority order (Blackwell order shows all backends)
482 + priority_order = priorities.get("blackwell", list(registry.keys()))
483 +
484 + prefill_backends: list[dict[str, Any]] = []
485 +
486 + # Backend-specific metadata that can't be easily parsed from code
487 + backend_metadata = {
488 + "TRTLLM_RAGGED": {
489 + "description": "TensorRT-LLM ragged attention",
490 + },
491 + "FLASHINFER": {
492 + "description": "FlashInfer CUTLASS backend",
493 + },
494 + "FLASH_ATTN": {
495 + "description": "FlashAttention varlen (FA2/FA3/FA4)",
496 + },
497 + }
498 +
499 + for backend_name in priority_order:
500 + if backend_name not in registry:
501 + continue
502 +
503 + class_path = registry[backend_name]
504 + backend_info = parse_mla_prefill_backend_file(class_path)
505 + if backend_info is None:
506 + continue
507 +
508 + metadata = backend_metadata.get(backend_name, {})
509 + display_name = backend_info.get("name", backend_name)
510 +
511 + # Add marker for default Blackwell backend
512 + marker = ""
513 + if backend_name == priority_order[0] and priorities.get("blackwell"):
514 + marker = "‡"
515 +
516 + notes = ""
517 + if backend_info.get("requires_r1_dims"):
518 + notes = "DeepSeek R1 dims only"
519 + elif backend_name == "FLASH_ATTN":
520 + notes = "FA4 on SM100+, FA3 on SM90, FA2 otherwise"
521 +
522 + prefill_backends.append(
523 + {
524 + "name": display_name,
525 + "marker": marker,
526 + "description": metadata.get("description", ""),
527 + "dtypes": backend_info.get("dtypes", "fp16, bf16"),
528 + "compute_capability": backend_info.get("compute_capability", "Any"),
529 + "notes": notes,
530 + }
531 + )
532 +
533 + return prefill_backends
534 +
535 +
536 # ---------------------------------------------------------------------------
537 # Backend feature extraction from AST
538 # ---------------------------------------------------------------------------
@@ -807,86 +1047,6 @@ def parse_flashinfer_trtllm_features() -> dict[str, dict[str, Any]]:
1047 }
1048
1049
810 -def parse_mla_prefill_backends() -> list[dict[str, Any]]:
811 - """Parse MLA prefill backend options from mla_attention.py.
812 -
813 - MLA uses different backends for prefill vs decode. The decode backends are
814 - registered in the registry, but prefill backends are selected at runtime
815 - based on conditions in MLACommonImpl.__init__.
816 -
817 - Returns a list of prefill backend info dicts with their requirements.
818 - """
819 - if not MLA_ATTENTION_FILE.exists():
820 - return []
821 -
822 - try:
823 - tree = ast.parse(MLA_ATTENTION_FILE.read_text())
824 - except Exception:
825 - return []
826 -
827 - # Find compute capability requirements by parsing use_* functions
828 - trtllm_cc = _find_cc_in_function(tree, "use_trtllm_ragged_deepseek_prefill")
829 - flashinfer_cc = _find_cc_in_function(tree, "use_flashinfer_prefill")
830 - cudnn_cc = _find_cc_in_function(tree, "use_cudnn_prefill")
831 -
832 - # Build prefill backend list based on what we found
833 - # Order matches the priority in MLACommonImpl.__init__
834 - prefill_backends: list[dict[str, Any]] = []
835 -
836 - # TRT-LLM Ragged (highest priority if available)
837 - if trtllm_cc:
838 - prefill_backends.append(
839 - {
840 - "name": "TRT-LLM Ragged‡",
841 - "description": "TensorRT-LLM ragged attention",
842 - "compute_capability": trtllm_cc,
843 - "enable": "Default on SM100",
844 - "disable": "`-ac.use_trtllm_ragged_deepseek_prefill=0`",
845 - "notes": "DeepSeek R1 dims only",
846 - }
847 - )
848 -
849 - # FlashInfer prefill
850 - if flashinfer_cc:
851 - prefill_backends.append(
852 - {
853 - "name": "FlashInfer",
854 - "description": "FlashInfer CUTLASS backend",
855 - "compute_capability": flashinfer_cc,
856 - "enable": "`-ac.disable_flashinfer_prefill=0`",
857 - "disable": "`-ac.disable_flashinfer_prefill=1`",
858 - "notes": "DeepSeek R1 dims only",
859 - }
860 - )
861 -
862 - # cuDNN prefill
863 - if cudnn_cc:
864 - prefill_backends.append(
865 - {
866 - "name": "cuDNN",
867 - "description": "cuDNN-based attention",
868 - "compute_capability": cudnn_cc,
869 - "enable": "`-ac.use_cudnn_prefill=1`",
870 - "disable": "`-ac.use_cudnn_prefill=0`",
871 - "notes": "",
872 - }
873 - )
874 -
875 - # FlashAttention is always available as fallback
876 - prefill_backends.append(
877 - {
878 - "name": "FlashAttention",
879 - "description": "FlashAttention varlen (FA2/FA3)",
880 - "compute_capability": "Any",
881 - "enable": "Default fallback",
882 - "disable": "Use other backends",
883 - "notes": "FA3 on SM90, FA2 otherwise",
884 - }
885 - )
886 -
887 - return prefill_backends
888 -
889 -
1050 # ---------------------------------------------------------------------------
1051 # Backend variant expansion (FA2/FA3/FA4, FlashInfer native/TRTLLM)
1052 # ---------------------------------------------------------------------------
@@ -1415,20 +1575,22 @@ def generate_mla_section(
1575 "",
1576 "### Prefill Backends",
1577 "",
1418 - "The prefill backend is selected at runtime based on hardware and",
1419 - "configuration.",
1578 + "To explicitly select a prefill backend, use",
1579 + "`-ac.mla_prefill_backend=<BACKEND>` (e.g., `FLASH_ATTN`, `FLASHINFER`).",
1580 + "Otherwise, the prefill backend is selected automatically at runtime based on",
1581 + "hardware and configuration.",
1582 "",
1421 - "| Backend | Description | Compute Cap. | Enable | Disable | Notes |",
1422 - "| ------- | ----------- | ------------ | ------ | ------- | ----- |",
1583 + "| Backend | Description | Dtypes | Compute Cap. | Notes |",
1584 + "| ------- | ----------- | ------ | ------------ | ----- |",
1585 ]
1586
1587 for backend in prefill_backends:
1426 - row = "| {} | {} | {} | {} | {} | {} |".format(
1588 + row = "| `{}`{} | {} | {} | {} | {} |".format(
1589 backend["name"],
1590 + backend.get("marker", ""),
1591 backend["description"],
1592 + backend.get("dtypes", "fp16, bf16"),
1593 backend["compute_capability"],
1430 - backend["enable"],
1431 - backend["disable"],
1594 backend.get("notes", ""),
1595 )
1596 lines.append(row.replace(" ", " "))
@@ -1441,6 +1603,9 @@ def generate_mla_section(
1603 "",
1604 "### Decode Backends",
1605 "",
1606 + "MLA decode backends are selected using the standard",
1607 + "`-ac.backend=<BACKEND>` argument (e.g., `FLASHMLA`, `TRITON_MLA`).",
1608 + "",
1609 ]
1610 )
1611
vllm/config/attention.py
+57 -2
@@ -6,8 +6,12 @@ from typing import Any, Literal
6 from pydantic import field_validator
7
8 from vllm.config.utils import config
9 +from vllm.logger import init_logger
10 +from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
11 from vllm.v1.attention.backends.registry import AttentionBackendEnum
12
13 +logger = init_logger(__name__)
14 +
15
16 @config
17 class AttentionConfig:
@@ -33,7 +37,7 @@ class AttentionConfig:
37 and buffers can be pre-allocated to avoid inflating the memory estimate."""
38
39 use_cudnn_prefill: bool = False
36 - """Whether to use cudnn prefill."""
40 + """Deprecated: cuDNN prefill backend has been removed."""
41
42 use_trtllm_ragged_deepseek_prefill: bool = False
43 """Whether to use TRTLLM ragged deepseek prefill."""
@@ -42,12 +46,18 @@ class AttentionConfig:
46 """If set to True/False, use or don't use the TRTLLM attention backend
47 in flashinfer. If None, auto-detect the attention backend in flashinfer."""
48
45 - disable_flashinfer_prefill: bool = True
49 + disable_flashinfer_prefill: bool | None = None
50 """Whether to disable flashinfer prefill."""
51
52 disable_flashinfer_q_quantization: bool = False
53 """If set, when using fp8 kv, do not quantize Q to fp8."""
54
55 + mla_prefill_backend: MLAPrefillBackendEnum | None = None
56 + """MLA prefill backend to use. If None, will be selected automatically.
57 + Valid options: FLASH_ATTN (FA3/FA4), FLASHINFER, TRTLLM_RAGGED.
58 + This option supersedes use_trtllm_ragged_deepseek_prefill
59 + and disable_flashinfer_prefill which are deprecated."""
60 +
61 use_prefill_query_quantization: bool = False
62 """If set, quantize query for attention in prefill."""
63
@@ -84,3 +94,48 @@ class AttentionConfig:
94 return None
95 return AttentionBackendEnum[value.upper()]
96 return value
97 +
98 + @field_validator("mla_prefill_backend", mode="before")
99 + @classmethod
100 + def validate_mla_prefill_backend_before(cls, value: Any) -> Any:
101 + """Enable parsing of the `mla_prefill_backend` enum type from string."""
102 + if isinstance(value, str):
103 + return MLAPrefillBackendEnum[value.upper()]
104 + return value
105 +
106 + def __post_init__(self) -> None:
107 + self._migrate_deprecated_mla_prefill_flags()
108 +
109 + def _migrate_deprecated_mla_prefill_flags(self) -> None:
110 + """Migrate deprecated MLA prefill flags to mla_prefill_backend."""
111 + # If the new option is already set, it takes precedence
112 + if self.mla_prefill_backend is not None:
113 + return
114 +
115 + # Check for deprecated flags and migrate them.
116 + # Only the first flag encountered sets the backend.
117 + if self.use_cudnn_prefill:
118 + raise ValueError(
119 + "The cuDNN MLA prefill backend has been removed. "
120 + "Use --attention-config.mla_prefill_backend=FLASH_ATTN or "
121 + "FLASHINFER or TRTLLM_RAGGED instead."
122 + )
123 +
124 + if self.use_trtllm_ragged_deepseek_prefill:
125 + if self.mla_prefill_backend is None:
126 + self.mla_prefill_backend = MLAPrefillBackendEnum.TRTLLM_RAGGED
127 + logger.warning_once(
128 + "use_trtllm_ragged_deepseek_prefill is deprecated and "
129 + "will be removed in v0.22. Use "
130 + "--attention-config.mla_prefill_backend=TRTLLM_RAGGED "
131 + "instead."
132 + )
133 +
134 + if self.disable_flashinfer_prefill:
135 + if self.mla_prefill_backend is None:
136 + self.mla_prefill_backend = MLAPrefillBackendEnum.FLASH_ATTN
137 + logger.warning_once(
138 + "disable_flashinfer_prefill is deprecated and will be removed "
139 + "in v0.22. Use --attention-config.mla_prefill_backend="
140 + "FLASH_ATTN instead."
141 + )
vllm/model_executor/layers/attention/mla_attention.py
+58 -595
@@ -189,12 +189,9 @@ return curr_o @ W_O
189
190 import functools
191 from abc import abstractmethod
192 -from dataclasses import dataclass, field
192 +from dataclasses import dataclass
193 from enum import Enum
194 -from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar, cast
195 -
196 -if TYPE_CHECKING:
197 - from flashinfer import BatchPrefillWithRaggedKVCacheWrapper
194 +from typing import ClassVar, Generic, TypeVar, cast
195
196 import torch
197 import torch.nn as nn
@@ -242,7 +239,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
239 kNvfp4Dynamic,
240 )
241 from vllm.platforms import current_platform
245 -from vllm.utils.flashinfer import has_flashinfer, has_nvidia_artifactory
242 +from vllm.utils.flashinfer import has_flashinfer
243 from vllm.utils.math_utils import cdiv, round_down
244 from vllm.utils.torch_utils import (
245 LayerNameType,
@@ -262,11 +259,9 @@ from vllm.v1.attention.backend import (
259 MLAAttentionImpl,
260 SparseMLAAttentionImpl,
261 )
265 -from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
262 +from vllm.v1.attention.backends.mla.prefill import MLAPrefillBackend
263 from vllm.v1.attention.backends.utils import (
264 get_dcp_local_seq_lens,
268 - get_per_layer_parameters,
269 - infer_global_hyperparameters,
265 split_decodes_and_prefills,
266 )
267 from vllm.v1.attention.ops.common import cp_lse_ag_out_rs
@@ -1123,33 +1118,6 @@ class QueryLenSupport(Enum):
1118 VARLEN = "varlen"
1119
1120
1126 -try:
1127 - from vllm.vllm_flash_attn import ( # type: ignore[attr-defined]
1128 - flash_attn_varlen_func,
1129 - )
1130 -
1131 - is_vllm_fa = True
1132 -except ImportError:
1133 - is_vllm_fa = False
1134 - flash_attn_varlen_func = None # type: ignore[assignment]
1135 - # On ROCm, vllm_flash_attn is not available, try upstream flash_attn instead.
1136 - # On CUDA, vllm_flash_attn should always be available (built with vLLM),
1137 - # so we don't attempt the fallback there.
1138 - if current_platform.is_rocm():
1139 - try:
1140 - from flash_attn import flash_attn_varlen_func # type: ignore[no-redef]
1141 - except ImportError:
1142 - logger.debug(
1143 - "flash_attn not available on ROCm; "
1144 - "MLA models using TRITON_MLA will require flash_attn. "
1145 - "AITER_MLA backends use aiter kernels instead."
1146 - )
1147 - elif current_platform.is_xpu():
1148 - from vllm._xpu_ops import xpu_ops
1149 -
1150 - flash_attn_varlen_func = xpu_ops.flash_attn_varlen_func # type: ignore[no-redef,attr-defined,assignment]
1151 -
1152 -
1121 def dynamic_per_batched_tensor_quant(
1122 x: torch.Tensor, dtype: torch.dtype = torch.float8_e4m3fn
1123 ):
@@ -1161,9 +1129,6 @@ def dynamic_per_batched_tensor_quant(
1129 return x_scl_sat.to(dtype).contiguous(), scale.float().reciprocal()
1130
1131
1164 -logger = init_logger(__name__)
1165 -
1166 -
1132 @CustomOp.register(
1133 "mla_decode_concat_quant_fp8",
1134 dynamic_arg_dims={"decode_ql_nope": 0, "decode_q_pe": 0},
@@ -1197,9 +1162,6 @@ class _DecodeConcatQuantFP8(QuantFP8):
1162 forward_hip = _make_forward(QuantFP8.forward_hip) # type: ignore[arg-type]
1163
1164
1200 -CUDNN_WORKSPACE_SIZE = 12800
1201 -
1202 -
1165 class MLACommonBackend(AttentionBackend):
1166 @staticmethod
1167 def get_name() -> str:
@@ -1268,26 +1230,9 @@ class MLACommonPrefillMetadata:
1230 query_start_loc: torch.Tensor
1231 max_query_len: int
1232 chunked_context: ChunkedContextMetadata | None = None
1271 - query_seq_lens: torch.Tensor | None = None
1272 - workspace_buffer: torch.Tensor | None = None
1233 q_data_type: torch.dtype | None = None
1234 output_dtype: torch.dtype | None = None
1275 -
1276 -
1277 -@dataclass
1278 -class FlashInferPrefillMetadata(MLACommonPrefillMetadata):
1279 - prefill_main: "BatchPrefillWithRaggedKVCacheWrapper | None" = None
1280 - prefill_chunks: "list[BatchPrefillWithRaggedKVCacheWrapper]" = field(
1281 - default_factory=list
1282 - )
1283 -
1284 -
1285 -@dataclass
1286 -class CudnnPrefillMetadata(MLACommonPrefillMetadata):
1287 - class ChunkedContextMetadata(MLACommonPrefillMetadata.ChunkedContextMetadata):
1288 - seq_lens: torch.Tensor
1289 -
1290 - cudnn_workspace: torch.Tensor | None = None
1235 + prefill_backend: MLAPrefillBackend | None = None
1236
1237
1238 @dataclass
@@ -1333,13 +1278,8 @@ class MLACommonMetadata(AttentionMetadata, Generic[D]):
1278 # The dimension of the attention heads
1279 head_dim: int | None = None
1280
1281 + prefill: MLACommonPrefillMetadata | None = None
1282 decode: D | None = None
1337 - prefill: (
1338 - MLACommonPrefillMetadata
1339 - | FlashInferPrefillMetadata
1340 - | CudnnPrefillMetadata
1341 - | None
1342 - ) = None
1283
1284 def __post_init__(self):
1285 if self.head_dim is not None and not MLACommonBackend.supports_head_size(
@@ -1352,64 +1292,6 @@ M = TypeVar("M", bound=MLACommonMetadata)
1292 A = TypeVar("A", bound=AttentionMetadata)
1293
1294
1355 -def is_deepseek_r1_mla_compatible(vllm_config: VllmConfig) -> bool:
1356 - # Check if model has DeepSeek R1 compatible MLA dimensions:
1357 - # qk_nope_head_dim = 128, qk_rope_head_dim = 64, v_head_dim = 128
1358 - # which results in query/key head dim = 192.
1359 - if vllm_config.model_config is None:
1360 - return False
1361 - hf_text_config = vllm_config.model_config.hf_text_config
1362 - qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 1)
1363 - qk_rope_head_dim = getattr(hf_text_config, "qk_rope_head_dim", 1)
1364 - v_head_dim = getattr(hf_text_config, "v_head_dim", 1)
1365 - return qk_nope_head_dim == 128 and qk_rope_head_dim == 64 and v_head_dim == 128
1366 -
1367 -
1368 -@functools.cache
1369 -def use_flashinfer_prefill() -> bool:
1370 - from vllm.config import get_current_vllm_config
1371 -
1372 - vllm_config = get_current_vllm_config()
1373 - if not (
1374 - not vllm_config.attention_config.disable_flashinfer_prefill
1375 - and has_flashinfer()
1376 - and not vllm_config.attention_config.use_cudnn_prefill
1377 - and current_platform.is_device_capability_family(100)
1378 - ):
1379 - return False
1380 -
1381 - return is_deepseek_r1_mla_compatible(vllm_config)
1382 -
1383 -
1384 -@functools.cache
1385 -def use_cudnn_prefill() -> bool:
1386 - from vllm.config import get_current_vllm_config
1387 -
1388 - vllm_config = get_current_vllm_config()
1389 - return (
1390 - has_flashinfer()
1391 - and vllm_config.attention_config.use_cudnn_prefill
1392 - and current_platform.is_device_capability_family(100)
1393 - and has_nvidia_artifactory()
1394 - )
1395 -
1396 -
1397 -@functools.cache
1398 -def use_trtllm_ragged_deepseek_prefill() -> bool:
1399 - """Check if TRT-LLM ragged DeepSeek prefill should be used."""
1400 - from vllm.config import get_current_vllm_config
1401 -
1402 - vllm_config = get_current_vllm_config()
1403 - if not (
1404 - has_flashinfer()
1405 - and vllm_config.attention_config.use_trtllm_ragged_deepseek_prefill
1406 - and current_platform.is_device_capability_family(100)
1407 - ):
1408 - return False
1409 -
1410 - return is_deepseek_r1_mla_compatible(vllm_config)
1411 -
1412 -
1295 @dataclass
1296 class MLADims:
1297 q_lora_rank: int | None
@@ -1447,15 +1329,14 @@ def get_mla_dims(model_config: ModelConfig) -> MLADims:
1329
1330 @functools.cache
1331 def backend_supports_prefill_query_quantization() -> bool:
1450 - """Check if the selected MLA backend supports prefill query quantization.
1332 + """Check if the selected MLA prefill backend supports query quantization.
1333
1334 Currently supported backends:
1453 - - FlashInfer prefill
1454 - - TRT-LLM ragged DeepSeek prefill
1335 + - FlashInfer
1336 + - TRT-LLM Ragged
1337
1338 Not supported:
1457 - - cuDNN Prefill
1458 - - FlashAttention
1339 + - FlashAttention (FA3/FA4)
1340 - Non-GB200 devices (FP8 prefill requires device capability 100)
1341 """
1342 # FP8 prefill query quantization requires GB200 (device capability 100)
@@ -1463,7 +1344,15 @@ def backend_supports_prefill_query_quantization() -> bool:
1344 if not current_platform.is_device_capability_family(100):
1345 return False
1346
1466 - return use_flashinfer_prefill() or use_trtllm_ragged_deepseek_prefill()
1347 + from vllm.config import get_current_vllm_config
1348 + from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
1349 +
1350 + vllm_config = get_current_vllm_config()
1351 + backend_cls = get_mla_prefill_backend(vllm_config)
1352 + return backend_cls.get_name() in (
1353 + "FLASHINFER",
1354 + "TRTLLM_RAGGED",
1355 + )
1356
1357
1358 class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
@@ -1574,7 +1463,6 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1463 metadata_cls if metadata_cls is not None else MLACommonMetadata
1464 )
1465 self.kv_cache_spec = kv_cache_spec
1577 - scheduler_config = vllm_config.scheduler_config
1466 self.model_config = vllm_config.model_config
1467 parallel_config = vllm_config.parallel_config
1468 self.compilation_config = vllm_config.compilation_config
@@ -1634,139 +1522,32 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1522 device=device,
1523 )
1524
1637 - self._use_cudnn_prefill = use_cudnn_prefill()
1638 - self._use_fi_prefill = use_flashinfer_prefill()
1639 - self._use_trtllm_ragged_prefill = use_trtllm_ragged_deepseek_prefill()
1640 - self.prefill_metadata_cls = (
1641 - FlashInferPrefillMetadata
1642 - if self._use_fi_prefill
1643 - else CudnnPrefillMetadata
1644 - if self._use_cudnn_prefill
1645 - else MLACommonPrefillMetadata
1646 - )
1647 -
1648 - if self._use_fi_prefill:
1649 - self._workspace_buffer = torch.empty(
1650 - envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE,
1651 - dtype=torch.uint8,
1652 - device=device,
1653 - )
1654 -
1655 - self._fi_prefill_main: BatchPrefillWithRaggedKVCacheWrapper | None = None
1656 - self._fi_prefill_chunks: list[BatchPrefillWithRaggedKVCacheWrapper] = []
1657 -
1658 - self._global_hyperparameters = infer_global_hyperparameters(
1659 - get_per_layer_parameters(vllm_config, layer_names, MLACommonImpl) # type: ignore[type-abstract]
1660 - )
1661 -
1662 - if self._use_trtllm_ragged_prefill:
1663 - self._workspace_buffer = torch.empty(
1664 - envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE,
1665 - dtype=torch.uint8,
1666 - device=device,
1667 - )
1525 + from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
1526
1669 - if self._use_cudnn_prefill:
1670 - self.cudnn_workspace = torch.empty(
1671 - CUDNN_WORKSPACE_SIZE * scheduler_config.max_num_seqs,
1672 - dtype=torch.int8,
1673 - device=device,
1674 - )
1527 + prefill_backend_cls = get_mla_prefill_backend(vllm_config)
1528 + self._prefill_backend = prefill_backend_cls(
1529 + num_heads=self.num_heads,
1530 + scale=self.model_config.get_head_size() ** -0.5,
1531 + kv_lora_rank=self.mla_dims.kv_lora_rank,
1532 + qk_nope_head_dim=self.mla_dims.qk_nope_head_dim,
1533 + qk_rope_head_dim=self.mla_dims.qk_rope_head_dim,
1534 + v_head_dim=self.mla_dims.v_head_dim,
1535 + vllm_config=vllm_config,
1536 + device=device,
1537 + layer_names=layer_names,
1538 + )
1539
1540 supports_spec_decode = self.query_len_support != QueryLenSupport.SINGLE_ONLY
1541 self._init_reorder_batch_threshold(
1542 self.reorder_batch_threshold, supports_spec_decode, supports_dcp_with_varlen
1543 )
1544
1681 - # Validate consistency between query_len_support and reorder_batch_threshold
1545 if self.query_len_support == QueryLenSupport.SINGLE_ONLY:
1546 assert self.reorder_batch_threshold == 1, (
1547 f"reorder_batch_threshold must be 1 when query_len_support is "
1548 f"SINGLE_ONLY, got {self.reorder_batch_threshold}"
1549 )
1550
1688 - def _build_fi_prefill_wrappers(self, prefill: FlashInferPrefillMetadata):
1689 - qo_indptr = prefill.query_start_loc
1690 -
1691 - has_context = False
1692 - if prefill.chunked_context is not None:
1693 - chunked_context = prefill.chunked_context
1694 - has_context = True
1695 -
1696 - if self._fi_prefill_main is None:
1697 - from flashinfer import BatchPrefillWithRaggedKVCacheWrapper
1698 -
1699 - self._fi_prefill_main = BatchPrefillWithRaggedKVCacheWrapper(
1700 - self._workspace_buffer, "NHD", backend="cutlass"
1701 - )
1702 -
1703 - if has_context:
1704 - num_chunks = chunked_context.cu_seq_lens.shape[0]
1705 - # Allocate more prefill chunk wrappers if needed
1706 - if len(self._fi_prefill_chunks) < num_chunks:
1707 - from flashinfer import BatchPrefillWithRaggedKVCacheWrapper
1708 -
1709 - for _ in range(len(self._fi_prefill_chunks), num_chunks):
1710 - self._fi_prefill_chunks.append(
1711 - BatchPrefillWithRaggedKVCacheWrapper(
1712 - self._workspace_buffer, "NHD", backend="cutlass"
1713 - )
1714 - )
1715 - assert num_chunks <= len(self._fi_prefill_chunks)
1716 -
1717 - # In MLA, the non-latent num_qo_heads == num_kv_heads
1718 - num_qo_heads = self.num_heads
1719 - num_kv_heads = num_qo_heads
1720 -
1721 - # Sanity: Verify that num_kv_heads == 1 since it is latent space
1722 - assert self.kv_cache_spec.num_kv_heads == 1
1723 -
1724 - # Get non-latent head_dim_qk and head_dim_vo
1725 - head_dim_qk = self.mla_dims.qk_nope_head_dim + self.mla_dims.qk_rope_head_dim
1726 - head_dim_vo = self.mla_dims.v_head_dim
1727 -
1728 - # For main run, qo_indptr == kv_indptr
1729 - kv_indptr = qo_indptr.clone()
1730 -
1731 - # Prepare main prefill
1732 - self._fi_prefill_main.plan(
1733 - qo_indptr=qo_indptr,
1734 - kv_indptr=kv_indptr,
1735 - num_qo_heads=num_qo_heads,
1736 - num_kv_heads=num_kv_heads,
1737 - head_dim_qk=head_dim_qk,
1738 - head_dim_vo=head_dim_vo,
1739 - causal=True, # This is main run
1740 - sm_scale=self._global_hyperparameters.sm_scale,
1741 - window_left=self._global_hyperparameters.window_left,
1742 - logits_soft_cap=self._global_hyperparameters.logits_soft_cap,
1743 - q_data_type=self.q_data_type,
1744 - o_data_type=prefill.output_dtype,
1745 - )
1746 -
1747 - # Prepare context prefills
1748 - if has_context:
1749 - for i in range(num_chunks):
1750 - kv_indptr_chunk = chunked_context.cu_seq_lens[i]
1751 -
1752 - self._fi_prefill_chunks[i].plan(
1753 - qo_indptr=qo_indptr,
1754 - kv_indptr=kv_indptr_chunk,
1755 - num_qo_heads=num_qo_heads,
1756 - num_kv_heads=num_kv_heads,
1757 - head_dim_qk=head_dim_qk,
1758 - head_dim_vo=head_dim_vo,
1759 - causal=False, # This is context run
1760 - sm_scale=self._global_hyperparameters.sm_scale,
1761 - window_left=self._global_hyperparameters.window_left,
1762 - logits_soft_cap=self._global_hyperparameters.logits_soft_cap,
1763 - q_data_type=self.q_data_type,
1764 - o_data_type=prefill.output_dtype,
1765 - )
1766 -
1767 - prefill.prefill_main = self._fi_prefill_main
1768 - prefill.prefill_chunks = self._fi_prefill_chunks
1769 -
1551 def _build_decode(
1552 self,
1553 block_table_tensor: torch.Tensor,
@@ -1972,18 +1753,14 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1753 dtype=torch.int32,
1754 )
1755
1975 - chunked_context_metadata_cls = (
1976 - CudnnPrefillMetadata.ChunkedContextMetadata
1977 - if self._use_cudnn_prefill
1978 - else MLACommonPrefillMetadata.ChunkedContextMetadata
1979 - )
1756 prefill_tokens_with_context = None
1757 if num_prefills_with_context_cpu > 0:
1758 prefill_tokens_with_context = prefill_query_start_loc_cpu[
1759 num_prefills_with_context_cpu
1760 ].item()
1761 + _ChunkedMetadata = MLACommonPrefillMetadata.ChunkedContextMetadata
1762 if self.dcp_world_size > 1:
1986 - chunked_context_metadata = chunked_context_metadata_cls(
1763 + chunked_context_metadata = _ChunkedMetadata(
1764 cu_seq_lens=cu_seq_lens_cpu.to(device, non_blocking=True),
1765 starts=local_chunk_starts.to(device, non_blocking=True),
1766 seq_tot=padded_local_chunk_seq_lens.sum(dim=1).tolist(),
@@ -2004,7 +1781,7 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1781 prefill_tokens_with_context=prefill_tokens_with_context,
1782 )
1783 else:
2007 - chunked_context_metadata = chunked_context_metadata_cls(
1784 + chunked_context_metadata = _ChunkedMetadata(
1785 cu_seq_lens=cu_seq_lens_cpu.to(device, non_blocking=True),
1786 starts=chunk_starts.to(device, non_blocking=True),
1787 seq_tot=chunk_seq_lens.sum(dim=1).tolist(),
@@ -2018,35 +1795,22 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1795 prefill_tokens_with_context=prefill_tokens_with_context,
1796 )
1797
2021 - if self._use_cudnn_prefill:
2022 - chunked_context_metadata.seq_lens = chunk_seq_lens
2023 -
1798 assert (
1799 max(chunked_context_metadata.max_seq_lens)
1800 <= self.chunked_prefill_workspace_size
1801 )
1802
2029 - prefill_metadata = self.prefill_metadata_cls(
1803 + prefill_metadata = MLACommonPrefillMetadata(
1804 block_table=block_table_tensor[reqs_start:, ...],
1805 query_start_loc=prefill_query_start_loc,
1806 max_query_len=max_query_len,
1807 chunked_context=chunked_context_metadata,
1808 output_dtype=self.model_config.dtype,
1809 q_data_type=self.q_data_type,
1810 + prefill_backend=self._prefill_backend,
1811 )
1812
2038 - if self._use_cudnn_prefill:
2039 - assert isinstance(prefill_metadata, CudnnPrefillMetadata)
2040 - prefill_metadata.query_seq_lens = (
2041 - prefill_query_start_loc[1:] - prefill_query_start_loc[:-1]
2042 - )
2043 - prefill_metadata.cudnn_workspace = self.cudnn_workspace
2044 -
2045 - if self._use_trtllm_ragged_prefill:
2046 - prefill_metadata.query_seq_lens = (
2047 - prefill_query_start_loc[1:] - prefill_query_start_loc[:-1]
2048 - )
2049 - prefill_metadata.workspace_buffer = self._workspace_buffer
1813 + self._prefill_backend.prepare_metadata(prefill_metadata)
1814
1815 decode_metadata = None
1816 if num_decodes > 0:
@@ -2091,10 +1855,6 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1855 decode=decode_metadata,
1856 )
1857
2094 - if self._use_fi_prefill and num_prefills > 0:
2095 - assert isinstance(attn_metadata.prefill, FlashInferPrefillMetadata)
2096 - self._build_fi_prefill_wrappers(attn_metadata.prefill)
2097 -
1858 return attn_metadata # type: ignore[return-value]
1859
1860
@@ -2240,308 +2000,12 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2000 and (self.qk_rope_head_dim == 64)
2001 )
2002
2243 - if use_trtllm_ragged_deepseek_prefill():
2244 - logger.info_once("Using TRT-LLM ragged DeepSeek prefill for MLA")
2245 - self._run_prefill_context_chunk = (
2246 - self._run_prefill_context_chunk_trtllm_ragged
2247 - )
2248 - self._run_prefill_new_tokens = self._run_prefill_new_tokens_trtllm_ragged
2249 - self._pad_v = False
2250 - elif use_flashinfer_prefill():
2251 - logger.info_once("Using FlashInfer prefill for MLA")
2252 - self._run_prefill_context_chunk = self._run_prefill_context_chunk_fi
2253 - self._run_prefill_new_tokens = self._run_prefill_new_tokens_fi
2254 - self._pad_v = False
2255 - elif use_cudnn_prefill():
2256 - logger.info_once("Using CUDNN prefill for MLA")
2257 - self._run_prefill_context_chunk = self._run_prefill_context_chunk_cudnn
2258 - self._run_prefill_new_tokens = self._run_prefill_new_tokens_cudnn
2259 - self._pad_v = False
2260 - else: # Use FlashAttention
2261 - if flash_attn_varlen_func is None:
2262 - raise RuntimeError(
2263 - "MLA attention requires FlashAttention but it is not "
2264 - "available. Please install flash_attn or use "
2265 - "--attention-backend ROCM_AITER_MLA."
2266 - )
2267 - logger.info_once("Using FlashAttention prefill for MLA")
2268 - self._run_prefill_context_chunk = self._run_prefill_context_chunk_fa
2269 - self._run_prefill_new_tokens = self._run_prefill_new_tokens_fa
2270 -
2271 - # Handle the differences between the flash_attn_varlen from
2272 - # flash_attn and the one from vllm_flash_attn. The former is used on
2273 - # RoCM and the latter has an additional parameter to control
2274 - # FA2 vs FA3
2275 - self.flash_attn_varlen_func = flash_attn_varlen_func
2276 - self.vllm_flash_attn_version = get_flash_attn_version(
2277 - head_size=self.qk_head_dim
2278 - )
2279 - if self.vllm_flash_attn_version is not None:
2280 - self.flash_attn_varlen_func = functools.partial(
2281 - flash_attn_varlen_func, fa_version=self.vllm_flash_attn_version
2282 - )
2283 -
2284 - # For MLA the v head dim is smaller than qk head dim so we pad out
2285 - # v with 0s to match the qk head dim for attention backends that do
2286 - # not support different headdims.
2287 - # FA3 on Hopper (SM90) and FA4 natively handle diff headdims.
2288 - device_capability = current_platform.get_device_capability()
2289 - self._pad_v = self.vllm_flash_attn_version is None or not (
2290 - (
2291 - self.vllm_flash_attn_version == 3
2292 - and device_capability is not None
2293 - and device_capability[0] == 9
2294 - )
2295 - or self.vllm_flash_attn_version == 4
2296 - )
2297 -
2003 self.dcp_world_size: int = -1
2004
2005 self.cp_kv_cache_interleave_size: int = (
2006 get_current_vllm_config().parallel_config.cp_kv_cache_interleave_size
2007 )
2008
2304 - def _flash_attn_varlen_diff_headdims(
2305 - self, q, k, v, return_softmax_lse=False, softmax_scale=None, **kwargs
2306 - ):
2307 - maybe_padded_v = v
2308 - if self._pad_v:
2309 - maybe_padded_v = torch.nn.functional.pad(
2310 - v, [0, q.shape[-1] - v.shape[-1]], value=0
2311 - )
2312 -
2313 - if is_vllm_fa:
2314 - kwargs["return_softmax_lse"] = return_softmax_lse
2315 - else:
2316 - # ROCm leverages the upstream flash_attn, which takes a parameter
2317 - # called "return_attn_probs" instead of return_softmax_lse
2318 - kwargs["return_attn_probs"] = return_softmax_lse
2319 - if envs.VLLM_BATCH_INVARIANT:
2320 - kwargs["num_splits"] = 1
2321 -
2322 - attn_out = self.flash_attn_varlen_func(
2323 - q=q,
2324 - k=k,
2325 - v=maybe_padded_v,
2326 - softmax_scale=softmax_scale,
2327 - **kwargs,
2328 - )
2329 -
2330 - # Unpack the output if there is multiple results
2331 - lse = None
2332 - if isinstance(attn_out, tuple):
2333 - attn_out, lse = attn_out[0], attn_out[1]
2334 -
2335 - # Remain consistent with old `flash_attn_varlen_func` where there
2336 - # is only one output tensor if `return_softmax_lse` is False.
2337 - if return_softmax_lse:
2338 - return attn_out, lse
2339 - return attn_out
2340 -
2341 - def _run_prefill_new_tokens_fa(
2342 - self, prefill: MLACommonPrefillMetadata, q, k, v, return_softmax_lse
2343 - ):
2344 - return self._flash_attn_varlen_diff_headdims(
2345 - q=q,
2346 - k=k,
2347 - v=v,
2348 - cu_seqlens_q=prefill.query_start_loc,
2349 - cu_seqlens_k=prefill.query_start_loc,
2350 - max_seqlen_q=prefill.max_query_len,
2351 - max_seqlen_k=prefill.max_query_len,
2352 - softmax_scale=self.scale,
2353 - causal=True,
2354 - return_softmax_lse=return_softmax_lse,
2355 - )
2356 -
2357 - def _run_prefill_new_tokens_fi(
2358 - self, prefill: MLACommonPrefillMetadata, q, k, v, return_softmax_lse
2359 - ):
2360 - assert isinstance(prefill, FlashInferPrefillMetadata)
2361 - assert prefill.prefill_main is not None
2362 -
2363 - ret = prefill.prefill_main.run(
2364 - q=q,
2365 - k=k,
2366 - v=v,
2367 - return_lse=return_softmax_lse,
2368 - )
2369 -
2370 - if isinstance(ret, tuple):
2371 - return ret[0], ret[1].transpose(0, 1).contiguous()
2372 - return ret
2373 -
2374 - def _run_prefill_new_tokens_cudnn(
2375 - self, prefill: MLACommonPrefillMetadata, q, k, v, return_softmax_lse
2376 - ):
2377 - assert isinstance(prefill, CudnnPrefillMetadata)
2378 - assert prefill.query_seq_lens is not None
2379 - from flashinfer.prefill import cudnn_batch_prefill_with_kv_cache
2380 -
2381 - output, lse = cudnn_batch_prefill_with_kv_cache(
2382 - q=q,
2383 - k_cache=k,
2384 - v_cache=v,
2385 - scale=self.scale,
2386 - workspace_buffer=prefill.cudnn_workspace,
2387 - max_token_per_sequence=prefill.max_query_len,
2388 - max_sequence_kv=prefill.max_query_len,
2389 - actual_seq_lens_q=prefill.query_seq_lens.view(-1, 1, 1, 1),
2390 - actual_seq_lens_kv=prefill.query_seq_lens.view(-1, 1, 1, 1),
2391 - causal=True,
2392 - # Do not support False for now
2393 - return_lse=True,
2394 - # Indicates actual_seq_lens are on GPU or CPU.
2395 - is_cuda_graph_compatible=True,
2396 - )
2397 - if return_softmax_lse:
2398 - return output, lse
2399 - return output
2400 -
2401 - def _run_prefill_context_chunk_fa(
2402 - self, prefill: MLACommonPrefillMetadata, chunk_idx: int, q, k, v
2403 - ):
2404 - assert prefill.chunked_context is not None
2405 - return self._flash_attn_varlen_diff_headdims(
2406 - q=q,
2407 - k=k,
2408 - v=v,
2409 - cu_seqlens_q=prefill.query_start_loc,
2410 - cu_seqlens_k=prefill.chunked_context.cu_seq_lens[chunk_idx],
2411 - max_seqlen_q=prefill.max_query_len,
2412 - max_seqlen_k=prefill.chunked_context.max_seq_lens[chunk_idx],
2413 - softmax_scale=self.scale,
2414 - causal=False, # Context is unmasked
2415 - return_softmax_lse=True,
2416 - )
2417 -
2418 - def _run_prefill_context_chunk_fi(
2419 - self, prefill: MLACommonPrefillMetadata, chunk_idx: int, q, k, v
2420 - ):
2421 - assert isinstance(prefill, FlashInferPrefillMetadata)
2422 -
2423 - attn_out, lse = prefill.prefill_chunks[chunk_idx].run(
2424 - q=q,
2425 - k=k,
2426 - v=v,
2427 - return_lse=True,
2428 - )
2429 -
2430 - # Convert from (q_len, num_heads) to (num_heads, q_len)
2431 - return attn_out, lse.transpose(0, 1).contiguous()
2432 -
2433 - def _run_prefill_context_chunk_cudnn(
2434 - self, prefill: MLACommonPrefillMetadata, chunk_idx: int, q, k, v
2435 - ):
2436 - assert isinstance(prefill, CudnnPrefillMetadata)
2437 - assert prefill.chunked_context is not None
2438 - assert prefill.chunked_context.seq_lens[chunk_idx] is not None
2439 - assert prefill.query_seq_lens is not None
2440 - from flashinfer.prefill import cudnn_batch_prefill_with_kv_cache
2441 -
2442 - return cudnn_batch_prefill_with_kv_cache(
2443 - q=q,
2444 - k_cache=k,
2445 - v_cache=v,
2446 - scale=self.scale,
2447 - workspace_buffer=prefill.cudnn_workspace,
2448 - max_token_per_sequence=prefill.max_query_len,
2449 - max_sequence_kv=prefill.chunked_context.max_seq_lens[chunk_idx],
2450 - actual_seq_lens_q=prefill.query_seq_lens.view(-1, 1, 1, 1),
2451 - actual_seq_lens_kv=prefill.chunked_context.seq_lens[chunk_idx].view(
2452 - -1, 1, 1, 1
2453 - ),
2454 - causal=False,
2455 - return_lse=True,
2456 - # Indicates actual_seq_lens are on GPU or CPU.
2457 - is_cuda_graph_compatible=True,
2458 - )
2459 -
2460 - def _run_prefill_new_tokens_trtllm_ragged(
2461 - self, prefill: MLACommonPrefillMetadata, q, k, v, return_softmax_lse
2462 - ):
2463 - """TRT-LLM ragged attention for new tokens (causal)."""
2464 - from flashinfer.prefill import trtllm_ragged_attention_deepseek
2465 -
2466 - assert prefill.query_seq_lens is not None
2467 - assert prefill.workspace_buffer is not None
2468 - # allocate BF16 / FP16 output tensor for TRT-LLM ragged attention
2469 - out = torch.empty(
2470 - q.shape[0],
2471 - q.shape[1],
2472 - v.shape[2],
2473 - device=q.device,
2474 - dtype=prefill.output_dtype,
2475 - )
2476 -
2477 - ret = trtllm_ragged_attention_deepseek(
2478 - query=q,
2479 - key=k,
2480 - value=v,
2481 - workspace_buffer=prefill.workspace_buffer,
2482 - seq_lens=prefill.query_seq_lens,
2483 - max_q_len=prefill.max_query_len,
2484 - max_kv_len=prefill.max_query_len,
2485 - bmm1_scale=self.scale,
2486 - bmm2_scale=1.0,
2487 - o_sf_scale=1.0,
2488 - batch_size=prefill.query_seq_lens.shape[0],
2489 - window_left=-1,
2490 - cum_seq_lens_q=prefill.query_start_loc,
2491 - cum_seq_lens_kv=prefill.query_start_loc,
2492 - enable_pdl=False,
2493 - is_causal=True,
2494 - return_lse=return_softmax_lse,
2495 - out=out,
2496 - )
2497 -
2498 - if isinstance(ret, tuple):
2499 - # Convert from (q_len, num_heads) to (num_heads, q_len)
2500 - return ret[0], ret[1].transpose(0, 1).contiguous()
2501 - return ret
2502 -
2503 - def _run_prefill_context_chunk_trtllm_ragged(
2504 - self, prefill: MLACommonPrefillMetadata, chunk_idx: int, q, k, v
2505 - ):
2506 - """TRT-LLM ragged attention for context chunks (non-causal)."""
2507 - from flashinfer.prefill import trtllm_ragged_attention_deepseek
2508 -
2509 - assert prefill.chunked_context is not None
2510 - assert prefill.chunked_context.seq_lens[chunk_idx] is not None
2511 - assert prefill.workspace_buffer is not None
2512 -
2513 - out = torch.empty(
2514 - q.shape[0],
2515 - q.shape[1],
2516 - v.shape[2],
2517 - device=q.device,
2518 - dtype=prefill.output_dtype,
2519 - )
2520 -
2521 - attn_out, lse = trtllm_ragged_attention_deepseek(
2522 - query=q,
2523 - key=k,
2524 - value=v,
2525 - workspace_buffer=prefill.workspace_buffer,
2526 - seq_lens=prefill.chunked_context.seq_lens[chunk_idx],
2527 - max_q_len=prefill.max_query_len,
2528 - max_kv_len=prefill.chunked_context.max_seq_lens[chunk_idx],
2529 - bmm1_scale=self.scale,
2530 - bmm2_scale=1.0,
2531 - o_sf_scale=1.0,
2532 - batch_size=prefill.chunked_context.seq_lens[chunk_idx].shape[0],
2533 - window_left=-1,
2534 - cum_seq_lens_q=prefill.query_start_loc,
2535 - cum_seq_lens_kv=prefill.chunked_context.cu_seq_lens[chunk_idx],
2536 - enable_pdl=False,
2537 - is_causal=False,
2538 - return_lse=True,
2539 - out=out,
2540 - )
2541 -
2542 - # Convert from (q_len, num_heads) to (num_heads, q_len)
2543 - return attn_out, lse.transpose(0, 1).contiguous()
2544 -
2009 def _concat_k_nope_k_pe(
2010 self, k_nope: torch.Tensor, k_pe: torch.Tensor
2011 ) -> torch.Tensor:
@@ -2582,6 +2046,7 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2046 ):
2047 assert attn_metadata.prefill is not None
2048 prefill_metadata = attn_metadata.prefill
2049 + assert prefill_metadata.prefill_backend is not None
2050 assert prefill_metadata.chunked_context is not None
2051
2052 use_fp8_prefill = prefill_metadata.q_data_type == current_platform.fp8_dtype()
@@ -2649,12 +2114,13 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2114
2115 k = self._concat_k_nope_k_pe(k_nope, k_pe)
2116
2652 - attn_output, attn_softmax_lse = self._run_prefill_context_chunk(
2653 - prefill=prefill_metadata,
2654 - chunk_idx=i,
2655 - q=q,
2656 - k=k,
2657 - v=v,
2117 + attn_output, attn_softmax_lse = (
2118 + prefill_metadata.prefill_backend.run_prefill_context_chunk(
2119 + chunk_idx=i,
2120 + q=q,
2121 + k=k,
2122 + v=v,
2123 + )
2124 )
2125
2126 if output is None:
@@ -2687,6 +2153,7 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2153 assert k_scale is None, "DCP not support scaled kvcache now."
2154 assert attn_metadata.prefill is not None
2155 prefill_metadata = attn_metadata.prefill
2156 + assert prefill_metadata.prefill_backend is not None
2157 assert prefill_metadata.chunked_context is not None
2158 assert prefill_metadata.chunked_context.padded_local_chunk_seq_lens is not None
2159 assert prefill_metadata.chunked_context.local_context_lens_allranks is not None
@@ -2753,12 +2220,13 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2220 k_nope, v = kv_nope.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1)
2221 k = self._concat_k_nope_k_pe(k_nope, k_pe)
2222
2756 - attn_output, attn_softmax_lse = self._run_prefill_context_chunk(
2757 - prefill=prefill_metadata,
2758 - chunk_idx=i,
2759 - q=q,
2760 - k=k,
2761 - v=v,
2223 + attn_output, attn_softmax_lse = (
2224 + prefill_metadata.prefill_backend.run_prefill_context_chunk(
2225 + chunk_idx=i,
2226 + q=q,
2227 + k=k,
2228 + v=v,
2229 + )
2230 )
2231
2232 if output is None:
@@ -2790,11 +2258,11 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2258 k_scale: torch.Tensor,
2259 output: torch.Tensor,
2260 ) -> None:
2793 - # TODO (zyongye): Prefill function here
2261 assert attn_metadata.prefill is not None
2262 assert self.dcp_world_size != -1
2263
2264 prefill_metadata = attn_metadata.prefill
2265 + assert prefill_metadata.prefill_backend is not None
2266 use_fp8_prefill = prefill_metadata.q_data_type == current_platform.fp8_dtype()
2267
2268 # Convert q to FP8 if FP8 prefill attention is enabled
@@ -2813,8 +2281,7 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2281 k = k.to(prefill_metadata.q_data_type)
2282 v = v.to(prefill_metadata.q_data_type)
2283
2816 - output_prefill = self._run_prefill_new_tokens(
2817 - prefill=prefill_metadata,
2284 + output_prefill = prefill_metadata.prefill_backend.run_prefill_new_tokens(
2285 q=q,
2286 k=k,
2287 v=v,
@@ -2839,11 +2306,6 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2306 q, kv_c_and_k_pe_cache, attn_metadata, k_scale
2307 )
2308
2842 - # unpad if necessary
2843 - if self._pad_v:
2844 - context_output = context_output[..., : v.shape[-1]]
2845 - suffix_output = suffix_output[..., : v.shape[-1]]
2846 -
2309 output = output.view(-1, self.num_heads, self.v_head_dim)
2310 merge_attn_states(
2311 output=output,
@@ -2854,7 +2316,8 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2316 prefill_tokens_with_context=prefill_metadata.chunked_context.prefill_tokens_with_context,
2317 )
2318 else:
2857 - output_prefill = output_prefill[..., : v.shape[-1]].flatten(start_dim=-2)
2319 + assert isinstance(output_prefill, torch.Tensor)
2320 + output_prefill = output_prefill.flatten(start_dim=-2)
2321 output.copy_(output_prefill)
2322
2323 @abstractmethod
vllm/platforms/interface.py
+3
@@ -86,6 +86,9 @@ class DeviceCapability(NamedTuple):
86 return NotImplemented
87 return (self.major, self.minor) > (other.major, other.minor)
88
89 + def __hash__(self) -> int:
90 + return hash((self.major, self.minor))
91 +
92 def as_version_str(self) -> str:
93 return f"{self.major}.{self.minor}"
94
vllm/v1/attention/backends/mla/prefill/__init__.py new
+11
@@ -0,0 +1,11 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
4 +from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
5 +from vllm.v1.attention.backends.mla.prefill.selector import get_mla_prefill_backend
6 +
7 +__all__ = [
8 + "MLAPrefillBackend",
9 + "MLAPrefillBackendEnum",
10 + "get_mla_prefill_backend",
11 +]
vllm/v1/attention/backends/mla/prefill/base.py new
+125
@@ -0,0 +1,125 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Abstract base class for MLA prefill backends."""
4 +
5 +from abc import ABC, abstractmethod
6 +from typing import TYPE_CHECKING, ClassVar
7 +
8 +import torch
9 +
10 +if TYPE_CHECKING:
11 + from vllm.config import VllmConfig
12 + from vllm.model_executor.layers.attention.mla_attention import (
13 + MLACommonPrefillMetadata,
14 + )
15 + from vllm.platforms.interface import DeviceCapability
16 + from vllm.v1.attention.backends.mla.prefill.selector import (
17 + MLAPrefillSelectorConfig,
18 + )
19 +
20 +
21 +class MLAPrefillBackend(ABC):
22 + """Abstract base class for MLA prefill backends."""
23 +
24 + supported_dtypes: ClassVar[list[torch.dtype]] = [
25 + torch.float16,
26 + torch.bfloat16,
27 + ]
28 + requires_r1_mla_dimensions: ClassVar[bool] = False
29 +
30 + @staticmethod
31 + @abstractmethod
32 + def get_name() -> str:
33 + raise NotImplementedError
34 +
35 + @classmethod
36 + def supports_compute_capability(cls, device_capability: "DeviceCapability") -> bool:
37 + return True
38 +
39 + @classmethod
40 + def supports_dtype(cls, dtype: torch.dtype) -> bool:
41 + return dtype in cls.supported_dtypes
42 +
43 + @classmethod
44 + def is_available(cls) -> bool:
45 + return True
46 +
47 + @classmethod
48 + def validate_configuration(
49 + cls,
50 + device_capability: "DeviceCapability",
51 + selector_config: "MLAPrefillSelectorConfig",
52 + ) -> list[str]:
53 + invalid_reasons: list[str] = []
54 +
55 + if not cls.supports_compute_capability(device_capability):
56 + invalid_reasons.append(
57 + f"compute capability {device_capability.major}."
58 + f"{device_capability.minor} not supported"
59 + )
60 +
61 + if not cls.supports_dtype(selector_config.dtype):
62 + invalid_reasons.append(f"dtype {selector_config.dtype} not supported")
63 +
64 + if not cls.is_available():
65 + invalid_reasons.append("required dependencies not available")
66 +
67 + if cls.requires_r1_mla_dimensions and not selector_config.is_r1_compatible:
68 + invalid_reasons.append(
69 + "model does not have DeepSeek R1 MLA dimensions "
70 + "(qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128)"
71 + )
72 +
73 + return invalid_reasons
74 +
75 + def __init__(
76 + self,
77 + num_heads: int,
78 + scale: float,
79 + kv_lora_rank: int,
80 + qk_nope_head_dim: int,
81 + qk_rope_head_dim: int,
82 + v_head_dim: int,
83 + vllm_config: "VllmConfig",
84 + device: torch.device,
85 + layer_names: list[str] | None = None,
86 + ) -> None:
87 + self.num_heads = num_heads
88 + self.scale = scale
89 + self.kv_lora_rank = kv_lora_rank
90 + self.qk_nope_head_dim = qk_nope_head_dim
91 + self.qk_rope_head_dim = qk_rope_head_dim
92 + self.v_head_dim = v_head_dim
93 + self.vllm_config = vllm_config
94 + self.device = device
95 + self.layer_names = layer_names
96 +
97 + def prepare_metadata( # noqa: B027
98 + self,
99 + prefill_metadata: "MLACommonPrefillMetadata",
100 + ) -> None:
101 + """Prepare backend-specific metadata before the forward pass.
102 +
103 + Called by the metadata builder after constructing the prefill metadata.
104 + """
105 + self._prefill_metadata = prefill_metadata
106 +
107 + @abstractmethod
108 + def run_prefill_new_tokens(
109 + self,
110 + q: torch.Tensor,
111 + k: torch.Tensor,
112 + v: torch.Tensor,
113 + return_softmax_lse: bool,
114 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
115 + raise NotImplementedError
116 +
117 + @abstractmethod
118 + def run_prefill_context_chunk(
119 + self,
120 + chunk_idx: int,
121 + q: torch.Tensor,
122 + k: torch.Tensor,
123 + v: torch.Tensor,
124 + ) -> tuple[torch.Tensor, torch.Tensor]:
125 + raise NotImplementedError
vllm/v1/attention/backends/mla/prefill/flash_attn.py new
+180
@@ -0,0 +1,180 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""FlashAttention backend for MLA prefill."""
4 +
5 +import functools
6 +from typing import TYPE_CHECKING
7 +
8 +import torch
9 +
10 +import vllm.envs as envs
11 +from vllm.platforms import current_platform
12 +from vllm.v1.attention.backends.fa_utils import (
13 + get_flash_attn_version,
14 + is_flash_attn_varlen_func_available,
15 +)
16 +from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
17 +
18 +if TYPE_CHECKING:
19 + from vllm.config import VllmConfig
20 +
21 +if is_flash_attn_varlen_func_available():
22 + from vllm.v1.attention.backends.fa_utils import flash_attn_varlen_func
23 +else:
24 + flash_attn_varlen_func = None # type: ignore[assignment]
25 +
26 +
27 +class FlashAttnPrefillBackend(MLAPrefillBackend):
28 + """FlashAttention backend for MLA prefill."""
29 +
30 + @staticmethod
31 + def get_name() -> str:
32 + return "FLASH_ATTN"
33 +
34 + @classmethod
35 + def is_available(cls) -> bool:
36 + return is_flash_attn_varlen_func_available()
37 +
38 + def __init__(
39 + self,
40 + num_heads: int,
41 + scale: float,
42 + kv_lora_rank: int,
43 + qk_nope_head_dim: int,
44 + qk_rope_head_dim: int,
45 + v_head_dim: int,
46 + vllm_config: "VllmConfig",
47 + device: torch.device,
48 + layer_names: list[str] | None = None,
49 + ) -> None:
50 + super().__init__(
51 + num_heads=num_heads,
52 + scale=scale,
53 + kv_lora_rank=kv_lora_rank,
54 + qk_nope_head_dim=qk_nope_head_dim,
55 + qk_rope_head_dim=qk_rope_head_dim,
56 + v_head_dim=v_head_dim,
57 + vllm_config=vllm_config,
58 + device=device,
59 + layer_names=layer_names,
60 + )
61 +
62 + # Handle the differences between the flash_attn_varlen from
63 + # flash_attn and the one from vllm_flash_attn
64 + assert flash_attn_varlen_func is not None, (
65 + "FlashAttnPrefillBackend requires flash_attn_varlen_func. "
66 + "Ensure FlashAttnPrefillBackend.is_available() is checked first."
67 + )
68 + qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
69 + self.flash_attn_varlen_func = flash_attn_varlen_func
70 + self.vllm_flash_attn_version = get_flash_attn_version(head_size=qk_head_dim)
71 + if self.vllm_flash_attn_version is not None:
72 + self.flash_attn_varlen_func = functools.partial(
73 + flash_attn_varlen_func, fa_version=self.vllm_flash_attn_version
74 + )
75 +
76 + # Determine if we need to pad V
77 + # For MLA the v head dim is smaller than qk head dim so we pad out
78 + # v with 0s to match the qk head dim for attention backends that do
79 + # not support different headdims.
80 + # FA3 on Hopper (SM90) and FA4 natively handle diff headdims.
81 + device_capability = current_platform.get_device_capability()
82 + self.requires_v_padding = self.vllm_flash_attn_version is None or not (
83 + (
84 + self.vllm_flash_attn_version == 3
85 + and device_capability is not None
86 + and device_capability[0] == 9
87 + )
88 + or self.vllm_flash_attn_version == 4
89 + )
90 +
91 + # Track whether we're using vllm's FA or upstream (for ROCm)
92 + self._is_vllm_fa = current_platform.is_cuda() or current_platform.is_xpu()
93 +
94 + def _flash_attn_varlen_diff_headdims(
95 + self,
96 + q: torch.Tensor,
97 + k: torch.Tensor,
98 + v: torch.Tensor,
99 + return_softmax_lse: bool = False,
100 + softmax_scale: float | None = None,
101 + **kwargs,
102 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
103 + maybe_padded_v = v
104 + if self.requires_v_padding:
105 + maybe_padded_v = torch.nn.functional.pad(
106 + v, [0, q.shape[-1] - v.shape[-1]], value=0
107 + )
108 +
109 + if self._is_vllm_fa:
110 + kwargs["return_softmax_lse"] = return_softmax_lse
111 + else:
112 + # ROCm leverages the upstream flash_attn, which takes a parameter
113 + # called "return_attn_probs" instead of return_softmax_lse
114 + kwargs["return_attn_probs"] = return_softmax_lse
115 + if envs.VLLM_BATCH_INVARIANT:
116 + kwargs["num_splits"] = 1
117 +
118 + attn_out = self.flash_attn_varlen_func(
119 + q=q,
120 + k=k,
121 + v=maybe_padded_v,
122 + softmax_scale=softmax_scale,
123 + **kwargs,
124 + )
125 +
126 + # Unpack the output if there are multiple results
127 + lse = None
128 + if isinstance(attn_out, tuple):
129 + attn_out, lse = attn_out[0], attn_out[1]
130 +
131 + # Unpad output back to v_head_dim if we padded V
132 + if self.requires_v_padding:
133 + attn_out = attn_out[..., : v.shape[-1]]
134 +
135 + # Remain consistent with old `flash_attn_varlen_func` where there
136 + # is only one output tensor if `return_softmax_lse` is False.
137 + if return_softmax_lse:
138 + return attn_out, lse
139 + return attn_out
140 +
141 + def run_prefill_new_tokens(
142 + self,
143 + q: torch.Tensor,
144 + k: torch.Tensor,
145 + v: torch.Tensor,
146 + return_softmax_lse: bool,
147 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
148 + return self._flash_attn_varlen_diff_headdims(
149 + q=q,
150 + k=k,
151 + v=v,
152 + cu_seqlens_q=self._prefill_metadata.query_start_loc,
153 + cu_seqlens_k=self._prefill_metadata.query_start_loc,
154 + max_seqlen_q=self._prefill_metadata.max_query_len,
155 + max_seqlen_k=self._prefill_metadata.max_query_len,
156 + softmax_scale=self.scale,
157 + causal=True,
158 + return_softmax_lse=return_softmax_lse,
159 + )
160 +
161 + def run_prefill_context_chunk(
162 + self,
163 + chunk_idx: int,
164 + q: torch.Tensor,
165 + k: torch.Tensor,
166 + v: torch.Tensor,
167 + ) -> tuple[torch.Tensor, torch.Tensor]:
168 + assert self._prefill_metadata.chunked_context is not None
169 + return self._flash_attn_varlen_diff_headdims(
170 + q=q,
171 + k=k,
172 + v=v,
173 + cu_seqlens_q=self._prefill_metadata.query_start_loc,
174 + cu_seqlens_k=self._prefill_metadata.chunked_context.cu_seq_lens[chunk_idx],
175 + max_seqlen_q=self._prefill_metadata.max_query_len,
176 + max_seqlen_k=self._prefill_metadata.chunked_context.max_seq_lens[chunk_idx],
177 + softmax_scale=self.scale,
178 + causal=False, # Context is unmasked
179 + return_softmax_lse=True,
180 + )
vllm/v1/attention/backends/mla/prefill/flashinfer.py new
+211
@@ -0,0 +1,211 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""FlashInfer backend for MLA prefill."""
4 +
5 +from typing import TYPE_CHECKING
6 +
7 +import torch
8 +
9 +import vllm.envs as envs
10 +from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
11 +from vllm.v1.attention.backends.utils import (
12 + get_per_layer_parameters,
13 + infer_global_hyperparameters,
14 +)
15 +from vllm.v1.worker.workspace import current_workspace_manager
16 +
17 +if TYPE_CHECKING:
18 + from vllm.config import VllmConfig
19 + from vllm.model_executor.layers.attention.mla_attention import (
20 + MLACommonPrefillMetadata,
21 + )
22 + from vllm.platforms.interface import DeviceCapability
23 +
24 +try:
25 + from flashinfer import BatchPrefillWithRaggedKVCacheWrapper
26 +except ImportError:
27 + BatchPrefillWithRaggedKVCacheWrapper = object # type: ignore[misc,assignment]
28 +
29 +_DEFAULT_NUM_CHUNKS = 32
30 +
31 +
32 +class FlashInferPrefillBackend(MLAPrefillBackend):
33 + """FlashInfer backend for MLA prefill."""
34 +
35 + requires_r1_mla_dimensions = True
36 +
37 + @staticmethod
38 + def get_name() -> str:
39 + return "FLASHINFER"
40 +
41 + @classmethod
42 + def supports_compute_capability(cls, device_capability: "DeviceCapability") -> bool:
43 + return device_capability.major == 10
44 +
45 + @classmethod
46 + def is_available(cls) -> bool:
47 + try:
48 + from flashinfer import (
49 + BatchPrefillWithRaggedKVCacheWrapper, # noqa: F401
50 + )
51 +
52 + return True
53 + except ImportError:
54 + return False
55 +
56 + def __init__(
57 + self,
58 + num_heads: int,
59 + scale: float,
60 + kv_lora_rank: int,
61 + qk_nope_head_dim: int,
62 + qk_rope_head_dim: int,
63 + v_head_dim: int,
64 + vllm_config: "VllmConfig",
65 + device: torch.device,
66 + layer_names: list[str] | None = None,
67 + ) -> None:
68 + super().__init__(
69 + num_heads=num_heads,
70 + scale=scale,
71 + kv_lora_rank=kv_lora_rank,
72 + qk_nope_head_dim=qk_nope_head_dim,
73 + qk_rope_head_dim=qk_rope_head_dim,
74 + v_head_dim=v_head_dim,
75 + vllm_config=vllm_config,
76 + device=device,
77 + layer_names=layer_names,
78 + )
79 +
80 + self._prefill_main: BatchPrefillWithRaggedKVCacheWrapper | None = None
81 + self._prefill_chunks: list[BatchPrefillWithRaggedKVCacheWrapper] = []
82 + if layer_names is None:
83 + raise ValueError(
84 + "FlashInferPrefillBackend requires layer_names to "
85 + "initialize global hyperparameters."
86 + )
87 +
88 + from vllm.model_executor.layers.attention.mla_attention import (
89 + MLACommonImpl,
90 + )
91 +
92 + self._global_hyperparameters = infer_global_hyperparameters(
93 + get_per_layer_parameters(vllm_config, layer_names, MLACommonImpl) # type: ignore[type-abstract]
94 + )
95 +
96 + def _ensure_chunks(
97 + self,
98 + num_chunks: int,
99 + workspace_buffer: torch.Tensor,
100 + ) -> None:
101 + if len(self._prefill_chunks) < num_chunks:
102 + for _ in range(len(self._prefill_chunks), num_chunks):
103 + self._prefill_chunks.append(
104 + BatchPrefillWithRaggedKVCacheWrapper(
105 + workspace_buffer, "NHD", backend="cutlass"
106 + )
107 + )
108 +
109 + def prepare_metadata(
110 + self,
111 + prefill_metadata: "MLACommonPrefillMetadata",
112 + ) -> None:
113 + qo_indptr = prefill_metadata.query_start_loc
114 + has_context = prefill_metadata.chunked_context is not None
115 + (workspace_buffer,) = current_workspace_manager().get_simultaneous(
116 + ((envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE,), torch.uint8),
117 + )
118 +
119 + if self._prefill_main is None:
120 + self._prefill_main = BatchPrefillWithRaggedKVCacheWrapper(
121 + workspace_buffer, "NHD", backend="cutlass"
122 + )
123 + self._ensure_chunks(_DEFAULT_NUM_CHUNKS, workspace_buffer)
124 +
125 + if has_context:
126 + chunked_context = prefill_metadata.chunked_context
127 + assert chunked_context is not None
128 + num_chunks = chunked_context.cu_seq_lens.shape[0]
129 + self._ensure_chunks(num_chunks, workspace_buffer)
130 +
131 + num_qo_heads = self.num_heads
132 + num_kv_heads = num_qo_heads
133 +
134 + head_dim_qk = self.qk_nope_head_dim + self.qk_rope_head_dim
135 + head_dim_vo = self.v_head_dim
136 + kv_indptr = qo_indptr.clone()
137 +
138 + assert self._prefill_main is not None
139 + self._prefill_main.plan(
140 + qo_indptr=qo_indptr,
141 + kv_indptr=kv_indptr,
142 + num_qo_heads=num_qo_heads,
143 + num_kv_heads=num_kv_heads,
144 + head_dim_qk=head_dim_qk,
145 + head_dim_vo=head_dim_vo,
146 + causal=True,
147 + sm_scale=self._global_hyperparameters.sm_scale,
148 + window_left=self._global_hyperparameters.window_left,
149 + logits_soft_cap=self._global_hyperparameters.logits_soft_cap,
150 + q_data_type=prefill_metadata.q_data_type,
151 + o_data_type=prefill_metadata.output_dtype,
152 + )
153 +
154 + if has_context:
155 + chunked_context = prefill_metadata.chunked_context
156 + assert chunked_context is not None
157 + for i in range(num_chunks):
158 + kv_indptr_chunk = chunked_context.cu_seq_lens[i]
159 +
160 + self._prefill_chunks[i].plan(
161 + qo_indptr=qo_indptr,
162 + kv_indptr=kv_indptr_chunk,
163 + num_qo_heads=num_qo_heads,
164 + num_kv_heads=num_kv_heads,
165 + head_dim_qk=head_dim_qk,
166 + head_dim_vo=head_dim_vo,
167 + causal=False,
168 + sm_scale=self._global_hyperparameters.sm_scale,
169 + window_left=self._global_hyperparameters.window_left,
170 + logits_soft_cap=self._global_hyperparameters.logits_soft_cap,
171 + q_data_type=prefill_metadata.q_data_type,
172 + o_data_type=prefill_metadata.output_dtype,
173 + )
174 +
175 + def run_prefill_new_tokens(
176 + self,
177 + q: torch.Tensor,
178 + k: torch.Tensor,
179 + v: torch.Tensor,
180 + return_softmax_lse: bool,
181 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
182 + assert self._prefill_main is not None
183 +
184 + ret = self._prefill_main.run(
185 + q=q,
186 + k=k,
187 + v=v,
188 + return_lse=return_softmax_lse,
189 + )
190 +
191 + if isinstance(ret, tuple):
192 + # Convert from (q_len, num_heads) to (num_heads, q_len)
193 + return ret[0], ret[1].transpose(0, 1).contiguous()
194 + return ret
195 +
196 + def run_prefill_context_chunk(
197 + self,
198 + chunk_idx: int,
199 + q: torch.Tensor,
200 + k: torch.Tensor,
201 + v: torch.Tensor,
202 + ) -> tuple[torch.Tensor, torch.Tensor]:
203 + attn_out, lse = self._prefill_chunks[chunk_idx].run(
204 + q=q,
205 + k=k,
206 + v=v,
207 + return_lse=True,
208 + )
209 +
210 + # Convert from (q_len, num_heads) to (num_heads, q_len)
211 + return attn_out, lse.transpose(0, 1).contiguous()
vllm/v1/attention/backends/mla/prefill/registry.py new
+53
@@ -0,0 +1,53 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Registry for MLA prefill backends.
4 +
5 +This module provides an enumeration of all available MLA prefill backends
6 +and utilities for loading them.
7 +"""
8 +
9 +from enum import Enum, EnumMeta
10 +from typing import TYPE_CHECKING
11 +
12 +from vllm.utils.import_utils import resolve_obj_by_qualname
13 +
14 +if TYPE_CHECKING:
15 + from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
16 +
17 +
18 +class _MLAPrefillBackendEnumMeta(EnumMeta):
19 + """Metaclass for MLAPrefillBackendEnum to provide better error messages."""
20 +
21 + def __getitem__(cls, name: str):
22 + try:
23 + return super().__getitem__(name)
24 + except KeyError:
25 + members = cls.__members__.keys()
26 + valid_backends = ", ".join(members)
27 + raise ValueError(
28 + f"Unknown MLA prefill backend: '{name}'. "
29 + f"Valid options are: {valid_backends}"
30 + ) from None
31 +
32 +
33 +class MLAPrefillBackendEnum(Enum, metaclass=_MLAPrefillBackendEnumMeta):
34 + """Enumeration of all supported MLA prefill backends."""
35 +
36 + FLASH_ATTN = (
37 + "vllm.v1.attention.backends.mla.prefill.flash_attn.FlashAttnPrefillBackend"
38 + )
39 + FLASHINFER = (
40 + "vllm.v1.attention.backends.mla.prefill.flashinfer.FlashInferPrefillBackend"
41 + )
42 + TRTLLM_RAGGED = (
43 + "vllm.v1.attention.backends.mla.prefill.trtllm_ragged."
44 + "TrtllmRaggedPrefillBackend"
45 + )
46 +
47 + def get_path(self) -> str:
48 + """Get the fully qualified class path for this backend."""
49 + return self.value
50 +
51 + def get_class(self) -> "type[MLAPrefillBackend]":
52 + """Lazy load and return the backend class."""
53 + return resolve_obj_by_qualname(self.get_path())
vllm/v1/attention/backends/mla/prefill/selector.py new
+183
@@ -0,0 +1,183 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Selector for MLA prefill backends.
4 +
5 +This module provides functions for selecting the appropriate MLA prefill
6 +backend based on device capabilities and configuration.
7 +"""
8 +
9 +from functools import cache
10 +from typing import TYPE_CHECKING, NamedTuple
11 +
12 +import torch
13 +
14 +from vllm.logger import init_logger
15 +from vllm.platforms.interface import DeviceCapability
16 +from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
17 +
18 +if TYPE_CHECKING:
19 + from vllm.config import VllmConfig
20 + from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
21 +
22 +logger = init_logger(__name__)
23 +
24 +
25 +class MLAPrefillSelectorConfig(NamedTuple):
26 + """Hashable configuration for MLA prefill backend selection.
27 +
28 + This is analogous to AttentionSelectorConfig and contains model-specific
29 + configuration needed to select an MLA prefill backend, extracted from
30 + VllmConfig into a hashable form for caching.
31 + """
32 +
33 + dtype: torch.dtype
34 + is_r1_compatible: bool
35 +
36 +
37 +def is_deepseek_r1_mla_compatible(vllm_config: "VllmConfig") -> bool:
38 + """Check if model has DeepSeek R1 compatible MLA dimensions.
39 +
40 + DeepSeek R1 MLA dimensions are:
41 + - qk_nope_head_dim = 128
42 + - qk_rope_head_dim = 64
43 + - v_head_dim = 128
44 + """
45 + if vllm_config.model_config is None:
46 + return False
47 + hf_text_config = vllm_config.model_config.hf_text_config
48 + qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 1)
49 + qk_rope_head_dim = getattr(hf_text_config, "qk_rope_head_dim", 1)
50 + v_head_dim = getattr(hf_text_config, "v_head_dim", 1)
51 + return qk_nope_head_dim == 128 and qk_rope_head_dim == 64 and v_head_dim == 128
52 +
53 +
54 +def _get_mla_prefill_backend_priorities(
55 + device_capability: DeviceCapability,
56 +) -> list[MLAPrefillBackendEnum]:
57 + """Get MLA prefill backend priorities based on device capability.
58 +
59 + Args:
60 + device_capability: The device's compute capability.
61 +
62 + Returns:
63 + List of backends in priority order (highest priority first).
64 + """
65 + if device_capability.major == 10: # Blackwell
66 + return [
67 + MLAPrefillBackendEnum.FLASH_ATTN,
68 + MLAPrefillBackendEnum.TRTLLM_RAGGED,
69 + MLAPrefillBackendEnum.FLASHINFER,
70 + ]
71 + else: # Hopper (SM90) and older
72 + return [
73 + MLAPrefillBackendEnum.FLASH_ATTN,
74 + ]
75 +
76 +
77 +def get_mla_prefill_backend(
78 + vllm_config: "VllmConfig",
79 +) -> "type[MLAPrefillBackend]":
80 + """Select the MLA prefill backend based on configuration and device.
81 +
82 + This function first checks for explicit user preferences via
83 + mla_prefill_backend in AttentionConfig, then falls back to automatic
84 + priority-based selection.
85 +
86 + Args:
87 + vllm_config: The vLLM configuration.
88 +
89 + Returns:
90 + The selected prefill backend class.
91 + """
92 + from vllm.platforms import current_platform
93 +
94 + device_capability = current_platform.get_device_capability()
95 + if device_capability is None:
96 + logger.info_once(
97 + "Device capability not available, using FlashAttention MLA prefill backend."
98 + )
99 + return MLAPrefillBackendEnum.FLASH_ATTN.get_class()
100 +
101 + attention_config = vllm_config.attention_config
102 +
103 + selector_config = MLAPrefillSelectorConfig(
104 + dtype=vllm_config.model_config.dtype,
105 + is_r1_compatible=is_deepseek_r1_mla_compatible(vllm_config),
106 + )
107 +
108 + if attention_config.mla_prefill_backend is not None:
109 + selected_backend = attention_config.mla_prefill_backend
110 + backend_cls: type[MLAPrefillBackend] | None = None
111 + try:
112 + backend_cls = selected_backend.get_class()
113 + invalid_reasons = backend_cls.validate_configuration(
114 + device_capability, selector_config
115 + )
116 + except ImportError:
117 + invalid_reasons = ["ImportError"]
118 + if invalid_reasons:
119 + raise ValueError(
120 + f"Selected MLA prefill backend {selected_backend.name} "
121 + f"is not valid for this configuration. "
122 + f"Reason: {invalid_reasons}"
123 + )
124 + assert backend_cls is not None
125 + logger.info("Using %s MLA prefill backend.", selected_backend.name)
126 + return backend_cls
127 +
128 + return _auto_select_mla_prefill_backend(
129 + device_capability,
130 + selector_config,
131 + )
132 +
133 +
134 +@cache
135 +def _auto_select_mla_prefill_backend(
136 + device_capability: DeviceCapability,
137 + selector_config: MLAPrefillSelectorConfig,
138 +) -> "type[MLAPrefillBackend]":
139 + """Auto-select the best available MLA prefill backend.
140 +
141 + Args:
142 + device_capability: The device's compute capability.
143 + selector_config: Hashable configuration for backend selection.
144 +
145 + Returns:
146 + The selected prefill backend class.
147 + """
148 + priorities = _get_mla_prefill_backend_priorities(device_capability)
149 + all_invalid_reasons: dict[str, list[str]] = {}
150 +
151 + for backend_enum in priorities:
152 + backend_cls: type[MLAPrefillBackend] | None = None
153 + try:
154 + backend_cls = backend_enum.get_class()
155 + invalid_reasons = backend_cls.validate_configuration(
156 + device_capability, selector_config
157 + )
158 + except ImportError:
159 + invalid_reasons = ["ImportError"]
160 + if not invalid_reasons:
161 + assert backend_cls is not None
162 + logger.info_once("Using %s MLA prefill backend.", backend_enum.name)
163 + return backend_cls
164 + all_invalid_reasons[backend_enum.name] = invalid_reasons
165 +
166 + reasons_str = (
167 + "{"
168 + + ", ".join(
169 + f"{name}: [{', '.join(reasons)}]"
170 + for name, reasons in all_invalid_reasons.items()
171 + )
172 + + "}"
173 + )
174 + config_str = repr(selector_config)
175 + logger.debug_once(
176 + "Some MLA prefill backends are not valid with %s. Reasons: %s.",
177 + config_str,
178 + reasons_str,
179 + )
180 +
181 + raise ValueError(
182 + f"No valid MLA prefill backend found with {config_str}. Reasons: {reasons_str}."
183 + )
vllm/v1/attention/backends/mla/prefill/trtllm_ragged.py new
+178
@@ -0,0 +1,178 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""TRT-LLM Ragged backend for MLA prefill."""
4 +
5 +from typing import TYPE_CHECKING
6 +
7 +import torch
8 +
9 +import vllm.envs as envs
10 +from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
11 +from vllm.v1.worker.workspace import current_workspace_manager
12 +
13 +if TYPE_CHECKING:
14 + from vllm.config import VllmConfig
15 + from vllm.model_executor.layers.attention.mla_attention import (
16 + MLACommonPrefillMetadata,
17 + )
18 + from vllm.platforms.interface import DeviceCapability
19 +
20 +
21 +class TrtllmRaggedPrefillBackend(MLAPrefillBackend):
22 + """TRT-LLM Ragged backend for MLA prefill."""
23 +
24 + requires_r1_mla_dimensions = True
25 +
26 + @staticmethod
27 + def get_name() -> str:
28 + return "TRTLLM_RAGGED"
29 +
30 + @classmethod
31 + def supports_compute_capability(cls, device_capability: "DeviceCapability") -> bool:
32 + return device_capability.major == 10
33 +
34 + @classmethod
35 + def is_available(cls) -> bool:
36 + try:
37 + from flashinfer.prefill import (
38 + trtllm_ragged_attention_deepseek, # noqa: F401
39 + )
40 +
41 + return True
42 + except ImportError:
43 + return False
44 +
45 + def __init__(
46 + self,
47 + num_heads: int,
48 + scale: float,
49 + kv_lora_rank: int,
50 + qk_nope_head_dim: int,
51 + qk_rope_head_dim: int,
52 + v_head_dim: int,
53 + vllm_config: "VllmConfig",
54 + device: torch.device,
55 + layer_names: list[str] | None = None,
56 + ) -> None:
57 + super().__init__(
58 + num_heads=num_heads,
59 + scale=scale,
60 + kv_lora_rank=kv_lora_rank,
61 + qk_nope_head_dim=qk_nope_head_dim,
62 + qk_rope_head_dim=qk_rope_head_dim,
63 + v_head_dim=v_head_dim,
64 + vllm_config=vllm_config,
65 + device=device,
66 + layer_names=layer_names,
67 + )
68 +
69 + def _get_workspace_buffer(self) -> torch.Tensor:
70 + (workspace_buffer,) = current_workspace_manager().get_simultaneous(
71 + (
72 + (envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE,),
73 + torch.uint8,
74 + ),
75 + )
76 + return workspace_buffer
77 +
78 + def prepare_metadata(
79 + self,
80 + prefill_metadata: "MLACommonPrefillMetadata",
81 + ) -> None:
82 + super().prepare_metadata(prefill_metadata)
83 + self._query_seq_lens = (
84 + prefill_metadata.query_start_loc[1:] - prefill_metadata.query_start_loc[:-1]
85 + )
86 +
87 + def run_prefill_new_tokens(
88 + self,
89 + q: torch.Tensor,
90 + k: torch.Tensor,
91 + v: torch.Tensor,
92 + return_softmax_lse: bool,
93 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
94 + from flashinfer.prefill import trtllm_ragged_attention_deepseek
95 +
96 + workspace_buffer = self._get_workspace_buffer()
97 + out = torch.empty(
98 + q.shape[0],
99 + q.shape[1],
100 + v.shape[2],
101 + device=q.device,
102 + dtype=self._prefill_metadata.output_dtype,
103 + )
104 +
105 + ret = trtllm_ragged_attention_deepseek(
106 + query=q,
107 + key=k,
108 + value=v,
109 + workspace_buffer=workspace_buffer,
110 + seq_lens=self._query_seq_lens,
111 + max_q_len=self._prefill_metadata.max_query_len,
112 + max_kv_len=self._prefill_metadata.max_query_len,
113 + bmm1_scale=self.scale,
114 + bmm2_scale=1.0,
115 + o_sf_scale=1.0,
116 + batch_size=self._query_seq_lens.shape[0],
117 + window_left=-1,
118 + cum_seq_lens_q=self._prefill_metadata.query_start_loc,
119 + cum_seq_lens_kv=self._prefill_metadata.query_start_loc,
120 + enable_pdl=False,
121 + is_causal=True,
122 + return_lse=return_softmax_lse,
123 + out=out,
124 + )
125 +
126 + if isinstance(ret, tuple):
127 + # Convert from (q_len, num_heads) to (num_heads, q_len)
128 + return ret[0], ret[1].transpose(0, 1).contiguous()
129 + return ret
130 +
131 + def run_prefill_context_chunk(
132 + self,
133 + chunk_idx: int,
134 + q: torch.Tensor,
135 + k: torch.Tensor,
136 + v: torch.Tensor,
137 + ) -> tuple[torch.Tensor, torch.Tensor]:
138 + from flashinfer.prefill import trtllm_ragged_attention_deepseek
139 +
140 + assert self._prefill_metadata.chunked_context is not None
141 + assert self._prefill_metadata.chunked_context.seq_lens[chunk_idx] is not None
142 + workspace_buffer = self._get_workspace_buffer()
143 +
144 + out = torch.empty(
145 + q.shape[0],
146 + q.shape[1],
147 + v.shape[2],
148 + device=q.device,
149 + dtype=self._prefill_metadata.output_dtype,
150 + )
151 +
152 + attn_out, lse = trtllm_ragged_attention_deepseek(
153 + query=q,
154 + key=k,
155 + value=v,
156 + workspace_buffer=workspace_buffer,
157 + seq_lens=self._prefill_metadata.chunked_context.seq_lens[chunk_idx],
158 + max_q_len=self._prefill_metadata.max_query_len,
159 + max_kv_len=self._prefill_metadata.chunked_context.max_seq_lens[chunk_idx],
160 + bmm1_scale=self.scale,
161 + bmm2_scale=1.0,
162 + o_sf_scale=1.0,
163 + batch_size=self._prefill_metadata.chunked_context.seq_lens[chunk_idx].shape[
164 + 0
165 + ],
166 + window_left=-1,
167 + cum_seq_lens_q=self._prefill_metadata.query_start_loc,
168 + cum_seq_lens_kv=self._prefill_metadata.chunked_context.cu_seq_lens[
169 + chunk_idx
170 + ],
171 + enable_pdl=False,
172 + is_causal=False,
173 + return_lse=True,
174 + out=out,
175 + )
176 +
177 + # Convert from (q_len, num_heads) to (num_heads, q_len)
178 + return attn_out, lse.transpose(0, 1).contiguous()
vllm/v1/attention/backends/mla/triton_mla.py
-12
@@ -123,18 +123,6 @@ class TritonMLAImpl(MLACommonImpl[MLACommonMetadata]):
123
124 self._sm_count = current_platform.num_compute_units()
125
126 - def _flash_attn_varlen_diff_headdims(
127 - self, q, k, v, return_softmax_lse=False, softmax_scale=None, **kwargs
128 - ):
129 - return super()._flash_attn_varlen_diff_headdims(
130 - q,
131 - k,
132 - v,
133 - return_softmax_lse=return_softmax_lse,
134 - softmax_scale=softmax_scale,
135 - **kwargs,
136 - )
137 -
126 def forward_mqa(
127 self,
128 q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],