Enable DeepSeek V4 and GLM-5.1 on SM120 (#43477)
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Yongye Zhu <zyy1102000@gmail.com>
Gabriel Wu committed
Jun 23, 2026 at 02:54 UTC
44d95069e9d6f764bea72f8a9ae6fa7f21187182
37 files changed
+2340
-469
.buildkite/intel_jobs/expert_parallelism_intel.yaml
+1
-1
@@ -1,5 +1,5 @@
1
group: Expert Parallelism
2
-depends_on:
2
+depends_on:
3
- image-build-xpu
4
steps:
5
- label: EPLB Algorithm
.buildkite/intel_jobs/models_multimodal_intel.yaml
+1
-1
@@ -1,5 +1,5 @@
1
group: Models - Multimodal
2
-depends_on:
2
+depends_on:
3
- image-build-xpu
4
steps:
5
- label: "Multi-Modal Models (Standard) 1: qwen2"
cmake/external_projects/deepgemm.cmake
+58
-28
@@ -8,43 +8,73 @@ if (DEFINED ENV{DEEPGEMM_SRC_DIR})
8
set(DEEPGEMM_SRC_DIR $ENV{DEEPGEMM_SRC_DIR})
9
endif()
10
11
+# Local tree: set deepgemm_SOURCE_DIR directly (no FetchContent download).
12
+# Upstream git: use FetchContent_Populate with explicit options (CMP0169 NEW
13
+# disallows one-argument Populate(dep) after Declare; MakeAvailable would run
14
+# DeepGEMM's top-level CMakeLists.txt, which vLLM must not load).
15
if(DEEPGEMM_SRC_DIR)
12
- FetchContent_Declare(
13
- deepgemm
14
- SOURCE_DIR ${DEEPGEMM_SRC_DIR}
15
- CONFIGURE_COMMAND ""
16
- BUILD_COMMAND ""
17
- )
16
+ # cmake_path(ABSOLUTE_PATH <var> ...) reads the path from <var>; NORMALIZE is a
17
+ # flag (no trailing path argument). Resolve relative paths against vLLM root.
18
+ set(_deepgemm_user_src "${DEEPGEMM_SRC_DIR}")
19
+ cmake_path(ABSOLUTE_PATH _deepgemm_user_src
20
+ BASE_DIRECTORY "${CMAKE_SOURCE_DIR}"
21
+ NORMALIZE)
22
+ set(DEEPGEMM_SRC_DIR "${_deepgemm_user_src}")
23
+ if(NOT IS_DIRECTORY "${DEEPGEMM_SRC_DIR}")
24
+ message(FATAL_ERROR
25
+ "DEEPGEMM_SRC_DIR is not an existing directory: '${DEEPGEMM_SRC_DIR}'")
26
+ endif()
27
+ set(deepgemm_SOURCE_DIR "${DEEPGEMM_SRC_DIR}")
28
+ message(STATUS "DeepGEMM using local DEEPGEMM_SRC_DIR: ${deepgemm_SOURCE_DIR}")
29
else()
19
- # This ref should be kept in sync with tools/install_deepgemm.sh
20
- FetchContent_Declare(
21
- deepgemm
22
- GIT_REPOSITORY https://github.com/deepseek-ai/DeepGEMM.git
23
- GIT_TAG 891d57b4db1071624b5c8fa0d1e51cb317fa709f
24
- GIT_SUBMODULES "third-party/cutlass" "third-party/fmt"
25
- GIT_PROGRESS TRUE
26
- CONFIGURE_COMMAND ""
27
- BUILD_COMMAND ""
28
- )
29
-endif()
30
+ # Keep in sync with tools/install_deepgemm.sh
31
+ set(_DEEPGEMM_UPSTREAM_REPO "https://github.com/deepseek-ai/DeepGEMM.git")
32
+ set(_DEEPGEMM_UPSTREAM_TAG "891d57b4db1071624b5c8fa0d1e51cb317fa709f")
33
31
-# Use FetchContent_Populate (not MakeAvailable) to avoid processing
32
-# DeepGEMM's own CMakeLists.txt which has incompatible find_package calls.
33
-FetchContent_GetProperties(deepgemm)
34
-if(NOT deepgemm_POPULATED)
35
- FetchContent_Populate(deepgemm)
34
+ set(_deepgemm_fc_root "${FETCHCONTENT_BASE_DIR}")
35
+ if(NOT _deepgemm_fc_root)
36
+ set(_deepgemm_fc_root "${CMAKE_BINARY_DIR}/_deps")
37
+ endif()
38
+ set(_deepgemm_src "${_deepgemm_fc_root}/deepgemm-src")
39
+ set(_deepgemm_bin "${_deepgemm_fc_root}/deepgemm-build")
40
+ set(_deepgemm_sub "${_deepgemm_fc_root}/deepgemm-subbuild")
41
+
42
+ if(EXISTS "${_deepgemm_src}/csrc/python_api.cpp")
43
+ set(deepgemm_SOURCE_DIR "${_deepgemm_src}")
44
+ set(deepgemm_BINARY_DIR "${_deepgemm_bin}")
45
+ else()
46
+ FetchContent_Populate(
47
+ deepgemm
48
+ SUBBUILD_DIR "${_deepgemm_sub}"
49
+ SOURCE_DIR "${_deepgemm_src}"
50
+ BINARY_DIR "${_deepgemm_bin}"
51
+ GIT_REPOSITORY "${_DEEPGEMM_UPSTREAM_REPO}"
52
+ GIT_TAG "${_DEEPGEMM_UPSTREAM_TAG}"
53
+ GIT_SUBMODULES "third-party/cutlass" "third-party/fmt"
54
+ GIT_PROGRESS TRUE
55
+ )
56
+ endif()
57
+ message(STATUS "DeepGEMM is available at ${deepgemm_SOURCE_DIR}")
58
endif()
37
-message(STATUS "DeepGEMM is available at ${deepgemm_SOURCE_DIR}")
59
39
-# DeepGEMM requires CUDA 12.3+ for SM90, 12.9+ for SM100
60
+# DeepGEMM requires CUDA 12.3+ for SM90, 12.9+ for SM100 (official upstream),
61
+# and 12.8+ for SM120 / SM12x. CUDA 13+ can use the family-specific SM12x
62
+# arch; CUDA 12.x builds the arch-specific SM120/SM121 variants.
63
set(DEEPGEMM_SUPPORT_ARCHS)
64
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.3)
65
list(APPEND DEEPGEMM_SUPPORT_ARCHS "9.0a")
66
endif()
44
-if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9)
45
- list(APPEND DEEPGEMM_SUPPORT_ARCHS "10.0f")
46
-elseif(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8)
47
- list(APPEND DEEPGEMM_SUPPORT_ARCHS "10.0a")
67
+if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8)
68
+ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9)
69
+ list(APPEND DEEPGEMM_SUPPORT_ARCHS "10.0f")
70
+ else()
71
+ list(APPEND DEEPGEMM_SUPPORT_ARCHS "10.0a")
72
+ endif()
73
+ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
74
+ list(APPEND DEEPGEMM_SUPPORT_ARCHS "12.0f")
75
+ else()
76
+ list(APPEND DEEPGEMM_SUPPORT_ARCHS "12.0a" "12.1a")
77
+ endif()
78
endif()
79
80
cuda_archs_loose_intersection(DEEPGEMM_ARCHS
cmake/external_projects/qutlass.cmake
+38
-16
@@ -6,25 +6,47 @@ if(DEFINED ENV{QUTLASS_SRC_DIR})
6
set(QUTLASS_SRC_DIR $ENV{QUTLASS_SRC_DIR})
7
endif()
8
9
+# CMP0169 NEW: one-argument FetchContent_Populate(name) after Declare is invalid.
10
+# Use explicit Populate(...) for git, or set SOURCE_DIR for local trees.
11
if(QUTLASS_SRC_DIR)
10
- FetchContent_Declare(
11
- qutlass
12
- SOURCE_DIR ${QUTLASS_SRC_DIR}
13
- CONFIGURE_COMMAND ""
14
- BUILD_COMMAND ""
15
- )
12
+ set(_qutlass_user_src "${QUTLASS_SRC_DIR}")
13
+ cmake_path(ABSOLUTE_PATH _qutlass_user_src
14
+ BASE_DIRECTORY "${CMAKE_SOURCE_DIR}"
15
+ NORMALIZE)
16
+ set(QUTLASS_SRC_DIR "${_qutlass_user_src}")
17
+ if(NOT IS_DIRECTORY "${QUTLASS_SRC_DIR}")
18
+ message(FATAL_ERROR
19
+ "[QUTLASS] QUTLASS_SRC_DIR is not an existing directory: '${QUTLASS_SRC_DIR}'")
20
+ endif()
21
+ set(qutlass_SOURCE_DIR "${QUTLASS_SRC_DIR}")
22
+ set(qutlass_BINARY_DIR "${CMAKE_BINARY_DIR}/qutlass-binary-dir-unused")
23
else()
17
- FetchContent_Declare(
18
- qutlass
19
- GIT_REPOSITORY https://github.com/IST-DASLab/qutlass.git
20
- GIT_TAG 830d2c4537c7396e14a02a46fbddd18b5d107c65
21
- GIT_PROGRESS TRUE
22
- CONFIGURE_COMMAND ""
23
- BUILD_COMMAND ""
24
- )
25
-endif()
24
+ set(_QUTLASS_UPSTREAM_REPO "https://github.com/IST-DASLab/qutlass.git")
25
+ set(_QUTLASS_UPSTREAM_TAG "830d2c4537c7396e14a02a46fbddd18b5d107c65")
26
27
-FetchContent_Populate(qutlass)
27
+ set(_qutlass_fc_root "${FETCHCONTENT_BASE_DIR}")
28
+ if(NOT _qutlass_fc_root)
29
+ set(_qutlass_fc_root "${CMAKE_BINARY_DIR}/_deps")
30
+ endif()
31
+ set(_qutlass_src "${_qutlass_fc_root}/qutlass-src")
32
+ set(_qutlass_bin "${_qutlass_fc_root}/qutlass-build")
33
+ set(_qutlass_sub "${_qutlass_fc_root}/qutlass-subbuild")
34
+
35
+ if(EXISTS "${_qutlass_src}/qutlass/csrc/bindings.cpp")
36
+ set(qutlass_SOURCE_DIR "${_qutlass_src}")
37
+ set(qutlass_BINARY_DIR "${_qutlass_bin}")
38
+ else()
39
+ FetchContent_Populate(
40
+ qutlass
41
+ SUBBUILD_DIR "${_qutlass_sub}"
42
+ SOURCE_DIR "${_qutlass_src}"
43
+ BINARY_DIR "${_qutlass_bin}"
44
+ GIT_REPOSITORY "${_QUTLASS_UPSTREAM_REPO}"
45
+ GIT_TAG "${_QUTLASS_UPSTREAM_TAG}"
46
+ GIT_PROGRESS TRUE
47
+ )
48
+ endif()
49
+endif()
50
51
if(NOT qutlass_SOURCE_DIR)
52
message(FATAL_ERROR "[QUTLASS] source directory could not be resolved.")
docs/design/attention_backends.md
+6
-14
@@ -133,16 +133,6 @@ Priority is **1 = highest** (tried first).
133
| 7 | `FLASHINFER_MLA_SPARSE`**\*** |
134
| 8 | `FLASHMLA_SPARSE` |
135
136
-**Ampere/Hopper (SM 8.x-9.x):**
137
-
138
-| Priority | Backend |
139
-| -------- | ------- |
140
-| 1 | `FLASH_ATTN_MLA` |
141
-| 2 | `FLASHMLA` |
142
-| 3 | `FLASHINFER_MLA` |
143
-| 4 | `TRITON_MLA` |
144
-| 5 | `FLASHMLA_SPARSE` |
145
-
136
> **\*** For sparse MLA, FP8 KV cache always prefers `FLASHINFER_MLA_SPARSE`. With BF16 KV cache, `FLASHINFER_MLA_SPARSE` is preferred for low query-head counts (<= 16), while `FLASHMLA_SPARSE` is preferred otherwise.
137
>
138
> **Note:** ROCm and CPU platforms have their own selection logic. See the platform-specific documentation for details.
@@ -231,7 +221,8 @@ MLA decode backends are selected using the standard
221
| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
222
| `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
223
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
234
-| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x |
224
+| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
225
+| `FLASHINFER_MLA_SPARSE_SM120` | bf16 | `auto`, `fp8`, `fp8_e4m3`, `fp8_ds_mla` | 64, 256 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 12.x |
226
| `FLASHMLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
227
| `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
228
| `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
@@ -248,10 +239,11 @@ DeepSeek V4 sparse MLA uses its own decode backends, selected via
239
`--attention-backend=<BACKEND>` (e.g., `FLASHMLA_SPARSE_DSV4`,
240
`FLASHINFER_MLA_SPARSE_DSV4`). They share the V4 sparse-index
241
pipeline (compressor + SWA + indexer, 256-token blocks, head 512);
251
-default on NVIDIA is `FLASHMLA_SPARSE_DSV4`.
242
+default on NVIDIA is `FLASHINFER_MLA_SPARSE_DSV4` on SM12x and
243
+`FLASHMLA_SPARSE_DSV4` on other supported CUDA architectures.
244
245
| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
246
| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
255
-| `FLASHINFER_MLA_SPARSE_DSV4` | bf16 | `auto`, `bfloat16`, `fp8` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
256
-| `FLASHMLA_SPARSE_DSV4` | bf16 | `auto`, `fp8_ds_mla`, `fp8` | 256 | 512 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
247
+| `FLASHINFER_MLA_SPARSE_DSV4` | bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_ds_mla` | 256 | 512 | ✅ | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x, 12.x |
248
+| `FLASHMLA_SPARSE_DSV4` | bf16 | `auto`, `fp8_ds_mla`, `fp8` | 256 | 512 | ✅ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
249
| `ROCM_FLASHMLA_SPARSE_DSV4` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
tests/model_executor/test_flashinfer_autotune_cache.py
deleted
-60
@@ -1,60 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-
4
-import sys
5
-from hashlib import sha256
6
-from pathlib import Path
7
-from types import SimpleNamespace
8
-
9
-from vllm.model_executor.warmup import kernel_warmup
10
-
11
-
12
-def test_resolve_flashinfer_autotune_file_default_layout(
13
- monkeypatch, tmp_path: Path
14
-) -> None:
15
- fake_jit = SimpleNamespace(
16
- env=SimpleNamespace(
17
- FLASHINFER_WORKSPACE_DIR=Path("/flashinfer-cache/0.6.11.post2/103a")
18
- )
19
- )
20
- fake_flashinfer = SimpleNamespace(jit=fake_jit)
21
- monkeypatch.setitem(sys.modules, "flashinfer", fake_flashinfer)
22
- monkeypatch.setitem(sys.modules, "flashinfer.jit", fake_jit)
23
- monkeypatch.setattr(
24
- kernel_warmup, "aot_compile_hash_factors", lambda _: ["env-hash", "config-hash"]
25
- )
26
- monkeypatch.setattr(kernel_warmup.envs, "VLLM_CACHE_ROOT", str(tmp_path))
27
- monkeypatch.setattr(kernel_warmup.envs, "VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR", None)
28
-
29
- runner = SimpleNamespace(vllm_config=SimpleNamespace())
30
- cache_hash = sha256(str(["env-hash", "config-hash"]).encode()).hexdigest()
31
-
32
- path = kernel_warmup._resolve_flashinfer_autotune_file(runner)
33
-
34
- assert path == (
35
- tmp_path
36
- / "flashinfer_autotune_cache"
37
- / "0.6.11.post2"
38
- / "103a"
39
- / cache_hash
40
- / "autotune_configs.json"
41
- )
42
- assert path.parent.is_dir()
43
-
44
-
45
-def test_resolve_flashinfer_autotune_file_uses_override_dir(
46
- monkeypatch, tmp_path: Path
47
-) -> None:
48
- monkeypatch.setattr(
49
- kernel_warmup.envs, "VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR", str(tmp_path)
50
- )
51
- monkeypatch.setattr(
52
- kernel_warmup, "aot_compile_hash_factors", lambda _: ["env-hash", "config-hash"]
53
- )
54
-
55
- runner = SimpleNamespace(vllm_config=SimpleNamespace())
56
- cache_hash = sha256(str(["env-hash", "config-hash"]).encode()).hexdigest()
57
-
58
- path = kernel_warmup._resolve_flashinfer_autotune_file(runner)
59
-
60
- assert path == tmp_path / cache_hash / "autotune_configs.json"
tests/v1/attention/test_flashinfer_sparse_mla_sm120_api.py
new
+54
@@ -0,0 +1,54 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Behavior checks for FlashInfer SM120 sparse MLA backend selection."""
4
+
5
+from types import SimpleNamespace
6
+
7
+import torch
8
+
9
+from vllm.config import set_current_vllm_config
10
+from vllm.platforms.interface import DeviceCapability
11
+from vllm.utils import flashinfer as fi_utils
12
+from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
13
+ FlashInferMLASparseSM120Backend,
14
+)
15
+from vllm.v1.attention.backends.registry import AttentionBackendEnum
16
+
17
+
18
+def _fake_vllm_config(model_type: str) -> SimpleNamespace:
19
+ return SimpleNamespace(
20
+ model_config=SimpleNamespace(
21
+ hf_text_config=SimpleNamespace(model_type=model_type, index_topk=2048),
22
+ ),
23
+ )
24
+
25
+
26
+def test_sm120_backend_uses_dedicated_backend_name() -> None:
27
+ assert FlashInferMLASparseSM120Backend.get_name() == "FLASHINFER_MLA_SPARSE_SM120"
28
+ assert (
29
+ AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120.get_class()
30
+ is FlashInferMLASparseSM120Backend
31
+ )
32
+
33
+
34
+def test_v32_glm_sm120_backend_accepts_glm_block_size(
35
+ monkeypatch,
36
+) -> None:
37
+ monkeypatch.setattr(fi_utils, "has_flashinfer_sparse_mla_sm120", lambda: True)
38
+
39
+ with set_current_vllm_config(_fake_vllm_config("glm4_moe")):
40
+ invalid_reasons = FlashInferMLASparseSM120Backend.validate_configuration(
41
+ head_size=576,
42
+ dtype=torch.bfloat16,
43
+ kv_cache_dtype="fp8",
44
+ block_size=256,
45
+ use_mla=True,
46
+ has_sink=False,
47
+ use_sparse=True,
48
+ use_mm_prefix=False,
49
+ use_per_head_quant_scales=False,
50
+ device_capability=DeviceCapability(12, 0),
51
+ attn_type="decoder",
52
+ )
53
+
54
+ assert invalid_reasons == []
tests/v1/attention/test_sparse_mla_backends.py
+9
-6
@@ -36,7 +36,7 @@ if not current_platform.is_cuda():
36
37
from vllm.utils.math_utils import cdiv
38
from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
39
- FlashInferMLASparseBackend,
39
+ FlashInferMLASparseTRTLLMBackend,
40
)
41
from vllm.v1.attention.backends.mla.flashmla_sparse import (
42
FlashMLASparseBackend,
@@ -174,8 +174,8 @@ def _quantize_dequantize_fp8_ds_mla(
174
175
@pytest.mark.parametrize(
176
"backend_cls",
177
- [FlashMLASparseBackend, FlashInferMLASparseBackend],
178
- ids=["FlashMLA", "FlashInfer"],
177
+ [FlashMLASparseBackend, FlashInferMLASparseTRTLLMBackend],
178
+ ids=["FlashMLA", "FlashInferTRTLLM"],
179
)
180
@pytest.mark.parametrize("batch_name", list(SPARSE_BACKEND_BATCH_SPECS.keys()))
181
@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_ds_mla"])
@@ -217,9 +217,12 @@ def test_sparse_backend_decode_correctness(
217
ok, reason = flashmla.is_flashmla_sparse_supported()
218
if not ok:
219
pytest.skip(reason)
220
- elif backend_cls == FlashInferMLASparseBackend:
221
- if not current_platform.has_device_capability(100):
222
- pytest.skip("FlashInferMLASparseBackend requires SM 10.0 or higher")
220
+ elif backend_cls == FlashInferMLASparseTRTLLMBackend:
221
+ device_capability = current_platform.get_device_capability()
222
+ if device_capability is None or not backend_cls.supports_compute_capability(
223
+ device_capability
224
+ ):
225
+ pytest.skip("FlashInferMLASparseTRTLLMBackend requires SM 10.x capability")
226
227
batch_spec = SPARSE_BACKEND_BATCH_SPECS[batch_name]
228
use_fp8_ds_mla_quantization = kv_cache_dtype == "fp8_ds_mla"
tests/v1/spec_decode/test_acceptance_length.py
+3
-3
@@ -132,9 +132,9 @@ def get_available_attention_backends() -> list[str]:
132
)
133
134
return [
135
- backend.name
136
- for backend, _ in valid_backends
137
- if backend not in EXCLUDED_BACKENDS
135
+ candidate.backend.name
136
+ for candidate in valid_backends
137
+ if candidate.backend not in EXCLUDED_BACKENDS
138
]
139
140
tools/pre_commit/generate_attention_backend_docs.py
+5
-2
@@ -690,7 +690,9 @@ def parse_compute_capability(node: ast.ClassDef) -> str:
690
major_list.sort()
691
if len(major_list) == 1:
692
return f"{major_list[0]}.x"
693
- return f"{major_list[0]}.x-{major_list[-1]}.x"
693
+ if major_list == list(range(major_list[0], major_list[-1] + 1)):
694
+ return f"{major_list[0]}.x-{major_list[-1]}.x"
695
+ return ", ".join(f"{major}.x" for major in major_list)
696
697
if min_cap:
698
if max_cap:
@@ -1668,7 +1670,8 @@ def generate_mla_section(
1670
"`--attention-backend=<BACKEND>` (e.g., `FLASHMLA_SPARSE_DSV4`,",
1671
"`FLASHINFER_MLA_SPARSE_DSV4`). They share the V4 sparse-index",
1672
"pipeline (compressor + SWA + indexer, 256-token blocks, head 512);",
1671
- "default on NVIDIA is `FLASHMLA_SPARSE_DSV4`.",
1673
+ "default on NVIDIA is `FLASHINFER_MLA_SPARSE_DSV4` on SM12x and",
1674
+ "`FLASHMLA_SPARSE_DSV4` on other supported CUDA architectures.",
1675
"",
1676
]
1677
)
vllm/model_executor/layers/attention/mla_attention.py
+29
-14
@@ -208,6 +208,7 @@ from vllm.config import (
208
get_current_vllm_config,
209
get_current_vllm_config_or_none,
210
)
211
+from vllm.config.cache import CacheDType
212
from vllm.distributed.parallel_state import (
213
get_dcp_group,
214
is_global_first_rank,
@@ -319,6 +320,22 @@ def _detect_output_quant_key(
320
return kFp8StaticTensorSym
321
322
323
+def _canonicalize_sparse_mla_kv_cache_dtype(
324
+ attn_backend: type[AttentionBackend],
325
+ kv_cache_dtype: CacheDType,
326
+) -> CacheDType:
327
+ backend_name = attn_backend.get_name()
328
+ if backend_name == "FLASHMLA_SPARSE" and is_quantized_kv_cache(kv_cache_dtype):
329
+ return "fp8_ds_mla"
330
+ if backend_name == "FLASHINFER_MLA_SPARSE_SM120" and kv_cache_dtype in (
331
+ "auto",
332
+ "fp8",
333
+ "fp8_e4m3",
334
+ ):
335
+ return "fp8_ds_mla"
336
+ return kv_cache_dtype
337
+
338
+
339
class MLAAttention(nn.Module, AttentionLayerBase):
340
"""Multi-Head Latent Attention layer.
341
@@ -369,7 +386,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
386
self.qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
387
388
if cache_config is not None:
372
- kv_cache_dtype = cache_config.cache_dtype
389
+ kv_cache_dtype: CacheDType = cache_config.cache_dtype
390
calculate_kv_scales = cache_config.calculate_kv_scales
391
else:
392
kv_cache_dtype = "auto"
@@ -393,24 +410,22 @@ class MLAAttention(nn.Module, AttentionLayerBase):
410
num_heads=self.num_heads,
411
)
412
396
- # FlashMLA Sparse Attention fp8 backend uses "fp8_ds_mla" kv-cache format
397
- # Automatically convert fp8 kv-cache format to "fp8_ds_mla"
398
- if (
399
- self.attn_backend.get_name() == "FLASHMLA_SPARSE"
400
- and is_quantized_kv_cache(kv_cache_dtype)
401
- and kv_cache_dtype != "fp8_ds_mla"
402
- ):
403
- assert cache_config is not None
404
- cache_config.cache_dtype = "fp8_ds_mla"
405
- kv_cache_dtype = "fp8_ds_mla"
413
+ normalized_kv_cache_dtype = _canonicalize_sparse_mla_kv_cache_dtype(
414
+ self.attn_backend, kv_cache_dtype
415
+ )
416
+ if normalized_kv_cache_dtype != kv_cache_dtype:
417
+ if cache_config is not None:
418
+ cache_config.cache_dtype = normalized_kv_cache_dtype
419
+ kv_cache_dtype = normalized_kv_cache_dtype
420
logger.info_once(
407
- "Using DeepSeek's fp8_ds_mla KV cache format. To use standard "
408
- "fp8 kv-cache format, please set `--attention-backend "
409
- "FLASHINFER_MLA_SPARSE`"
421
+ "Using %s KV cache format for %s backend.",
422
+ kv_cache_dtype,
423
+ self.attn_backend.get_name(),
424
)
425
426
if (
427
self.attn_backend.get_name() == "FLASHINFER_MLA_SPARSE"
428
+ and kv_cache_dtype != "fp8_ds_mla"
429
and is_quantized_kv_cache(kv_cache_dtype)
430
):
431
logger.info_once(
vllm/model_executor/layers/fused_moe/deep_gemm_utils.py
+76
-18
@@ -23,24 +23,82 @@ def expert_num_tokens_round_up_and_sum(
23
return torch.sum(ent).item()
24
25
26
-def compute_aligned_M(
26
+def compute_aligned_M_and_alignment(
27
M: int,
28
num_topk: int,
29
local_num_experts: int,
30
alignment: int,
31
expert_tokens_meta: mk.ExpertTokensMetadata | None,
32
-):
32
+) -> tuple[int, int]:
33
+ """Return (M_sum, alignment_used).
34
+
35
+ `alignment_used` may be smaller than the caller-supplied `alignment` on
36
+ SM100/SM120 when DeepGEMM can JIT a smaller BLOCK_M for the per-call
37
+ expected_m. Callers that index by block size (e.g. ``M_sum // block_m``)
38
+ or assert workspace alignment must use the returned `alignment_used`,
39
+ not their original `alignment` argument.
40
+
41
+ Prefer this over the int-returning :func:`compute_aligned_M` when the
42
+ GEMM call site needs to wrap itself in ``mk_alignment_scope`` or
43
+ otherwise reason about the actual per-expert padding.
44
+ """
45
if (expert_tokens_meta is not None) and (
46
expert_tokens_meta.expert_num_tokens_cpu is not None
47
):
36
- return expert_num_tokens_round_up_and_sum(
37
- expert_tokens_meta.expert_num_tokens_cpu, alignment=alignment
48
+ return (
49
+ expert_num_tokens_round_up_and_sum(
50
+ expert_tokens_meta.expert_num_tokens_cpu, alignment=alignment
51
+ ),
52
+ alignment,
53
+ )
54
+
55
+ # expert_num_tokens not on cpu. Cap padding by min(M*num_topk,
56
+ # local_num_experts) — at batch=1 decode only `num_topk` experts can be
57
+ # active, so the worst-case `local_num_experts*(align-1)` is too loose.
58
+ # Also shrink `alignment` to DeepGEMM's per-call theoretical BLOCK_M on
59
+ # SM100/SM120 when smaller.
60
+ expected_m = M * num_topk
61
+ try:
62
+ from vllm.utils.deep_gemm import (
63
+ get_theoretical_mk_alignment_for_contiguous_layout,
64
+ )
65
+
66
+ # num_groups=local_num_experts so the helper recovers per-expert em;
67
+ # omitting it over-picks BLOCK_M on SM120 (heuristic assumes em is
68
+ # already per-expert).
69
+ per_call_align = get_theoretical_mk_alignment_for_contiguous_layout(
70
+ expected_m=expected_m,
71
+ num_groups=local_num_experts,
72
)
73
+ if per_call_align and per_call_align <= alignment:
74
+ alignment = per_call_align
75
+ except Exception:
76
+ pass
77
40
- # expert_num_tokens information is not available on the cpu.
41
- # compute the max required size.
42
- M_sum = (M * num_topk) + local_num_experts * (alignment - 1)
78
+ max_active_experts = min(M * num_topk, local_num_experts)
79
+ M_sum = (M * num_topk) + max_active_experts * (alignment - 1)
80
M_sum = round_up(M_sum, alignment)
81
+ return M_sum, alignment
82
+
83
+
84
+def compute_aligned_M(
85
+ M: int,
86
+ num_topk: int,
87
+ local_num_experts: int,
88
+ alignment: int,
89
+ expert_tokens_meta: mk.ExpertTokensMetadata | None,
90
+) -> int:
91
+ """Return ``M_sum`` only (backward-compat wrapper).
92
+
93
+ Equivalent to :func:`compute_aligned_M_and_alignment`'s first return
94
+ value. Existing downstream callers and the warmup path that only size
95
+ a workspace use this. Call sites that need the actual per-expert
96
+ alignment (to wrap GEMMs in ``mk_alignment_scope``) should use
97
+ :func:`compute_aligned_M_and_alignment` instead.
98
+ """
99
+ M_sum, _ = compute_aligned_M_and_alignment(
100
+ M, num_topk, local_num_experts, alignment, expert_tokens_meta
101
+ )
102
return M_sum
103
104
@@ -51,12 +109,6 @@ def apply_expert_map(expert_id, expert_map):
109
return expert_id
110
111
54
-@triton.jit
55
-def round_up_128(x: int) -> int:
56
- y = 128
57
- return ((x + y - 1) // y) * y
58
-
59
-
112
@triton.jit
113
def _fwd_kernel_ep_scatter_1(
114
num_recv_tokens_per_expert,
@@ -65,6 +117,7 @@ def _fwd_kernel_ep_scatter_1(
117
num_experts: tl.constexpr,
118
BLOCK_E: tl.constexpr,
119
BLOCK_EXPERT_NUM: tl.constexpr,
120
+ ALIGN_M: tl.constexpr,
121
):
122
cur_expert = tl.program_id(0)
123
@@ -74,7 +127,8 @@ def _fwd_kernel_ep_scatter_1(
127
mask=offset_cumsum < num_experts,
128
other=0,
129
)
77
- tokens_per_expert = round_up_128(tokens_per_expert)
130
+ # Round up to ALIGN_M so cumsum matches the workspace's per-expert slices.
131
+ tokens_per_expert = ((tokens_per_expert + ALIGN_M - 1) // ALIGN_M) * ALIGN_M
132
cumsum = tl.cumsum(tokens_per_expert) - tokens_per_expert
133
134
# Extract this block's offset from the register vector (warp shuffle,
@@ -227,10 +281,12 @@ def ep_scatter(
281
output_tensor_scale: torch.Tensor,
282
m_indices: torch.Tensor,
283
output_index: torch.Tensor,
284
+ align_m: int = 128,
285
block_size: int = 128,
286
pack_ue8m0: bool = False,
287
):
233
- BLOCK_E = 128 # token num of per expert is aligned to 128
288
+ # BLOCK_E is the m_indices fill-loop tile (masked), independent of align_m.
289
+ BLOCK_E = 128
290
BLOCK_D = block_size # block size of activation-scale quantization
291
num_warps = 8
292
num_experts = num_recv_tokens_per_expert.shape[0]
@@ -238,7 +294,7 @@ def ep_scatter(
294
# grid = (triton.cdiv(hidden_size, BLOCK_D), num_experts)
295
grid = num_experts
296
241
- assert m_indices.shape[0] % BLOCK_E == 0
297
+ assert m_indices.shape[0] % align_m == 0
298
assert expert_start_loc.shape[0] == num_experts
299
300
# pack_ue8m0: scatter packs 4 UE8M0 bytes per int32; else copies scales as-is.
@@ -253,6 +309,7 @@ def ep_scatter(
309
num_warps=num_warps,
310
BLOCK_E=BLOCK_E,
311
BLOCK_EXPERT_NUM=triton.next_power_of_2(num_experts),
312
+ ALIGN_M=align_m,
313
)
314
315
grid = min(recv_topk.shape[0], 1024 * 8)
@@ -418,7 +475,7 @@ def deepgemm_moe_permute(
475
if block_size is not None:
476
block_k = block_size
477
421
- M_sum = compute_aligned_M(
478
+ M_sum, align_used = compute_aligned_M_and_alignment(
479
M=topk_ids.size(0),
480
num_topk=topk_ids.size(1),
481
local_num_experts=local_num_experts,
@@ -482,11 +539,12 @@ def deepgemm_moe_permute(
539
output_tensor_scale=aq_scale_out,
540
m_indices=expert_ids,
541
output_index=inv_perm,
542
+ align_m=align_used,
543
block_size=block_k,
544
pack_ue8m0=pack_ue8m0,
545
)
546
489
- return aq_out, aq_scale_out, expert_ids, inv_perm
547
+ return aq_out, aq_scale_out, expert_ids, inv_perm, align_used
548
549
550
def deepgemm_unpermute_and_reduce(
vllm/model_executor/layers/fused_moe/experts/batched_deep_gemm_moe.py
+5
-4
@@ -318,11 +318,12 @@ class BatchedDeepGemmExperts(mk.FusedMoEExpertsModular):
318
319
def supports_packed_ue8m0_act_scales(self) -> bool:
320
"""
321
- DeepGemm supports packed ue8m0 activation scales format in devices == sm100
321
+ DeepGemm supports packed ue8m0 activation scales on Blackwell-family
322
+ GPUs (SM100 datacenter and SM120 consumer).
323
"""
323
- return (
324
- is_deep_gemm_e8m0_used()
325
- and current_platform.is_device_capability_family(100)
324
+ return is_deep_gemm_e8m0_used() and (
325
+ current_platform.is_device_capability_family(100)
326
+ or current_platform.is_device_capability_family(120)
327
)
328
329
def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
vllm/model_executor/layers/fused_moe/experts/deep_gemm_moe.py
+74
-57
@@ -12,7 +12,7 @@ from vllm.model_executor.layers.fused_moe.config import (
12
FusedMoEQuantConfig,
13
)
14
from vllm.model_executor.layers.fused_moe.deep_gemm_utils import (
15
- compute_aligned_M,
15
+ compute_aligned_M_and_alignment,
16
deepgemm_moe_permute,
17
deepgemm_unpermute_and_reduce,
18
)
@@ -43,6 +43,7 @@ from vllm.utils.deep_gemm import (
43
is_deep_gemm_supported,
44
m_grouped_fp8_fp4_gemm_nt_contiguous,
45
m_grouped_fp8_gemm_nt_contiguous,
46
+ mk_alignment_scope,
47
)
48
from vllm.utils.import_utils import has_deep_gemm
49
@@ -210,10 +211,10 @@ class DeepGemmExperts(mk.FusedMoEExpertsModular):
211
# Use the contiguous-layout M alignment (matches apply()); block_shape[0]
212
# is the quant block (1 for MXFP8) and would under-size the workspace.
213
block_m = get_mk_alignment_for_contiguous_layout()[0]
213
- M_sum = compute_aligned_M(
214
+ M_sum, align_used = compute_aligned_M_and_alignment(
215
M, topk, local_num_experts, block_m, expert_tokens_meta
216
)
216
- assert M_sum % block_m == 0
217
+ assert M_sum % align_used == 0
218
219
activation_out_dim = self.adjust_N_for_activation(N, activation)
220
workspace1 = (M_sum, max(activation_out_dim, K))
@@ -316,7 +317,7 @@ class DeepGemmExperts(mk.FusedMoEExpertsModular):
317
318
assert w2.size(1) == K
319
319
- M_sum = compute_aligned_M(
320
+ M_sum, _ = compute_aligned_M_and_alignment(
321
M=topk_ids.size(0),
322
num_topk=topk_ids.size(1),
323
local_num_experts=local_num_experts,
@@ -327,7 +328,7 @@ class DeepGemmExperts(mk.FusedMoEExpertsModular):
328
a1q_perm = _resize_cache(
329
workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, K)
330
)
330
- a1q, a1q_scale, expert_ids, inv_perm = deepgemm_moe_permute(
331
+ a1q, a1q_scale, expert_ids, inv_perm, align_used = deepgemm_moe_permute(
332
aq=a1q,
333
aq_scale=a1q_scale,
334
topk_ids=topk_ids,
@@ -349,23 +350,35 @@ class DeepGemmExperts(mk.FusedMoEExpertsModular):
350
else {}
351
)
352
352
- mm1_out = _resize_cache(workspace2, (M_sum, N))
353
- m_grouped_fp8_gemm_nt_contiguous(
354
- (a1q, a1q_scale), (w1, self.w1_scale), mm1_out, expert_ids, **gemm_kwargs
355
- )
353
+ # Cap DG's BLOCK_M heuristic at the workspace's per-expert alignment;
354
+ # otherwise the scheduler can pick the wrong expert id from m_indices
355
+ # under cudagraph replay.
356
+ with mk_alignment_scope(align_used):
357
+ mm1_out = _resize_cache(workspace2, (M_sum, N))
358
+ m_grouped_fp8_gemm_nt_contiguous(
359
+ (a1q, a1q_scale),
360
+ (w1, self.w1_scale),
361
+ mm1_out,
362
+ expert_ids,
363
+ **gemm_kwargs,
364
+ )
365
357
- activation_out_dim = self.adjust_N_for_activation(N, activation)
358
- quant_out = _resize_cache(
359
- workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, activation_out_dim)
360
- )
361
- a2q, a2q_scale = self._act_mul_quant(
362
- input=mm1_out.view(-1, N), output=quant_out, activation=activation
363
- )
366
+ activation_out_dim = self.adjust_N_for_activation(N, activation)
367
+ quant_out = _resize_cache(
368
+ workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, activation_out_dim)
369
+ )
370
+ a2q, a2q_scale = self._act_mul_quant(
371
+ input=mm1_out.view(-1, N), output=quant_out, activation=activation
372
+ )
373
365
- mm2_out = _resize_cache(workspace2, (M_sum, K))
366
- m_grouped_fp8_gemm_nt_contiguous(
367
- (a2q, a2q_scale), (w2, self.w2_scale), mm2_out, expert_ids, **gemm_kwargs
368
- )
374
+ mm2_out = _resize_cache(workspace2, (M_sum, K))
375
+ m_grouped_fp8_gemm_nt_contiguous(
376
+ (a2q, a2q_scale),
377
+ (w2, self.w2_scale),
378
+ mm2_out,
379
+ expert_ids,
380
+ **gemm_kwargs,
381
+ )
382
383
if apply_router_weight_on_input:
384
topk_weights = torch.ones_like(topk_weights)
@@ -384,7 +397,8 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
397
"""DeepGemm-based fused MoE expert implementation for FP4 weights.
398
399
Uses m_grouped_fp8_fp4_gemm_nt_contiguous with FP8 activations and
387
- MXFP4 (FP4 E2M1 packed as uint8) weights. Requires SM100+ (Blackwell).
400
+ MXFP4 (FP4 E2M1 packed as uint8) weights. Requires Blackwell-family
401
+ GPUs (SM100 datacenter or SM120 consumer).
402
"""
403
404
# FP8 activation block size (hardcoded since mxfp4_w4a8 quant config
@@ -409,9 +423,9 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
423
def _supports_current_device() -> bool:
424
from vllm.platforms import current_platform
425
412
- return (
413
- is_deep_gemm_supported()
414
- and current_platform.is_device_capability_family(100)
426
+ return is_deep_gemm_supported() and (
427
+ current_platform.is_device_capability_family(100)
428
+ or current_platform.is_device_capability_family(120)
429
)
430
431
@staticmethod
@@ -454,10 +468,10 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
468
activation: MoEActivation,
469
) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
470
block_m = get_mk_alignment_for_contiguous_layout()[0]
457
- M_sum = compute_aligned_M(
471
+ M_sum, align_used = compute_aligned_M_and_alignment(
472
M, topk, local_num_experts, block_m, expert_tokens_meta
473
)
460
- assert M_sum % block_m == 0
474
+ assert M_sum % align_used == 0
475
476
activation_out_dim = self.adjust_N_for_activation(N, activation)
477
workspace1 = (M_sum, max(activation_out_dim, K))
@@ -533,7 +547,7 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
547
if global_num_experts == -1:
548
global_num_experts = local_num_experts
549
536
- M_sum = compute_aligned_M(
550
+ M_sum, _ = compute_aligned_M_and_alignment(
551
M=topk_ids.size(0),
552
num_topk=topk_ids.size(1),
553
local_num_experts=local_num_experts,
@@ -544,7 +558,7 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
558
a1q_perm = _resize_cache(
559
workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, K)
560
)
547
- a1q, a1q_scale, expert_ids, inv_perm = deepgemm_moe_permute(
561
+ a1q, a1q_scale, expert_ids, inv_perm, align_used = deepgemm_moe_permute(
562
aq=a1q,
563
aq_scale=a1q_scale,
564
topk_ids=topk_ids,
@@ -555,37 +569,40 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
569
)
570
assert a1q.size(0) == M_sum
571
558
- # FC1: FP8 activations x FP4 weights
559
- # DeepGEMM 2.4.2 requires FP4-packed weights as int8 (kPackedFP4).
560
- mm1_out = _resize_cache(workspace2, (M_sum, N))
561
- m_grouped_fp8_fp4_gemm_nt_contiguous(
562
- (a1q, a1q_scale),
563
- (w1.view(torch.int8), self.w1_scale),
564
- mm1_out,
565
- expert_ids,
566
- recipe_a=(1, self._ACT_BLOCK_K),
567
- recipe_b=(1, self._WEIGHT_BLOCK_K),
568
- )
572
+ # Cap DG's BLOCK_M heuristic at the workspace's per-expert alignment;
573
+ # see DeepGemmExperts.apply for rationale.
574
+ with mk_alignment_scope(align_used):
575
+ # FC1: FP8 activations x FP4 weights
576
+ # DeepGEMM 2.4.2 requires FP4-packed weights as int8 (kPackedFP4).
577
+ mm1_out = _resize_cache(workspace2, (M_sum, N))
578
+ m_grouped_fp8_fp4_gemm_nt_contiguous(
579
+ (a1q, a1q_scale),
580
+ (w1.view(torch.int8), self.w1_scale),
581
+ mm1_out,
582
+ expert_ids,
583
+ recipe_a=(1, self._ACT_BLOCK_K),
584
+ recipe_b=(1, self._WEIGHT_BLOCK_K),
585
+ )
586
570
- # SwiGLU activation + FP8 requant
571
- activation_out_dim = self.adjust_N_for_activation(N, activation)
572
- quant_out = _resize_cache(
573
- workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, activation_out_dim)
574
- )
575
- a2q, a2q_scale = self._act_mul_quant(
576
- input=mm1_out.view(-1, N), output=quant_out, activation=activation
577
- )
587
+ # SwiGLU activation + FP8 requant
588
+ activation_out_dim = self.adjust_N_for_activation(N, activation)
589
+ quant_out = _resize_cache(
590
+ workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, activation_out_dim)
591
+ )
592
+ a2q, a2q_scale = self._act_mul_quant(
593
+ input=mm1_out.view(-1, N), output=quant_out, activation=activation
594
+ )
595
579
- # FC2: FP8 activations x FP4 weights
580
- mm2_out = _resize_cache(workspace2, (M_sum, K))
581
- m_grouped_fp8_fp4_gemm_nt_contiguous(
582
- (a2q, a2q_scale),
583
- (w2.view(torch.int8), self.w2_scale),
584
- mm2_out,
585
- expert_ids,
586
- recipe_a=(1, self._ACT_BLOCK_K),
587
- recipe_b=(1, self._WEIGHT_BLOCK_K),
588
- )
596
+ # FC2: FP8 activations x FP4 weights
597
+ mm2_out = _resize_cache(workspace2, (M_sum, K))
598
+ m_grouped_fp8_fp4_gemm_nt_contiguous(
599
+ (a2q, a2q_scale),
600
+ (w2.view(torch.int8), self.w2_scale),
601
+ mm2_out,
602
+ expert_ids,
603
+ recipe_a=(1, self._ACT_BLOCK_K),
604
+ recipe_b=(1, self._WEIGHT_BLOCK_K),
605
+ )
606
607
if apply_router_weight_on_input:
608
topk_weights = torch.ones_like(topk_weights)
vllm/model_executor/layers/fused_moe/oracle/mxfp4.py
+48
-10
@@ -57,6 +57,40 @@ if has_triton_kernels():
57
)
58
59
60
+def _pack_deepgemm_mxfp4_scales(
61
+ w13_weight: torch.Tensor,
62
+ w2_weight: torch.Tensor,
63
+ w13_weight_scale: torch.Tensor,
64
+ w2_weight_scale: torch.Tensor,
65
+) -> tuple[torch.Tensor, torch.Tensor]:
66
+ from vllm.model_executor.layers.quantization.utils.fp8_utils import (
67
+ deepgemm_post_process_weight_scale_block,
68
+ )
69
+
70
+ num_experts = w13_weight.shape[0]
71
+ intermediate_size_2 = w13_weight.shape[1] # = intermediate*2
72
+ hidden_size = w13_weight.shape[2] * 2 # weight is FP4-packed
73
+ intermediate_size = w2_weight.shape[2] * 2 # weight is FP4-packed
74
+ block_shape = (1, 32) # MXFP4 block (per-row, K=32)
75
+
76
+ return (
77
+ deepgemm_post_process_weight_scale_block(
78
+ ws=w13_weight_scale.data,
79
+ mn=intermediate_size_2,
80
+ k=hidden_size,
81
+ quant_block_shape=block_shape,
82
+ num_groups=num_experts,
83
+ ),
84
+ deepgemm_post_process_weight_scale_block(
85
+ ws=w2_weight_scale.data,
86
+ mn=hidden_size,
87
+ k=intermediate_size,
88
+ quant_block_shape=block_shape,
89
+ num_groups=num_experts,
90
+ ),
91
+ )
92
+
93
+
94
class Mxfp4MoeBackend(Enum):
95
NONE = "None"
96
# DeepGEMM FP8xFP4 backend (SM100+)
@@ -652,15 +686,18 @@ def convert_gpt_oss_weight_to_mxfp4_moe_kernel_format(
686
"""Convert loaded weights into backend-specific kernel format."""
687
688
if mxfp4_backend == Mxfp4MoeBackend.DEEPGEMM_MXFP4:
655
- from vllm.model_executor.layers.quantization.utils.fp8_utils import (
656
- _upcast_e8m0_to_fp32,
689
+ w13_weight_scale, w2_weight_scale = _pack_deepgemm_mxfp4_scales(
690
+ w13_weight,
691
+ w2_weight,
692
+ w13_weight_scale,
693
+ w2_weight_scale,
694
)
695
696
return (
697
w13_weight.data,
698
w2_weight.data,
662
- _upcast_e8m0_to_fp32(w13_weight_scale.data),
663
- _upcast_e8m0_to_fp32(w2_weight_scale.data),
699
+ w13_weight_scale,
700
+ w2_weight_scale,
701
w13_bias,
702
w2_bias,
703
)
@@ -1195,17 +1232,18 @@ def convert_weight_to_mxfp4_moe_kernel_format(
1232
"""
1233
1234
if mxfp4_backend == Mxfp4MoeBackend.DEEPGEMM_MXFP4:
1198
- from vllm.model_executor.layers.quantization.utils.fp8_utils import (
1199
- _upcast_e8m0_to_fp32,
1235
+ w13_weight_scale, w2_weight_scale = _pack_deepgemm_mxfp4_scales(
1236
+ w13_weight,
1237
+ w2_weight,
1238
+ w13_weight_scale,
1239
+ w2_weight_scale,
1240
)
1241
1202
- # Weights stay as uint8 packed FP4 — no layout change needed.
1203
- # Convert E8M0 uint8 scales to float32.
1242
return (
1243
w13_weight.data,
1244
w2_weight.data,
1207
- _upcast_e8m0_to_fp32(w13_weight_scale.data),
1208
- _upcast_e8m0_to_fp32(w2_weight_scale.data),
1245
+ w13_weight_scale,
1246
+ w2_weight_scale,
1247
w13_bias,
1248
w2_bias,
1249
)
vllm/model_executor/layers/quantization/utils/fp8_utils.py
+36
-22
@@ -1058,6 +1058,34 @@ def _upcast_e8m0_to_fp32(scale: torch.Tensor) -> torch.Tensor:
1058
return fp32_bits.view(torch.float32)
1059
1060
1061
+def deepgemm_post_process_weight_scale_block(
1062
+ ws: torch.Tensor,
1063
+ mn: int,
1064
+ k: int,
1065
+ quant_block_shape: tuple[int, ...],
1066
+ num_groups: int,
1067
+ is_sfa: bool = False,
1068
+) -> torch.Tensor:
1069
+ if ws.dtype in (torch.float8_e8m0fnu, torch.uint8):
1070
+ # Scales already in E8M0 from checkpoint; upcast to fp32 and let
1071
+ # DeepGEMM pack the layout expected by the target architecture.
1072
+ ws = _upcast_e8m0_to_fp32(ws)
1073
+ else:
1074
+ assert ws.dtype == torch.float32, (
1075
+ f"Expected tensor scales dtype to be torch.float32 or "
1076
+ f"torch.float8_e8m0fnu or torch.uint8, got {ws.dtype} instead"
1077
+ )
1078
+
1079
+ return transform_sf_into_required_layout(
1080
+ sf=ws,
1081
+ mn=mn,
1082
+ k=k,
1083
+ recipe=(1, quant_block_shape[0], quant_block_shape[1]),
1084
+ num_groups=num_groups,
1085
+ is_sfa=is_sfa,
1086
+ )
1087
+
1088
+
1089
def deepgemm_post_process_fp8_weight_block(
1090
wq: torch.Tensor,
1091
ws: torch.Tensor,
@@ -1073,13 +1101,13 @@ def deepgemm_post_process_fp8_weight_block(
1101
1102
if ws.dtype in (torch.float8_e8m0fnu, torch.uint8):
1103
# Scales already in E8M0 from checkpoint (float8_e8m0fnu, or raw E8M0
1076
- # bits as uint8 for MXFP8) — upcast to fp32 and skip requantization
1104
+ # bits as uint8 for MXFP8) - upcast to fp32 and skip requantization
1105
# (weights already have power-of-two scales).
1106
ws = _upcast_e8m0_to_fp32(ws)
1107
else:
1108
assert ws.dtype == torch.float32, (
1109
f"Expected tensor scales dtype to be torch.float32 or "
1082
- f"torch.float8_e8m0fnu, got {ws.dtype} instead"
1110
+ f"torch.float8_e8m0fnu or torch.uint8, got {ws.dtype} instead"
1111
)
1112
if use_e8m0:
1113
requant_weight_ue8m0_inplace(wq, ws, block_size=quant_block_shape)
@@ -1094,16 +1122,12 @@ def deepgemm_post_process_fp8_weight_block(
1122
r = wq.size(0) // g
1123
wq = wq.view(g, r, d)
1124
ws = ws.view(g, r // quant_block_shape[0], d // quant_block_shape[1])
1097
- # Pre-transform scale with recipe=(1, 128, 128) to broadcast + pack
1098
- # into TMA-aligned UE8M0 (INT32) layout. At runtime fp8_einsum uses
1099
- # recipe=(1, 1, 128) which sees INT dtype and skips re-transform.
1100
- dg_ws = transform_sf_into_required_layout(
1101
- sf=ws,
1125
+ dg_ws = deepgemm_post_process_weight_scale_block(
1126
+ ws=ws,
1127
mn=r,
1128
k=d,
1104
- recipe=(1, quant_block_shape[0], quant_block_shape[1]),
1129
+ quant_block_shape=quant_block_shape,
1130
num_groups=g,
1106
- is_sfa=False,
1131
)
1132
return wq, dg_ws
1133
@@ -1113,22 +1137,12 @@ def deepgemm_post_process_fp8_weight_block(
1137
wq = wq.unsqueeze(0)
1138
ws = ws.unsqueeze(0)
1139
1116
- # From https://github.com/deepseek-ai/DeepGEMM/blob/c9f8b34dcdacc20aa746b786f983492c51072870/csrc/utils/layout.hpp#L46
1117
- # (1, block_n, block_k): (1, 128, 128) for FP8 block, (1, 1, 32) for MXFP8.
1118
- recipe = (1, quant_block_shape[0], quant_block_shape[1])
1119
-
1120
- # Ref : https://github.com/deepseek-ai/DeepGEMM/blob/c9f8b34dcdacc20aa746b786f983492c51072870/csrc/apis/gemm.hpp
1121
- # DeepGemm uses the `transform_sf_into_required_layout` function to
1122
- # represent scales in the correct format.
1123
- dg_ws = transform_sf_into_required_layout(
1124
- sf=ws,
1140
+ dg_ws = deepgemm_post_process_weight_scale_block(
1141
+ ws=ws,
1142
mn=wq.size(1),
1143
k=wq.size(2),
1127
- recipe=recipe,
1144
+ quant_block_shape=quant_block_shape,
1145
num_groups=wq.size(0),
1129
- # is the scale factors for A in (Refers to the argument A in A @ B).
1130
- # Weights are B.
1131
- is_sfa=False,
1146
)
1147
1148
if original_ndim == 2:
vllm/model_executor/warmup/deep_gemm_warmup.py
+58
-25
@@ -12,7 +12,9 @@ from tqdm import tqdm
12
import vllm.envs as envs
13
from vllm.distributed.parallel_state import get_dp_group, is_global_first_rank
14
from vllm.model_executor.layers.fused_moe import MoERunner
15
-from vllm.model_executor.layers.fused_moe.deep_gemm_utils import compute_aligned_M
15
+from vllm.model_executor.layers.fused_moe.deep_gemm_utils import (
16
+ compute_aligned_M_and_alignment,
17
+)
18
from vllm.model_executor.layers.fused_moe.experts.deep_gemm_moe import DeepGemmExperts
19
from vllm.model_executor.layers.fused_moe.experts.triton_deep_gemm_moe import (
20
TritonOrDeepGemmExperts,
@@ -25,6 +27,7 @@ from vllm.utils.deep_gemm import (
27
fp8_gemm_nt,
28
get_mk_alignment_for_contiguous_layout,
29
m_grouped_fp8_gemm_nt_contiguous,
30
+ mk_alignment_scope,
31
)
32
from vllm.utils.math_utils import cdiv
33
from vllm.utils.platform_utils import num_compute_units
@@ -238,7 +241,7 @@ def _get_grouped_gemm_params(
241
w2: torch.Tensor,
242
num_topk: int,
243
max_tokens: int,
241
-) -> tuple[int, int, torch.Tensor]:
244
+) -> tuple[int, int, list[tuple[int, int, torch.Tensor]]]:
245
assert w1.size(0) == w2.size(0), "w1 and w2 must have the same number of experts"
246
247
block_m = get_mk_alignment_for_contiguous_layout()[0]
@@ -248,19 +251,46 @@ def _get_grouped_gemm_params(
251
# Assumes all ranks have the same max_num_batched_tokens
252
max_tokens = get_dp_group().world_size * max_tokens
253
251
- # This is the maximum GroupedGemm M size that we expect to run
252
- # the grouped_gemm with.
253
- MAX_M = compute_aligned_M(
254
- max_tokens, num_topk, num_experts, block_m, expert_tokens_meta=None
255
- )
256
- # Distribute expert-ids evenly.
257
- MAX_BLOCKS = MAX_M // block_m
258
- expert_ids_block = torch.randint(
259
- low=0, high=num_experts, size=(MAX_BLOCKS,), device=device, dtype=torch.int32
254
+ request_m_values = _generate_optimal_warmup_m_values(
255
+ max_tokens,
256
+ max(w1.size(1), w2.size(1)),
257
+ device,
258
)
261
- expert_ids = torch.repeat_interleave(expert_ids_block, block_m, dim=0)
259
+ request_m_values = sorted({m for m in (*request_m_values, max_tokens) if m > 0})
260
+ if not request_m_values:
261
+ return 0, block_m, []
262
+
263
+ cases_by_shape: dict[tuple[int, int], torch.Tensor] = {}
264
+ for request_m in request_m_values:
265
+ M_sum, align_used = compute_aligned_M_and_alignment(
266
+ M=request_m,
267
+ num_topk=num_topk,
268
+ local_num_experts=num_experts,
269
+ alignment=block_m,
270
+ expert_tokens_meta=None,
271
+ )
272
+ if (M_sum, align_used) in cases_by_shape:
273
+ continue
274
+
275
+ num_blocks = M_sum // align_used
276
+ expert_ids_block = torch.randint(
277
+ low=0,
278
+ high=num_experts,
279
+ size=(num_blocks,),
280
+ device=device,
281
+ dtype=torch.int32,
282
+ )
283
+ cases_by_shape[(M_sum, align_used)] = torch.repeat_interleave(
284
+ expert_ids_block, align_used, dim=0
285
+ )
286
263
- return MAX_M, block_m, expert_ids
287
+ max_m = max(M_sum for M_sum, _ in cases_by_shape)
288
+ warmup_cases = [
289
+ (M_sum, align_used, expert_ids)
290
+ for (M_sum, align_used), expert_ids in sorted(cases_by_shape.items())
291
+ ]
292
+
293
+ return max_m, block_m, warmup_cases
294
295
296
def _deepgemm_grouped_fp8_gemm_nt_contiguous_warmup(
@@ -278,7 +308,11 @@ def _deepgemm_grouped_fp8_gemm_nt_contiguous_warmup(
308
):
309
return
310
281
- MAX_M, block_m, expert_ids = _get_grouped_gemm_params(w1, w2, num_topk, max_tokens)
311
+ MAX_M, block_m, warmup_cases = _get_grouped_gemm_params(
312
+ w1, w2, num_topk, max_tokens
313
+ )
314
+ if not warmup_cases:
315
+ return
316
device = w1.device
317
318
def _warmup(w: torch.Tensor, w_scale: torch.Tensor):
@@ -289,15 +323,14 @@ def _deepgemm_grouped_fp8_gemm_nt_contiguous_warmup(
323
)
324
out = torch.empty((MAX_M, n), device=device, dtype=torch.bfloat16)
325
292
- m_values = list(range(block_m, MAX_M + 1, block_m))
293
-
294
- for num_tokens in m_values:
295
- m_grouped_fp8_gemm_nt_contiguous(
296
- (a1q[:num_tokens], a1q_scales[:num_tokens]),
297
- (w, w_scale),
298
- out[:num_tokens],
299
- expert_ids[:num_tokens],
300
- )
326
+ for num_tokens, align_used, expert_ids in warmup_cases:
327
+ with mk_alignment_scope(align_used):
328
+ m_grouped_fp8_gemm_nt_contiguous(
329
+ (a1q[:num_tokens], a1q_scales[:num_tokens]),
330
+ (w, w_scale),
331
+ out[:num_tokens],
332
+ expert_ids,
333
+ )
334
if pbar is not None:
335
pbar.update(1)
336
@@ -350,8 +383,8 @@ def _count_warmup_iterations(model: torch.nn.Module, max_tokens: int) -> int:
383
w13, _, w2, _, num_topk = _extract_data_from_fused_moe_module(m)
384
if w13.size() in seen_grouped_sizes and w2.size() in seen_grouped_sizes:
385
continue
353
- MAX_M, block_m, _ = _get_grouped_gemm_params(w13, w2, num_topk, max_tokens)
354
- n_values = (MAX_M - block_m) // block_m + 1
386
+ _, _, warmup_cases = _get_grouped_gemm_params(w13, w2, num_topk, max_tokens)
387
+ n_values = len(warmup_cases)
388
if w13.size() not in seen_grouped_sizes:
389
total += n_values
390
seen_grouped_sizes.add(w13.size())
vllm/model_executor/warmup/deepseek_v4_mhc_warmup.py
new
+226
@@ -0,0 +1,226 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Warm up DeepSeek V4 mHC TileLang kernels before serving requests.
4
+
5
+Ported from lucifer1004/vllm-jasl with the two env-var knobs removed
6
+(`VLLM_ENABLE_DEEPSEEK_V4_MHC_WARMUP`, `VLLM_DEEPSEEK_V4_MHC_WARMUP_TOKEN_SIZES`).
7
+Gating is intrinsic: non-DSv4 models and layers without hc_* attributes
8
+return early, so the warmup is a no-op except where it's needed.
9
+"""
10
+
11
+import time
12
+from collections.abc import Iterable
13
+
14
+import torch
15
+
16
+from vllm.logger import init_logger
17
+from vllm.tracing import instrument
18
+from vllm.utils.math_utils import cdiv
19
+
20
+logger = init_logger(__name__)
21
+
22
+_AUTO_WARMUP_MAX_TOKENS = 16_384
23
+_DEFAULT_TOKEN_SIZE_CANDIDATES = (
24
+ 1,
25
+ 2,
26
+ 4,
27
+ 8,
28
+ 16,
29
+ 32,
30
+ 64,
31
+ 128,
32
+ 256,
33
+ 512,
34
+ 1024,
35
+ 2048,
36
+ 4096,
37
+ 8192,
38
+ 16_384,
39
+)
40
+
41
+
42
+def _compute_mhc_pre_num_split(
43
+ *,
44
+ num_tokens: int,
45
+ hidden_size: int,
46
+ hc_mult: int,
47
+ num_sms: int,
48
+) -> int:
49
+ block_k = 64
50
+ block_m = 64
51
+ k = hc_mult * hidden_size
52
+ grid_size = cdiv(num_tokens, block_m)
53
+ split_k = num_sms // grid_size
54
+ num_block_k = cdiv(k, block_k)
55
+ split_k = min(split_k, num_block_k // 4)
56
+ return max(split_k, 1)
57
+
58
+
59
+def _normalize_token_sizes(
60
+ token_sizes: Iterable[int],
61
+ *,
62
+ max_tokens: int,
63
+) -> list[int]:
64
+ return sorted({size for size in token_sizes if 1 <= size <= max_tokens})
65
+
66
+
67
+def _select_mhc_warmup_token_sizes(
68
+ *,
69
+ max_tokens: int,
70
+ cudagraph_capture_sizes: list[int],
71
+) -> list[int]:
72
+ if max_tokens <= 0:
73
+ return []
74
+
75
+ max_auto_tokens = min(max_tokens, _AUTO_WARMUP_MAX_TOKENS)
76
+ candidates = list(_DEFAULT_TOKEN_SIZE_CANDIDATES)
77
+ candidates.extend(cudagraph_capture_sizes)
78
+ candidates.append(max_auto_tokens)
79
+ return _normalize_token_sizes(candidates, max_tokens=max_auto_tokens)
80
+
81
+
82
+def _find_first_mhc_layer(model: torch.nn.Module) -> torch.nn.Module | None:
83
+ for module in model.modules():
84
+ if module.__class__.__name__ != "DeepseekV4DecoderLayer":
85
+ continue
86
+ if all(
87
+ hasattr(module, attr)
88
+ for attr in (
89
+ "hc_pre",
90
+ "hc_post",
91
+ "hc_attn_fn",
92
+ "hc_attn_scale",
93
+ "hc_attn_base",
94
+ "hc_ffn_fn",
95
+ "hc_ffn_scale",
96
+ "hc_ffn_base",
97
+ )
98
+ ):
99
+ return module
100
+ return None
101
+
102
+
103
+def _find_deepseek_v4_model(model: torch.nn.Module) -> torch.nn.Module | None:
104
+ for module in model.modules():
105
+ if module.__class__.__name__ != "DeepseekV4Model":
106
+ continue
107
+ if all(
108
+ hasattr(module, attr)
109
+ for attr in ("hc_head_fn", "hc_head_scale", "hc_head_base")
110
+ ):
111
+ return module
112
+ return None
113
+
114
+
115
+def _warmup_layer_mhc(
116
+ layer: torch.nn.Module,
117
+ token_sizes: list[int],
118
+) -> None:
119
+ max_tokens = max(token_sizes)
120
+ hidden_size = int(layer.hidden_size)
121
+ hc_mult = int(layer.hc_mult)
122
+ device = layer.hc_attn_fn.device
123
+ residual = torch.zeros(
124
+ max_tokens,
125
+ hc_mult,
126
+ hidden_size,
127
+ dtype=torch.bfloat16,
128
+ device=device,
129
+ )
130
+
131
+ for size in token_sizes:
132
+ residual_slice = residual[:size]
133
+ for fn, scale, base in (
134
+ (layer.hc_attn_fn, layer.hc_attn_scale, layer.hc_attn_base),
135
+ (layer.hc_ffn_fn, layer.hc_ffn_scale, layer.hc_ffn_base),
136
+ ):
137
+ layer_input, post_mix, comb_mix = layer.hc_pre(
138
+ residual_slice,
139
+ fn,
140
+ scale,
141
+ base,
142
+ )
143
+ layer.hc_post(layer_input, residual_slice, post_mix, comb_mix)
144
+
145
+
146
+def _warmup_hc_head(
147
+ model: torch.nn.Module,
148
+ token_sizes: list[int],
149
+) -> None:
150
+ # Upstream a8887c208 ("[DSV4] aiter mhc support (ROCm)") refactored
151
+ # ``hc_head`` from a free function into the ``HCHeadOp`` CustomOp
152
+ # instance attached to the model as ``hc_head_op``. We call through
153
+ # that instance so the warmup exercises the same dispatched
154
+ # implementation as the inference path.
155
+ hc_head_op = getattr(model, "hc_head_op", None)
156
+ if hc_head_op is None:
157
+ return
158
+
159
+ max_tokens = max(token_sizes)
160
+ hidden_size = int(model.config.hidden_size)
161
+ hc_mult = int(model.hc_mult)
162
+ device = model.hc_head_fn.device
163
+ hidden_states = torch.zeros(
164
+ max_tokens,
165
+ hc_mult,
166
+ hidden_size,
167
+ dtype=torch.bfloat16,
168
+ device=device,
169
+ )
170
+
171
+ for size in token_sizes:
172
+ hc_head_op(
173
+ hidden_states[:size],
174
+ model.hc_head_fn,
175
+ model.hc_head_scale,
176
+ model.hc_head_base,
177
+ model.rms_norm_eps,
178
+ model.hc_eps,
179
+ )
180
+
181
+
182
+@instrument(span_name="DeepSeek V4 mHC warmup")
183
+def deepseek_v4_mhc_warmup(
184
+ model: torch.nn.Module,
185
+ *,
186
+ max_tokens: int,
187
+ cudagraph_capture_sizes: list[int] | None = None,
188
+) -> None:
189
+ # Cheap model-type gate before walking ``model.modules()``. The class
190
+ # walk below is O(num_layers) and shows up in startup time on very
191
+ # large checkpoints; bail out for any model that is not DeepSeek V4.
192
+ config = getattr(model, "config", None)
193
+ model_type = getattr(config, "model_type", None) if config is not None else None
194
+ if model_type is not None and model_type != "deepseek_v4":
195
+ return
196
+
197
+ layer = _find_first_mhc_layer(model)
198
+ if layer is None:
199
+ return
200
+
201
+ device = layer.hc_attn_fn.device
202
+ if device.type != "cuda":
203
+ return
204
+
205
+ deepseek_model = _find_deepseek_v4_model(model)
206
+ token_sizes = _select_mhc_warmup_token_sizes(
207
+ max_tokens=max_tokens,
208
+ cudagraph_capture_sizes=cudagraph_capture_sizes or [],
209
+ )
210
+ if not token_sizes:
211
+ return
212
+
213
+ started = time.perf_counter()
214
+ logger.info(
215
+ "Warming up DeepSeek V4 mHC TileLang kernels for token sizes: %s",
216
+ token_sizes,
217
+ )
218
+ with torch.inference_mode():
219
+ _warmup_layer_mhc(layer, token_sizes)
220
+ if deepseek_model is not None:
221
+ _warmup_hc_head(deepseek_model, token_sizes)
222
+ torch.accelerator.synchronize()
223
+ logger.info(
224
+ "DeepSeek V4 mHC TileLang warmup finished in %.2f seconds.",
225
+ time.perf_counter() - started,
226
+ )
vllm/model_executor/warmup/flashinfer_autotune_cache.py
new
+56
@@ -0,0 +1,56 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""FlashInfer autotune cache helpers."""
4
+
5
+import hashlib
6
+import os
7
+import tempfile
8
+from contextlib import suppress
9
+from pathlib import Path
10
+from typing import TYPE_CHECKING
11
+
12
+import vllm.envs as envs
13
+from vllm.compilation.caching import aot_compile_hash_factors
14
+
15
+if TYPE_CHECKING:
16
+ from vllm.v1.worker.gpu_model_runner import GPUModelRunner
17
+
18
+
19
+def flashinfer_autotune_cache_hash(runner: "GPUModelRunner") -> str:
20
+ factors = aot_compile_hash_factors(runner.vllm_config)
21
+ return hashlib.sha256(str(factors).encode()).hexdigest()
22
+
23
+
24
+def resolve_flashinfer_autotune_file(runner: "GPUModelRunner") -> Path:
25
+ override_dir = envs.VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR
26
+ if override_dir:
27
+ root = Path(override_dir).expanduser()
28
+ else:
29
+ from flashinfer.jit import env as flashinfer_jit_env
30
+
31
+ flashinfer_workspace = flashinfer_jit_env.FLASHINFER_WORKSPACE_DIR
32
+ root = (
33
+ Path(envs.VLLM_CACHE_ROOT)
34
+ / "flashinfer_autotune_cache"
35
+ / flashinfer_workspace.parent.name
36
+ / flashinfer_workspace.name
37
+ )
38
+
39
+ output_dir = root / flashinfer_autotune_cache_hash(runner)
40
+ output_dir.mkdir(parents=True, exist_ok=True)
41
+ return output_dir / "autotune_configs.json"
42
+
43
+
44
+def write_flashinfer_autotune_cache(cache_path: Path, contents: bytes) -> None:
45
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
46
+ fd, tmp_path = tempfile.mkstemp(
47
+ dir=cache_path.parent, suffix=".tmp", prefix=f".{cache_path.name}."
48
+ )
49
+ try:
50
+ with os.fdopen(fd, "wb") as f:
51
+ f.write(contents)
52
+ os.replace(tmp_path, cache_path)
53
+ except BaseException:
54
+ with suppress(OSError):
55
+ os.unlink(tmp_path)
56
+ raise
vllm/model_executor/warmup/flashinfer_sparse_mla_warmup.py
new
+255
@@ -0,0 +1,255 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Warmup and autotune helpers for FlashInfer sparse MLA backends."""
4
+
5
+from typing import TYPE_CHECKING, cast
6
+
7
+import torch
8
+
9
+from vllm.logger import init_logger
10
+from vllm.model_executor.warmup.flashinfer_autotune_cache import (
11
+ resolve_flashinfer_autotune_file,
12
+ write_flashinfer_autotune_cache,
13
+)
14
+from vllm.platforms import current_platform
15
+from vllm.utils.flashinfer import autotune as flashinfer_autotune
16
+from vllm.utils.flashinfer import has_flashinfer
17
+from vllm.v1.worker.gpu.warmup import run_mixed_prefill_decode_warmup
18
+
19
+if TYPE_CHECKING:
20
+ from vllm.v1.worker.gpu.model_runner import GPUModelRunner as V2GPUModelRunner
21
+ from vllm.v1.worker.gpu_model_runner import GPUModelRunner
22
+ from vllm.v1.worker.gpu_worker import Worker
23
+
24
+logger = init_logger(__name__)
25
+
26
+_DEEPSEEK_V4_SPARSE_MLA_BACKENDS = frozenset(
27
+ {
28
+ "FLASHMLA_SPARSE_DSV4",
29
+ "FLASHINFER_MLA_SPARSE_DSV4",
30
+ "ROCM_FLASHMLA_SPARSE_DSV4",
31
+ "DEEPSEEK_SPARSE_SWA",
32
+ }
33
+)
34
+_FLASHINFER_MLA_SPARSE_BACKENDS = frozenset({"FLASHINFER_MLA_SPARSE_SM120"})
35
+_DEEPSEEK_V4_FLASHINFER_MLA_SPARSE_BACKENDS = frozenset({"FLASHINFER_MLA_SPARSE_DSV4"})
36
+
37
+_FLASHINFER_SM120_SPARSE_MLA_DECODE_LABELS = {
38
+ "FLASHINFER_MLA_SPARSE_SM120": "DSv3.2",
39
+ "FLASHINFER_MLA_SPARSE_DSV4": "DSv4",
40
+}
41
+
42
+_SPARSE_MLA_MIXED_WARMUP_TOKENS = 16
43
+
44
+
45
+def _attention_backend_name(backend: object) -> str | None:
46
+ get_name = getattr(backend, "get_name", None)
47
+ if get_name is None:
48
+ return None
49
+ try:
50
+ return get_name()
51
+ except NotImplementedError:
52
+ return None
53
+
54
+
55
+def _has_deepseek_v4_sparse_mla_backend(runner: "GPUModelRunner") -> bool:
56
+ for groups in getattr(runner, "attn_groups", []) or ():
57
+ for group in groups:
58
+ name = _attention_backend_name(getattr(group, "backend", None))
59
+ if name in _DEEPSEEK_V4_SPARSE_MLA_BACKENDS:
60
+ return True
61
+ return False
62
+
63
+
64
+def _flashinfer_sparse_mla_decode_label(
65
+ runner: "GPUModelRunner",
66
+ allowed_backends: frozenset[str],
67
+) -> str | None:
68
+ for groups in getattr(runner, "attn_groups", []) or ():
69
+ for group in groups:
70
+ name = _attention_backend_name(getattr(group, "backend", None))
71
+ if name in allowed_backends:
72
+ return _FLASHINFER_SM120_SPARSE_MLA_DECODE_LABELS.get(name)
73
+ return None
74
+
75
+
76
+def _clamp_warmup_tokens(num_tokens: int, max_tokens: int) -> int:
77
+ return max(0, min(num_tokens, max_tokens))
78
+
79
+
80
+def _uses_v2_model_runner(runner: "GPUModelRunner") -> bool:
81
+ vllm_config = getattr(runner, "vllm_config", None)
82
+ return bool(getattr(vllm_config, "use_v2_model_runner", False))
83
+
84
+
85
+def _run_flashinfer_sparse_mla_decode_autotune(
86
+ worker: "Worker",
87
+ num_tokens: int,
88
+ allowed_backends: frozenset[str],
89
+) -> bool:
90
+ """Autotune FlashInfer's SM120 sparse-MLA decode path."""
91
+ runner = worker.model_runner
92
+ log_label = _flashinfer_sparse_mla_decode_label(runner, allowed_backends)
93
+ if log_label is None:
94
+ return False
95
+ if worker.vllm_config.kernel_config.enable_flashinfer_autotune is not True:
96
+ return False
97
+ if not has_flashinfer() or not current_platform.is_device_capability_family(120):
98
+ return False
99
+
100
+ try:
101
+ from flashinfer.autotuner import AutoTuner
102
+ except ImportError:
103
+ logger.warning(
104
+ "Skipping FlashInfer SM120 sparse MLA decode autotune because "
105
+ "FlashInfer autotuner is unavailable."
106
+ )
107
+ return False
108
+
109
+ from vllm.distributed.parallel_state import get_world_group
110
+
111
+ world = get_world_group()
112
+ is_leader = world.rank_in_group == 0
113
+ cache_path = resolve_flashinfer_autotune_file(runner)
114
+
115
+ dummy_run_kwargs = dict(
116
+ num_tokens=num_tokens,
117
+ skip_eplb=True,
118
+ is_profile=True,
119
+ force_attention=True,
120
+ create_mixed_batch=True,
121
+ )
122
+
123
+ if is_leader:
124
+ logger.info(
125
+ "Autotuning FlashInfer SM120 sparse MLA %s decode with cache: %s",
126
+ log_label,
127
+ cache_path,
128
+ )
129
+
130
+ with torch.inference_mode():
131
+ warmup_executed = True
132
+ if is_leader:
133
+ if _uses_v2_model_runner(runner):
134
+ v2_runner = cast("V2GPUModelRunner", runner)
135
+ warmup_executed = run_mixed_prefill_decode_warmup(
136
+ v2_runner,
137
+ worker.execute_model,
138
+ worker.sample_tokens,
139
+ num_tokens,
140
+ mixed_step_context=flashinfer_autotune(True, cache=str(cache_path)),
141
+ req_id_prefix="_sparse_mla_v2_warmup",
142
+ )
143
+ else:
144
+ with flashinfer_autotune(True, cache=str(cache_path)):
145
+ runner._dummy_run(**dummy_run_kwargs)
146
+ else:
147
+ if _uses_v2_model_runner(runner):
148
+ v2_runner = cast("V2GPUModelRunner", runner)
149
+ warmup_executed = run_mixed_prefill_decode_warmup(
150
+ v2_runner,
151
+ worker.execute_model,
152
+ worker.sample_tokens,
153
+ num_tokens,
154
+ req_id_prefix="_sparse_mla_v2_warmup",
155
+ )
156
+ else:
157
+ runner._dummy_run(**dummy_run_kwargs)
158
+
159
+ if not warmup_executed:
160
+ return False
161
+
162
+ tune_results: bytes | None = None
163
+ if is_leader and cache_path.exists():
164
+ with open(cache_path, "rb") as f:
165
+ tune_results = f.read()
166
+
167
+ tune_results = world.broadcast_object(tune_results, src=0)
168
+ if tune_results is None:
169
+ logger.warning(
170
+ "No FlashInfer SM120 sparse MLA %s decode autotune cache entries found. "
171
+ "Falling back to FlashInfer's default tactic heuristic.",
172
+ log_label,
173
+ )
174
+ world.barrier()
175
+ return True
176
+
177
+ write_flashinfer_autotune_cache(cache_path, tune_results)
178
+ world.barrier()
179
+
180
+ AutoTuner.get().load_configs(str(cache_path))
181
+ logger.info(
182
+ "FlashInfer SM120 sparse MLA %s decode autotune cache loaded on rank %d "
183
+ "from %s.",
184
+ log_label,
185
+ world.rank_in_group,
186
+ cache_path,
187
+ )
188
+ return True
189
+
190
+
191
+def _flashinfer_sparse_mla_decode_autotune(
192
+ worker: "Worker",
193
+ num_tokens: int,
194
+) -> bool:
195
+ return _run_flashinfer_sparse_mla_decode_autotune(
196
+ worker, num_tokens, _FLASHINFER_MLA_SPARSE_BACKENDS
197
+ )
198
+
199
+
200
+def _deepseek_v4_sparse_mla_decode_autotune(
201
+ worker: "Worker",
202
+ num_tokens: int,
203
+) -> bool:
204
+ return _run_flashinfer_sparse_mla_decode_autotune(
205
+ worker, num_tokens, _DEEPSEEK_V4_FLASHINFER_MLA_SPARSE_BACKENDS
206
+ )
207
+
208
+
209
+def flashinfer_sparse_mla_decode_autotune_warmup(worker: "Worker") -> None:
210
+ """Autotune generic FlashInfer sparse MLA decode when selected."""
211
+ runner = worker.model_runner
212
+ if runner.is_pooling_model:
213
+ return
214
+
215
+ max_tokens = worker.scheduler_config.max_num_batched_tokens
216
+ mixed_tokens = _clamp_warmup_tokens(_SPARSE_MLA_MIXED_WARMUP_TOKENS, max_tokens)
217
+ if mixed_tokens <= 0:
218
+ return
219
+ _flashinfer_sparse_mla_decode_autotune(worker, mixed_tokens)
220
+
221
+
222
+def deepseek_v4_sparse_mla_attention_warmup(worker: "Worker") -> None:
223
+ """Warm DSv4 sparse-MLA mixed prefill+decode attention."""
224
+ runner = worker.model_runner
225
+ if runner.is_pooling_model or not _has_deepseek_v4_sparse_mla_backend(runner):
226
+ return
227
+
228
+ max_tokens = worker.scheduler_config.max_num_batched_tokens
229
+ mixed_tokens = _clamp_warmup_tokens(_SPARSE_MLA_MIXED_WARMUP_TOKENS, max_tokens)
230
+ if mixed_tokens <= 0:
231
+ return
232
+
233
+ logger.info(
234
+ "Warming up DeepSeek V4 sparse MLA attention for mixed tokens=%s.",
235
+ mixed_tokens,
236
+ )
237
+ mixed_warmup_done = _deepseek_v4_sparse_mla_decode_autotune(worker, mixed_tokens)
238
+ if not mixed_warmup_done:
239
+ if _uses_v2_model_runner(runner):
240
+ v2_runner = cast("V2GPUModelRunner", runner)
241
+ run_mixed_prefill_decode_warmup(
242
+ v2_runner,
243
+ worker.execute_model,
244
+ worker.sample_tokens,
245
+ mixed_tokens,
246
+ req_id_prefix="_sparse_mla_v2_warmup",
247
+ )
248
+ else:
249
+ runner._dummy_run(
250
+ num_tokens=mixed_tokens,
251
+ skip_eplb=True,
252
+ is_profile=True,
253
+ force_attention=True,
254
+ create_mixed_batch=True,
255
+ )
vllm/model_executor/warmup/kernel_warmup.py
+28
-32
@@ -6,16 +6,24 @@ This is useful specifically for JIT'ed kernels as we don't want JIT'ing to
6
happen during model execution.
7
"""
8
9
-import hashlib
10
-from pathlib import Path
9
from typing import TYPE_CHECKING
10
11
import torch
12
13
import vllm.envs as envs
16
-from vllm.compilation.caching import aot_compile_hash_factors
14
from vllm.logger import init_logger
15
from vllm.model_executor.warmup.deep_gemm_warmup import deep_gemm_warmup
16
+from vllm.model_executor.warmup.deepseek_v4_mhc_warmup import (
17
+ deepseek_v4_mhc_warmup,
18
+)
19
+from vllm.model_executor.warmup.flashinfer_autotune_cache import (
20
+ resolve_flashinfer_autotune_file,
21
+ write_flashinfer_autotune_cache,
22
+)
23
+from vllm.model_executor.warmup.flashinfer_sparse_mla_warmup import (
24
+ deepseek_v4_sparse_mla_attention_warmup,
25
+ flashinfer_sparse_mla_decode_autotune_warmup,
26
+)
27
from vllm.platforms import current_platform
28
from vllm.utils.deep_gemm import is_deep_gemm_supported
29
from vllm.utils.flashinfer import has_flashinfer
@@ -27,36 +35,26 @@ if TYPE_CHECKING:
35
logger = init_logger(__name__)
36
37
30
-def _flashinfer_autotune_cache_hash(runner: "GPUModelRunner") -> str:
31
- factors = aot_compile_hash_factors(runner.vllm_config)
32
- return hashlib.sha256(str(factors).encode()).hexdigest()
33
-
34
-
35
-def _resolve_flashinfer_autotune_file(runner: "GPUModelRunner") -> Path:
36
- override_dir = envs.VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR
37
- if override_dir:
38
- root = Path(override_dir).expanduser()
39
- else:
40
- from flashinfer.jit import env as flashinfer_jit_env
41
-
42
- flashinfer_workspace = flashinfer_jit_env.FLASHINFER_WORKSPACE_DIR
43
- root = (
44
- Path(envs.VLLM_CACHE_ROOT)
45
- / "flashinfer_autotune_cache"
46
- / flashinfer_workspace.parent.name
47
- / flashinfer_workspace.name
48
- )
49
-
50
- output_dir = root / _flashinfer_autotune_cache_hash(runner)
51
- output_dir.mkdir(parents=True, exist_ok=True)
52
- return output_dir / "autotune_configs.json"
53
-
54
-
38
def kernel_warmup(worker: "Worker"):
39
from vllm.model_executor.warmup.minimax_m3_msa_warmup import (
40
minimax_m3_msa_warmup,
41
)
42
43
+ # DSv4 mHC TileLang kernels (hc_pre/hc_post/hc_head_op) run every decoder
44
+ # layer per token; warm them across token sizes first so the first real
45
+ # request doesn't pay JIT cost. No-op for non-DSv4 models (gated inside).
46
+ deepseek_v4_mhc_warmup(
47
+ worker.get_model(),
48
+ max_tokens=worker.scheduler_config.max_num_batched_tokens,
49
+ cudagraph_capture_sizes=(
50
+ worker.vllm_config.compilation_config.cudagraph_capture_sizes or []
51
+ ),
52
+ )
53
+
54
+ # Run next so input-prep kernels JIT against pristine runner state.
55
+ flashinfer_sparse_mla_decode_autotune_warmup(worker)
56
+ deepseek_v4_sparse_mla_attention_warmup(worker)
57
+
58
# Deep GEMM warmup
59
do_deep_gemm_warmup = (
60
envs.VLLM_USE_DEEP_GEMM
@@ -147,7 +145,7 @@ def flashinfer_autotune(runner: "GPUModelRunner") -> None:
145
world = get_world_group()
146
is_leader = world.rank_in_group == 0
147
150
- cache_path = _resolve_flashinfer_autotune_file(runner)
148
+ cache_path = resolve_flashinfer_autotune_file(runner)
149
if is_leader:
150
logger.info("Using FlashInfer autotune cache file: %s", cache_path)
151
@@ -183,9 +181,7 @@ def flashinfer_autotune(runner: "GPUModelRunner") -> None:
181
"Falling back to default tactics."
182
)
183
else:
186
- if not is_leader and world.local_rank == 0:
187
- with open(cache_path, "wb") as f:
188
- f.write(tune_results)
184
+ write_flashinfer_autotune_cache(cache_path, tune_results)
185
world.barrier()
186
from flashinfer.autotuner import AutoTuner
187
vllm/models/deepseek_v4/attention.py
+34
-32
@@ -62,23 +62,22 @@ logger = init_logger(__name__)
62
63
64
def _resolve_dsv4_kv_cache_dtype(
65
- use_flashmla_fp8_layout: bool,
65
+ use_fp8_ds_mla_layout: bool,
66
kv_cache_dtype: str,
67
cache_config: CacheConfig | None,
68
) -> tuple[str, torch.dtype]:
69
"""Map ``(layout, --kv-cache-dtype)`` to ``(cache_dtype_str, torch_dtype)``.
70
71
Both layouts are paged; they differ in the per-token block format. The
72
- FlashMLA fp8 layout (FlashMLA / ROCm Aiter) is the ``fp8_ds_mla`` format:
73
- UE8M0 block-scaled fp8 packed as ``uint8`` (the canonical ``fp8_ds_mla``
74
- string is written back onto ``cache_config`` so the page-size specs pick
75
- the 576B per-token slot). Otherwise (FlashInfer) each token's KV row is
76
- stored in its plain element dtype — bf16 or per-tensor FP8 E4M3.
72
+ ``fp8_ds_mla`` format is UE8M0 block-scaled fp8 packed as ``uint8`` (the
73
+ canonical ``fp8_ds_mla`` string is written back onto ``cache_config`` so the
74
+ page-size specs pick the 576B per-token slot). Plain-row backends store each
75
+ token's KV row in its element dtype: bf16 or per-tensor FP8 E4M3.
76
"""
78
- if use_flashmla_fp8_layout:
77
+ if use_fp8_ds_mla_layout:
78
# fp8_ds_mla block format: UE8M0 block-scaled fp8 packed as uint8.
79
assert kv_cache_dtype.startswith("fp8"), (
81
- f"DeepseekV4 FlashMLA fp8 layout only supports fp8 kv-cache, "
80
+ f"DeepseekV4 fp8_ds_mla layout only supports fp8 kv-cache, "
81
f"got {kv_cache_dtype}"
82
)
83
if kv_cache_dtype != "fp8_ds_mla":
@@ -100,18 +99,20 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
99
100
The platform-specific sparse-MLA forward (``forward_mqa`` /
101
``get_padded_num_q_heads`` / ``_o_proj`` / ``backend_cls``) is provided by a
103
- subclass — ``DeepseekV4FlashMLAAttention`` / ``DeepseekV4FlashInferMLAAttention``
104
- (CUDA) or ``DeepseekV4ROCMAiterMLAAttention`` (ROCm) — selected by the
105
- platform-specific deepseek_v4 model module. The base is never instantiated
106
- directly.
102
+ subclass — ``DeepseekV4FlashMLAAttention`` /
103
+ ``DeepseekV4FlashInferSM120Attention`` /
104
+ ``DeepseekV4FlashInferMLAAttention`` (CUDA) or
105
+ ``DeepseekV4ROCMAiterMLAAttention`` (ROCm) — selected by the platform-specific
106
+ deepseek_v4 model module. The base is never instantiated directly.
107
"""
108
109
# Provided by the platform subclass.
110
backend_cls: ClassVar[type[AttentionBackend]]
111
# KV-cache per-token block format (both layouts are paged). True (default)
112
- # = FlashMLA / ROCm fp8_ds_mla (UE8M0 block-scaled fp8 packed as uint8);
113
- # False = FlashInfer plain bf16 / per-tensor fp8 KV row.
114
- use_flashmla_fp8_layout: ClassVar[bool] = True
112
+ # = fp8_ds_mla (UE8M0 block-scaled fp8 packed as uint8); False = plain
113
+ # bf16 / per-tensor fp8 KV row. Backends can override the instance hook when
114
+ # a single attention class dispatches across arch-specific layouts.
115
+ use_fp8_ds_mla_layout: ClassVar[bool] = True
116
# Prefill is processed in fixed-size chunks; this bounds the bf16 kv-gather
117
# workspace allocated in _forward_prefill and is also read by the dummy-run
118
# path to pre-reserve that workspace.
@@ -145,6 +146,10 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
146
"""Inverse-RoPE + wo_a + wo_b output projection (platform-specific)."""
147
raise NotImplementedError
148
149
+ def _uses_fp8_ds_mla_layout(self) -> bool:
150
+ """Return whether this instance stores fp8 KV in fp8_ds_mla layout."""
151
+ return self.use_fp8_ds_mla_layout
152
+
153
def __init__(
154
self,
155
vllm_config: VllmConfig,
@@ -276,13 +281,10 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
281
)
282
self.max_model_len = vllm_config.model_config.max_model_len
283
279
- # Resolve the kv-cache dtype from this backend's block format (a
280
- # ClassVar set by the subclass): fp8_ds_mla (UE8M0 block-scaled fp8 as
281
- # uint8) for FlashMLA / ROCm, vs a plain bf16 / per-tensor fp8 row for
282
- # FlashInfer. The same resolution drives the SWA cache tensor dtype
283
- # below.
284
+ # Resolve the kv-cache dtype from this backend's block format. The same
285
+ # resolution drives the SWA cache tensor dtype below.
286
self.kv_cache_dtype, self.kv_cache_torch_dtype = _resolve_dsv4_kv_cache_dtype(
285
- self.use_flashmla_fp8_layout, cache_config.cache_dtype, cache_config
287
+ self._uses_fp8_ds_mla_layout(), cache_config.cache_dtype, cache_config
288
)
289
290
self.swa_cache_layer = DeepseekV4SWACache(
@@ -539,7 +541,7 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
541
542
# kv is unchanged; attention reads kv solely via swa_kv_cache.
543
if cache_dtype == torch.uint8:
542
- # Legacy FlashMLA UE8M0 paged path. Horizontally fused:
544
+ # fp8_ds_mla UE8M0 paged path. Horizontally fused:
545
# Q side: per-head RMSNorm (no weight) + GPT-J RoPE, zero-filling
546
# the padding head slots; the kernel allocates and returns
547
# the padded q tensor.
@@ -557,10 +559,10 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
559
swa_metadata.block_size,
560
)
561
560
- # FlashInfer full-cache path: the [num_blocks, block_size, 512] cache
561
- # stores the KV row in its plain dtype (no Q padding). bf16 rewrites q
562
- # in place; per-tensor fp8 writes a separately-allocated fp8 q and
563
- # quantizes the KV row.
562
+ # Plain-row path: the [num_blocks, block_size, 512] cache stores the KV
563
+ # row in its element dtype (no Q padding). bf16 rewrites q in place;
564
+ # per-tensor fp8 writes a separately-allocated fp8 q and quantizes the
565
+ # KV row.
566
block_size = swa_metadata.block_size
567
swa_kv_cache_3d = swa_kv_cache.view(-1, block_size, self.head_dim)
568
if cache_dtype == torch.bfloat16:
@@ -601,18 +603,18 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
603
self.compress_ratio <= 1
604
): # SWA part. Allocated separately as DeepseekV4SWACache.
605
return None
604
- # FlashMLA uses the fp8_ds_mla block format (UE8M0 block-scaled fp8 as
605
- # uint8, 576B aligned); FlashInfer stores a plain bf16 / per-tensor fp8
606
- # row with no extra alignment.
607
- is_flashmla = self.kv_cache_dtype == "fp8_ds_mla"
606
+ # fp8_ds_mla is a UE8M0 block-scaled uint8 layout and needs 576B
607
+ # alignment; plain bf16 / per-tensor fp8 rows use natural element-size
608
+ # pages.
609
+ uses_fp8_ds_mla_layout = self.kv_cache_dtype == "fp8_ds_mla"
610
return MLAAttentionSpec(
611
block_size=vllm_config.cache_config.block_size,
612
num_kv_heads=1,
613
head_size=self.head_dim,
612
- dtype=torch.uint8 if is_flashmla else self.kv_cache_torch_dtype,
614
+ dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype,
615
compress_ratio=self.compress_ratio,
616
cache_dtype_str=self.kv_cache_dtype,
615
- alignment=576 if is_flashmla else None, # FlashMLA needs 576B
617
+ alignment=576 if uses_fp8_ds_mla_layout else None,
618
model_version="deepseek_v4",
619
)
620
vllm/models/deepseek_v4/common/ops/fused_inv_rope_fp8_quant.py
+10
-1
@@ -40,9 +40,18 @@ def _fused_inv_rope_fp8_quant_per_head(
40
USE_GDC: tl.constexpr,
41
launch_pdl: tl.constexpr, # triton metadata
42
):
43
- # int64: stride multiply overflows int32 past num_tokens=32768 (IMA).
43
+ # Cast every stride to int64 — without this, Python-int strides are
44
+ # inferred as int32 and `pid_token(int64) × stride(int32)` can lower to
45
+ # int32 arithmetic, wrapping past 2³¹ for large prefill batches → IMA.
46
pid_token = tl.program_id(0).to(tl.int64)
47
pid_gh = tl.program_id(1).to(tl.int64)
48
+ o_stride_token = o_stride_token.to(tl.int64)
49
+ o_stride_head = o_stride_head.to(tl.int64)
50
+ cache_stride_pos = cache_stride_pos.to(tl.int64)
51
+ fp8_stride_group = fp8_stride_group.to(tl.int64)
52
+ fp8_stride_token = fp8_stride_token.to(tl.int64)
53
+ scale_stride_group = scale_stride_group.to(tl.int64)
54
+ scale_stride_k = scale_stride_k.to(tl.int64)
55
56
g = pid_gh // heads_per_group
57
head_in_group = pid_gh % heads_per_group
vllm/models/deepseek_v4/compressor.py
+9
-9
@@ -155,17 +155,17 @@ class CompressorStateCache(torch.nn.Module, AttentionLayerBase):
155
raise ValueError(f"Invalid compress ratio: {compress_ratio}")
156
157
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
158
- # FlashMLA's UE8M0 paged layout needs 576B alignment; the FlashInfer
159
- # full-cache path shares state pages with contiguous KV pages, so
160
- # padding would break page matching.
161
- is_flashmla = vllm_config.cache_config.cache_dtype == "fp8_ds_mla"
158
+ # fp8_ds_mla is the UE8M0 paged layout and needs 576B alignment. Plain
159
+ # full-cache rows share state pages with contiguous KV pages, so padding
160
+ # would break page matching.
161
+ uses_fp8_ds_mla_layout = vllm_config.cache_config.cache_dtype == "fp8_ds_mla"
162
return SlidingWindowMLASpec( # only has one vector instead of K + V
163
block_size=self.block_size,
164
num_kv_heads=1,
165
head_size=self.state_dim,
166
dtype=self.dtype,
167
sliding_window=self.sliding_window,
168
- alignment=576 if is_flashmla else None,
168
+ alignment=576 if uses_fp8_ds_mla_layout else None,
169
)
170
171
def forward(self): ...
@@ -340,8 +340,8 @@ class DeepseekCompressor(nn.Module):
340
k_cache_layer = self._static_forward_context[self.k_cache_prefix]
341
kv_cache = k_cache_layer.kv_cache
342
343
- # FlashInfer V4 reads a contiguous bf16 / per-tensor fp8 cache row; the
344
- # legacy FlashMLA path uses the UE8M0 paged uint8 layout.
343
+ # Plain-row V4 reads a contiguous bf16 / per-tensor fp8 cache row; the
344
+ # fp8_ds_mla path uses the UE8M0 paged uint8 layout.
345
store_full_kv = self.head_dim == 512 and kv_cache.dtype != torch.uint8
346
store_full_fp8 = kv_cache.dtype == torch.float8_e4m3fn
347
fp8_scale = (
@@ -358,8 +358,8 @@ class DeepseekCompressor(nn.Module):
358
compress_norm_rope_store_cutedsl,
359
)
360
361
- # head=512 on CUDA always uses cutedsl, for both the legacy UE8M0
362
- # layout and the FlashInfer full-cache layout. The full-cache flags
361
+ # head=512 on CUDA always uses cutedsl, for both the fp8_ds_mla
362
+ # layout and the plain full-cache layout. The full-cache flags
363
# are consumed only here.
364
compress_norm_rope_store_fn = compress_norm_rope_store_cutedsl
365
extra_kwargs: dict[str, Any] = dict(
vllm/models/deepseek_v4/nvidia/flashinfer_sparse.py
+473
-27
@@ -1,13 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""DeepSeek V4 FlashInfer TRTLLM-gen sparse MLA backend.
4
-
5
-Uses FlashInfer's public ``trtllm_batch_decode_sparse_mla_dsv4`` launcher with a
6
-plain bf16 / per-tensor FP8 KV row (vs FlashMLA's packed ``fp8_ds_mla`` block
7
-format). Shares the V4 sparse-index pipeline (SWA cache + compressor + indexer,
8
-256-token blocks, head_size 512) with the FlashMLA V4 backend; only the
9
-attention forward differs.
10
-"""
3
+"""DeepSeek V4 FlashInfer sparse MLA backend."""
4
5
from typing import TYPE_CHECKING, ClassVar, cast
6
@@ -18,6 +11,7 @@ from vllm.forward_context import get_forward_context
11
from vllm.models.deepseek_v4.attention import DeepseekV4Attention
12
from vllm.models.deepseek_v4.common.ops import (
13
build_flashinfer_mixed_sparse_indices,
14
+ compute_global_topk_indices_and_lens,
15
)
16
from vllm.models.deepseek_v4.nvidia.ops.o_proj import (
17
compute_fp8_einsum_recipe,
@@ -27,13 +21,14 @@ from vllm.models.deepseek_v4.sparse_mla import (
21
DeepseekV4FlashMLABackend,
22
DeepseekV4FlashMLAMetadata,
23
)
24
+from vllm.platforms import current_platform
25
+from vllm.platforms.interface import DeviceCapability
26
from vllm.utils.flashinfer import flashinfer_trtllm_batch_decode_sparse_mla_dsv4
27
+from vllm.v1.attention.backend import MultipleOf
28
29
if TYPE_CHECKING:
30
from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
31
35
-# 128 MB TRTLLM-gen workspace, allocated once per device and zero-initialized
36
-# (required for first use). Reused across all FlashInfer V4 layers.
32
_FLASHINFER_DSV4_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
33
_flashinfer_dsv4_workspace_by_device: dict[torch.device, torch.Tensor] = {}
34
@@ -51,34 +46,113 @@ def _get_flashinfer_dsv4_workspace(device: torch.device) -> torch.Tensor:
46
47
48
class DeepseekV4FlashInferMLASparseBackend(DeepseekV4FlashMLABackend):
54
- """Shares the FlashMLA V4 metadata/cache pipeline; swaps the attention impl.
49
+ """FlashInfer backend using the DSv4 sparse metadata/cache layout.
50
56
- Inheriting from the FlashMLA V4 backend reuses its ``DeepseekV4FlashMLAMetadata``
57
- builder.
51
+ Inheriting from the FlashMLA V4 backend reuses its
52
+ ``DeepseekV4FlashMLAMetadata`` builder.
53
"""
54
55
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
61
- supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = ["auto", "bfloat16", "fp8"]
56
+ supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
57
+ "auto",
58
+ "bfloat16",
59
+ "fp8",
60
+ "fp8_e4m3",
61
+ "fp8_ds_mla",
62
+ ]
63
+
64
+ @staticmethod
65
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
66
+ return [256]
67
68
@staticmethod
69
def get_name() -> str:
70
return "FLASHINFER_MLA_SPARSE_DSV4"
71
72
+ @classmethod
73
+ def get_supported_head_sizes(cls) -> list[int]:
74
+ return [512]
75
+
76
+ @classmethod
77
+ def supports_sink(cls) -> bool:
78
+ return True
79
+
80
+ @classmethod
81
+ def is_sparse(cls) -> bool:
82
+ return True
83
+
84
+ @classmethod
85
+ def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
86
+ return capability.major in [10, 12]
87
+
88
+ @classmethod
89
+ def supports_combination(
90
+ cls,
91
+ head_size: int,
92
+ dtype: torch.dtype,
93
+ kv_cache_dtype: CacheDType | None,
94
+ block_size: int | None,
95
+ use_mla: bool,
96
+ has_sink: bool,
97
+ use_sparse: bool,
98
+ use_mm_prefix: bool,
99
+ device_capability: DeviceCapability,
100
+ ) -> str | None:
101
+ if device_capability.major == 10:
102
+ if kv_cache_dtype == "fp8_ds_mla":
103
+ return (
104
+ "FLASHINFER_MLA_SPARSE_DSV4 SM10x uses the plain "
105
+ "per-tensor FP8 KV layout, not fp8_ds_mla"
106
+ )
107
+ if kv_cache_dtype not in (None, "auto", "bfloat16", "fp8", "fp8_e4m3"):
108
+ return "kv_cache_dtype not supported"
109
+ return None
110
+ if device_capability.major == 12:
111
+ if kv_cache_dtype not in ("fp8", "fp8_e4m3", "fp8_ds_mla"):
112
+ return "kv_cache_dtype not supported"
113
+ from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
114
+
115
+ if not has_flashinfer_sparse_mla_sm120():
116
+ return (
117
+ "FLASHINFER_MLA_SPARSE_DSV4 SM120 requires FlashInfer's "
118
+ "sparse MLA decode API"
119
+ )
120
+ return None
121
+ return "FLASHINFER_MLA_SPARSE_DSV4 requires SM10x or SM12x"
122
+
123
+ @staticmethod
124
+ def get_kv_cache_shape(
125
+ num_blocks: int,
126
+ block_size: int,
127
+ num_kv_heads: int,
128
+ head_size: int,
129
+ cache_dtype_str: str = "auto",
130
+ ) -> tuple[int, ...]:
131
+ device_capability = current_platform.get_device_capability()
132
+ if device_capability is not None and device_capability.major == 12:
133
+ return DeepseekV4FlashMLABackend.get_kv_cache_shape(
134
+ num_blocks,
135
+ block_size,
136
+ num_kv_heads,
137
+ head_size,
138
+ cache_dtype_str,
139
+ )
140
+ assert num_kv_heads == 1
141
+ return (num_blocks, block_size, head_size)
142
+
143
144
class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
69
- """FlashInfer TRTLLM-gen sparse MLA attention layer for DeepSeek V4."""
145
+ """FlashInfer TRTLLM-gen sparse MLA attention layer for SM100 DeepSeek V4."""
146
147
backend_cls = DeepseekV4FlashInferMLASparseBackend
72
- # FlashInfer stores a plain bf16 / per-tensor fp8 KV row, not the FlashMLA
73
- # packed fp8_ds_mla block format (UE8M0 block-scaled fp8 as uint8).
74
- use_flashmla_fp8_layout: ClassVar[bool] = False
148
+ use_fp8_ds_mla_layout: ClassVar[bool] = False
149
150
@classmethod
151
def get_padded_num_q_heads(cls, num_heads: int) -> int:
152
# FP8 decode kernel only supports h_q = 64 or 128.
153
if num_heads > 128:
154
raise ValueError(
81
- f"DeepseekV4 Flashinfer MLA Sparse does not support {num_heads} heads "
155
+ f"DeepseekV4 FlashInfer MLA Sparse does not support {num_heads} heads "
156
"(FP8 decode kernel requires h_q in {64, 128})."
157
)
158
return 64 if num_heads <= 64 else 128
@@ -106,8 +180,6 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
180
# per-tensor FP8 cache path consumes these; bf16 reads ``self.scale``.
181
if self.kv_cache_torch_dtype != torch.float8_e4m3fn:
182
return
109
- # TODO: load real per-tensor Q/KV scales from the checkpoint; unit
110
- # scales until the scale tensor names are wired.
183
fp8_q_scale = 1.0
184
fp8_kv_scale = 1.0
185
self.register_buffer(
@@ -125,9 +197,8 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
197
torch.tensor([fp8_kv_scale], dtype=torch.float32),
198
persistent=False,
199
)
128
- # TRTLLM-gen takes scalar scale args on a distinct (correct) C++ path
129
- # vs 1-elem tensors, so these are Python floats. bmm1 folds the softmax
130
- # scale and the Q/KV per-tensor scales; bmm2 is the KV scale.
200
+ # TRTLLM-gen takes scalar scale args on a distinct C++ path vs
201
+ # one-element tensors, so these are Python floats.
202
self._flashinfer_fp8_bmm1_scale = self.scale * fp8_q_scale * fp8_kv_scale
203
self._flashinfer_fp8_bmm2_scale = fp8_kv_scale
204
@@ -387,9 +458,8 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
458
query_start_loc_cpu = swa_metadata.query_start_loc_cpu
459
assert query_start_loc is not None and query_start_loc_cpu is not None
460
390
- # Keep Perkz's two-call decode/prefill split: the TRTLLM-gen launcher is
391
- # tuned for uniform-q batches, and collapsing the mixed batch into a
392
- # single call is the suspected source of the prior IMA.
461
+ # Keep the TRTLLM-gen decode/prefill split: the launcher is tuned for
462
+ # uniform-q batches, and this avoids flattening mixed batches into one call.
463
if num_decode_tokens > 0:
464
decode_cu = query_start_loc[: num_decodes + 1]
465
decode_cu_cpu = query_start_loc_cpu[: num_decodes + 1]
@@ -434,3 +504,379 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
504
cum_seq_lens_q=prefill_cu,
505
max_q_len=int(prefill_lens_cpu.max().item()),
506
)
507
+
508
+
509
+class DeepseekV4FlashInferSM120Attention(DeepseekV4Attention):
510
+ """DeepSeek V4 sparse MLA attention through FlashInfer's SM120 kernels."""
511
+
512
+ backend_cls = DeepseekV4FlashInferMLASparseBackend
513
+ use_fp8_ds_mla_layout: ClassVar[bool] = True
514
+
515
+ @staticmethod
516
+ def _get_workspace(device: torch.device) -> torch.Tensor:
517
+ return _get_flashinfer_dsv4_workspace(device)
518
+
519
+ @staticmethod
520
+ def _as_sparse_cache(kv_cache: torch.Tensor) -> torch.Tensor:
521
+ if kv_cache.dtype == torch.float8_e4m3fn:
522
+ kv_cache = kv_cache.view(torch.uint8)
523
+ if kv_cache.dim() == 4:
524
+ return kv_cache
525
+ return kv_cache.unsqueeze(-2)
526
+
527
+ @classmethod
528
+ def get_padded_num_q_heads(cls, num_heads: int) -> int:
529
+ if num_heads <= 16:
530
+ return 16
531
+ if num_heads <= 32:
532
+ return 32
533
+ if num_heads <= 64:
534
+ return 64
535
+ if num_heads <= 128:
536
+ return 128
537
+ raise ValueError(
538
+ f"DeepseekV4 FlashInfer MLA Sparse does not support {num_heads} heads "
539
+ "(SM120 kernel requires h_q in {16, 32, 64, 128})."
540
+ )
541
+
542
+ def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
543
+ return deep_gemm_fp8_o_proj(
544
+ o,
545
+ positions,
546
+ self.rotary_emb.cos_sin_cache,
547
+ self.wo_a,
548
+ self.wo_b,
549
+ n_groups=self.n_local_groups,
550
+ heads_per_group=self.n_local_heads // self.n_local_groups,
551
+ nope_dim=self.nope_head_dim,
552
+ rope_dim=self.rope_head_dim,
553
+ o_lora_rank=self.o_lora_rank,
554
+ einsum_recipe=self._einsum_recipe,
555
+ tma_aligned_scales=self._tma_aligned_scales,
556
+ )
557
+
558
+ def __init__(self, *args, **kwargs) -> None:
559
+ super().__init__(*args, **kwargs)
560
+ from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
561
+
562
+ if not has_flashinfer_sparse_mla_sm120():
563
+ raise RuntimeError(
564
+ "FLASHINFER_MLA_SPARSE_DSV4 on SM120 requires FlashInfer's "
565
+ "sparse MLA decode API."
566
+ )
567
+ self._einsum_recipe, self._tma_aligned_scales = compute_fp8_einsum_recipe()
568
+ # Per-tensor FP8 cache path scales.
569
+ if self.kv_cache_torch_dtype != torch.float8_e4m3fn:
570
+ return
571
+ fp8_q_scale = 1.0
572
+ fp8_kv_scale = 1.0
573
+ self.register_buffer(
574
+ "_flashinfer_fp8_q_scale",
575
+ torch.tensor([fp8_q_scale], dtype=torch.float32),
576
+ persistent=False,
577
+ )
578
+ self.register_buffer(
579
+ "_flashinfer_fp8_q_scale_inv",
580
+ torch.tensor([1.0 / fp8_q_scale], dtype=torch.float32),
581
+ persistent=False,
582
+ )
583
+ self.register_buffer(
584
+ "_flashinfer_fp8_kv_scale",
585
+ torch.tensor([fp8_kv_scale], dtype=torch.float32),
586
+ persistent=False,
587
+ )
588
+ # FlashInfer expects scalar scale arguments for this path.
589
+ self._flashinfer_fp8_bmm1_scale = self.scale * fp8_q_scale * fp8_kv_scale
590
+ self._flashinfer_fp8_bmm2_scale = fp8_kv_scale
591
+
592
+ def _reserve_empty_forward_workspace(self) -> None:
593
+ self._get_workspace(
594
+ torch.device("cuda", torch.accelerator.current_device_index())
595
+ )
596
+
597
+ def _forward_sparse_impl(
598
+ self,
599
+ q: torch.Tensor,
600
+ output: torch.Tensor,
601
+ flashmla_metadata: DeepseekV4FlashMLAMetadata | None,
602
+ swa_metadata: "DeepseekSparseSWAMetadata",
603
+ self_kv_cache: torch.Tensor | None,
604
+ swa_kv_cache: torch.Tensor,
605
+ swa_only: bool,
606
+ ) -> None:
607
+ num_decode_tokens = swa_metadata.num_decode_tokens
608
+ if swa_metadata.num_prefills > 0:
609
+ self._forward_prefill(
610
+ q=q[num_decode_tokens:],
611
+ compressed_k_cache=self_kv_cache,
612
+ swa_k_cache=swa_kv_cache,
613
+ output=output[num_decode_tokens:],
614
+ attn_metadata=flashmla_metadata,
615
+ swa_metadata=swa_metadata,
616
+ )
617
+ if swa_metadata.num_decodes > 0:
618
+ self._forward_decode(
619
+ q=q[:num_decode_tokens],
620
+ kv_cache=self_kv_cache,
621
+ swa_metadata=swa_metadata,
622
+ attn_metadata=flashmla_metadata,
623
+ swa_only=swa_only,
624
+ output=output[:num_decode_tokens],
625
+ )
626
+
627
+ def forward_mqa(
628
+ self,
629
+ q: torch.Tensor,
630
+ kv: torch.Tensor,
631
+ positions: torch.Tensor,
632
+ output: torch.Tensor,
633
+ ) -> None:
634
+ # Output may be padded to backend-supported head counts.
635
+ assert output.shape[0] == q.shape[0] and output.shape[-1] == q.shape[-1], (
636
+ f"output buffer shape {output.shape} incompatible with q shape {q.shape}"
637
+ )
638
+ assert output.shape[1] >= q.shape[1], (
639
+ f"output heads {output.shape[1]} must be >= q heads {q.shape[1]}"
640
+ )
641
+ # Per-tensor FP8 q produces a bf16 attention output.
642
+ expected_output_dtype = (
643
+ torch.bfloat16 if q.dtype == torch.float8_e4m3fn else q.dtype
644
+ )
645
+ assert output.dtype == expected_output_dtype, (
646
+ f"output dtype {output.dtype} must match expected {expected_output_dtype} "
647
+ f"for q dtype {q.dtype}"
648
+ )
649
+
650
+ forward_context = get_forward_context()
651
+ attn_metadata = forward_context.attn_metadata
652
+ if attn_metadata is None:
653
+ self._reserve_empty_forward_workspace()
654
+ output.zero_()
655
+ return
656
+
657
+ assert isinstance(attn_metadata, dict)
658
+ flashmla_metadata = cast(
659
+ DeepseekV4FlashMLAMetadata | None, attn_metadata.get(self.prefix)
660
+ )
661
+ swa_metadata = cast(
662
+ "DeepseekSparseSWAMetadata | None",
663
+ attn_metadata.get(self.swa_cache_layer.prefix),
664
+ )
665
+ assert swa_metadata is not None
666
+
667
+ swa_only = self.compress_ratio <= 1
668
+ # SWA-only layers don't allocate their own compressed KV cache.
669
+ self_kv_cache = self.kv_cache if not swa_only else None
670
+ swa_kv_cache = self.swa_cache_layer.kv_cache
671
+
672
+ self._forward_sparse_impl(
673
+ q=q,
674
+ output=output,
675
+ flashmla_metadata=flashmla_metadata,
676
+ swa_metadata=swa_metadata,
677
+ self_kv_cache=self_kv_cache,
678
+ swa_kv_cache=swa_kv_cache,
679
+ swa_only=swa_only,
680
+ )
681
+
682
+ def _prepare_query(self, q: torch.Tensor, output: torch.Tensor) -> torch.Tensor:
683
+ if self.kv_cache_torch_dtype == torch.float8_e4m3fn:
684
+ assert q.dtype == torch.float8_e4m3fn
685
+ q = q.to(torch.bfloat16)
686
+ else:
687
+ assert q.dtype == torch.bfloat16
688
+ padded_heads = output.shape[1]
689
+ if q.shape[1] < padded_heads:
690
+ padded_query = q.new_zeros((q.shape[0], padded_heads, q.shape[2]))
691
+ padded_query[:, : q.shape[1], :] = q
692
+ q = padded_query
693
+ return q.contiguous()
694
+
695
+ def _forward_decode(
696
+ self,
697
+ q: torch.Tensor,
698
+ kv_cache: torch.Tensor | None,
699
+ swa_metadata: "DeepseekSparseSWAMetadata",
700
+ attn_metadata: DeepseekV4FlashMLAMetadata | None,
701
+ swa_only: bool,
702
+ output: torch.Tensor,
703
+ ) -> None:
704
+ num_decodes = swa_metadata.num_decodes
705
+ num_decode_tokens = swa_metadata.num_decode_tokens
706
+
707
+ extra_sparse_indices = None
708
+ extra_sparse_lengths = None
709
+ if not swa_only:
710
+ if attn_metadata is None:
711
+ raise RuntimeError(
712
+ "Sparse MLA metadata is required for compressed layers."
713
+ )
714
+ if swa_metadata.is_valid_token is None:
715
+ raise RuntimeError(
716
+ "SWA validity metadata is required for compressed layers."
717
+ )
718
+ is_valid = swa_metadata.is_valid_token[:num_decode_tokens]
719
+ if self.compress_ratio == 4:
720
+ if self.topk_indices_buffer is None:
721
+ raise RuntimeError(
722
+ "C4A decode requires top-k indices from the indexer."
723
+ )
724
+ block_size = attn_metadata.block_size // self.compress_ratio
725
+ global_indices, extra_sparse_lengths = (
726
+ compute_global_topk_indices_and_lens(
727
+ self.topk_indices_buffer[:num_decode_tokens],
728
+ swa_metadata.token_to_req_indices,
729
+ attn_metadata.block_table[:num_decodes],
730
+ block_size,
731
+ is_valid,
732
+ )
733
+ )
734
+ extra_sparse_indices = global_indices.view(num_decode_tokens, 1, -1)
735
+ else:
736
+ extra_sparse_indices = attn_metadata.c128a_global_decode_topk_indices
737
+ extra_sparse_lengths = attn_metadata.c128a_decode_topk_lens
738
+
739
+ swa_indices = swa_metadata.decode_swa_indices
740
+ swa_lens = swa_metadata.decode_swa_lens
741
+ assert swa_indices is not None
742
+ assert swa_lens is not None
743
+ q = self._prepare_query(q, output)
744
+ swa_cache = self._as_sparse_cache(self.swa_cache_layer.kv_cache)
745
+ extra_cache = self._as_sparse_cache(kv_cache) if kv_cache is not None else None
746
+ if extra_cache is not None and extra_sparse_indices is None:
747
+ raise RuntimeError(
748
+ "Compressed sparse MLA decode requires compressed sparse indices."
749
+ )
750
+ flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
751
+ query=q,
752
+ swa_kv_cache=swa_cache,
753
+ workspace_buffer=self._get_workspace(q.device),
754
+ sparse_indices=swa_indices,
755
+ compressed_kv_cache=extra_cache,
756
+ out=output,
757
+ bmm1_scale=self.scale,
758
+ sinks=self.attn_sink,
759
+ kv_layout="NHD",
760
+ swa_topk_lens=swa_lens,
761
+ extra_sparse_indices=extra_sparse_indices,
762
+ extra_sparse_topk_lens=extra_sparse_lengths,
763
+ )
764
+
765
+ def _forward_prefill(
766
+ self,
767
+ q: torch.Tensor,
768
+ compressed_k_cache: torch.Tensor | None,
769
+ swa_k_cache: torch.Tensor,
770
+ output: torch.Tensor,
771
+ attn_metadata: DeepseekV4FlashMLAMetadata | None,
772
+ swa_metadata: "DeepseekSparseSWAMetadata",
773
+ ) -> None:
774
+ swa_only = self.compress_ratio <= 1
775
+
776
+ num_prefills = swa_metadata.num_prefills
777
+ num_decodes = swa_metadata.num_decodes
778
+ num_decode_tokens = swa_metadata.num_decode_tokens
779
+ num_prefill_tokens = swa_metadata.num_prefill_tokens
780
+
781
+ query_start_loc_cpu = swa_metadata.query_start_loc_cpu
782
+ assert query_start_loc_cpu is not None
783
+ prefill_token_base = query_start_loc_cpu[num_decodes]
784
+
785
+ local_topk_indices: torch.Tensor | None
786
+ if swa_only:
787
+ local_topk_indices = None
788
+ elif self.compress_ratio == 4:
789
+ if self.topk_indices_buffer is None:
790
+ raise RuntimeError(
791
+ "C4A prefill requires top-k indices from the indexer."
792
+ )
793
+ local_topk_indices = self.topk_indices_buffer[
794
+ num_decode_tokens : num_decode_tokens + num_prefill_tokens
795
+ ]
796
+ else:
797
+ if attn_metadata is None:
798
+ raise RuntimeError("C128A prefill metadata is missing.")
799
+ local_topk_indices = attn_metadata.c128a_prefill_topk_indices
800
+
801
+ extra_sparse_indices: torch.Tensor | None = None
802
+ extra_sparse_lengths: torch.Tensor | None = None
803
+ if local_topk_indices is not None:
804
+ if attn_metadata is None:
805
+ raise RuntimeError("C4A prefill metadata is missing.")
806
+ if swa_metadata.token_to_req_indices is None:
807
+ raise RuntimeError("C4A prefill request mapping is missing.")
808
+ if swa_metadata.is_valid_token is None:
809
+ raise RuntimeError("C4A prefill validity metadata is missing.")
810
+ prefill_token_slice = slice(
811
+ num_decode_tokens, num_decode_tokens + num_prefill_tokens
812
+ )
813
+ block_size = attn_metadata.block_size // self.compress_ratio
814
+ extra_sparse_indices, extra_sparse_lengths = (
815
+ compute_global_topk_indices_and_lens(
816
+ local_topk_indices,
817
+ swa_metadata.token_to_req_indices[prefill_token_slice],
818
+ attn_metadata.block_table,
819
+ block_size,
820
+ swa_metadata.is_valid_token[prefill_token_slice],
821
+ )
822
+ )
823
+
824
+ assert swa_metadata.prefill_swa_indices is not None
825
+ assert swa_metadata.prefill_swa_lens is not None
826
+
827
+ q = self._prepare_query(q, output)
828
+ swa_kv_paged = self._as_sparse_cache(swa_k_cache)
829
+ if swa_only:
830
+ extra_kv_paged = None
831
+ else:
832
+ if compressed_k_cache is None:
833
+ raise RuntimeError(
834
+ "Compressed sparse MLA layers require their compressed KV cache."
835
+ )
836
+ extra_kv_paged = self._as_sparse_cache(compressed_k_cache)
837
+
838
+ num_chunks = (
839
+ num_prefills + self.PREFILL_CHUNK_SIZE - 1
840
+ ) // self.PREFILL_CHUNK_SIZE
841
+ for chunk_idx in range(num_chunks):
842
+ chunk_start = chunk_idx * self.PREFILL_CHUNK_SIZE
843
+ chunk_end = min(chunk_start + self.PREFILL_CHUNK_SIZE, num_prefills)
844
+ query_start = (
845
+ query_start_loc_cpu[num_decodes + chunk_start] - prefill_token_base
846
+ )
847
+ query_end = (
848
+ query_start_loc_cpu[num_decodes + chunk_end] - prefill_token_base
849
+ )
850
+
851
+ extra_sparse_indices_chunk = (
852
+ extra_sparse_indices[query_start:query_end]
853
+ if extra_sparse_indices is not None
854
+ else None
855
+ )
856
+ extra_sparse_lengths_chunk = (
857
+ extra_sparse_lengths[query_start:query_end]
858
+ if extra_sparse_lengths is not None
859
+ else None
860
+ )
861
+
862
+ q_chunk = q[query_start:query_end]
863
+ swa_indices_chunk = swa_metadata.prefill_swa_indices[query_start:query_end]
864
+ swa_lens_chunk = swa_metadata.prefill_swa_lens[query_start:query_end]
865
+ if extra_kv_paged is not None and extra_sparse_indices_chunk is None:
866
+ raise RuntimeError(
867
+ "Compressed sparse MLA prefill requires compressed sparse indices."
868
+ )
869
+ flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
870
+ query=q_chunk,
871
+ swa_kv_cache=swa_kv_paged,
872
+ workspace_buffer=self._get_workspace(q.device),
873
+ sparse_indices=swa_indices_chunk,
874
+ compressed_kv_cache=extra_kv_paged,
875
+ out=output[query_start:query_end],
876
+ bmm1_scale=self.scale,
877
+ sinks=self.attn_sink,
878
+ kv_layout="NHD",
879
+ swa_topk_lens=swa_lens_chunk,
880
+ extra_sparse_indices=extra_sparse_indices_chunk,
881
+ extra_sparse_topk_lens=extra_sparse_lengths_chunk,
882
+ )
vllm/models/deepseek_v4/nvidia/model.py
+27
-5
@@ -60,9 +60,11 @@ from vllm.model_executor.utils import set_weight_attrs
60
from vllm.models.deepseek_v4.attention import DeepseekV4Attention
61
from vllm.models.deepseek_v4.nvidia.flashinfer_sparse import (
62
DeepseekV4FlashInferMLAAttention,
63
+ DeepseekV4FlashInferSM120Attention,
64
)
65
from vllm.models.deepseek_v4.nvidia.flashmla import DeepseekV4FlashMLAAttention
66
from vllm.models.deepseek_v4.nvidia.ops.prepare_megamoe import prepare_megamoe_inputs
67
+from vllm.platforms import current_platform
68
from vllm.sequence import IntermediateTensors
69
from vllm.utils.math_utils import cdiv
70
from vllm.v1.attention.backends.registry import AttentionBackendEnum
@@ -736,14 +738,34 @@ class DeepseekV4MoE(nn.Module):
738
def _select_dsv4_attn_cls(vllm_config: VllmConfig) -> type[DeepseekV4Attention]:
739
"""Pick the CUDA sparse-MLA attention class for the configured backend.
740
739
- An explicit ``--attention-backend FLASHINFER_MLA_SPARSE_DSV4`` selects the
740
- FlashInfer TRTLLM-gen path; otherwise the FlashMLA path is used.
741
+ The generic CUDA backend selector does not instantiate DSv4 layers directly,
742
+ so map generic sparse-MLA choices to the DSv4-specialized attention class.
743
+ Without an explicit backend, SM12 defaults to FlashInfer while the other
744
+ CUDA arches keep the FlashMLA path.
745
"""
742
- if (
743
- vllm_config.attention_config.backend
744
- == AttentionBackendEnum.FLASHINFER_MLA_SPARSE_DSV4
746
+ backend = vllm_config.attention_config.backend
747
+ device_capability = current_platform.get_device_capability()
748
+ if backend in (
749
+ AttentionBackendEnum.FLASHINFER_MLA_SPARSE,
750
+ AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120,
751
):
752
+ raise ValueError(
753
+ f"{backend.name} is not a DeepSeek V4 attention backend. "
754
+ "Use FLASHINFER_MLA_SPARSE_DSV4 for DeepSeek V4 FlashInfer "
755
+ "sparse MLA."
756
+ )
757
+ if backend == AttentionBackendEnum.FLASHINFER_MLA_SPARSE_DSV4:
758
+ if device_capability is not None and device_capability.major == 12:
759
+ return DeepseekV4FlashInferSM120Attention
760
return DeepseekV4FlashInferMLAAttention
761
+ if backend in (
762
+ AttentionBackendEnum.FLASHMLA_SPARSE,
763
+ AttentionBackendEnum.FLASHMLA_SPARSE_DSV4,
764
+ ):
765
+ return DeepseekV4FlashMLAAttention
766
+
767
+ if device_capability is not None and device_capability.major == 12:
768
+ return DeepseekV4FlashInferSM120Attention
769
return DeepseekV4FlashMLAAttention
770
771
vllm/models/deepseek_v4/sparse_mla.py
+4
@@ -86,6 +86,10 @@ class DeepseekV4FlashMLABackend(AttentionBackend):
86
def is_sparse(cls) -> bool:
87
return True
88
89
+ @classmethod
90
+ def supports_sink(cls) -> bool:
91
+ return True
92
+
93
@classmethod
94
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
95
return capability.major in [9, 10]
vllm/platforms/cuda.py
+46
-15
@@ -11,7 +11,7 @@ import platform
11
from collections.abc import Callable
12
from datetime import timedelta
13
from functools import cache, lru_cache, wraps
14
-from typing import TYPE_CHECKING, TypeVar
14
+from typing import TYPE_CHECKING, NamedTuple, TypeVar
15
16
import torch
17
from torch.distributed import PrefixStore, ProcessGroup
@@ -31,6 +31,7 @@ if TYPE_CHECKING:
31
from vllm.config import VllmConfig
32
from vllm.config.cache import CacheDType
33
from vllm.config.kernel import IrOpPriorityConfig
34
+ from vllm.v1.attention.backend import AttentionBackend
35
from vllm.v1.attention.selector import AttentionSelectorConfig
36
else:
37
VllmConfig = None
@@ -126,6 +127,11 @@ def _get_backend_priorities(
127
AttentionBackendEnum.TRITON_MLA,
128
*sparse_backends,
129
]
130
+ elif device_capability.major == 12:
131
+ return [
132
+ AttentionBackendEnum.TRITON_MLA,
133
+ AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120,
134
+ ]
135
else:
136
return [
137
AttentionBackendEnum.FLASH_ATTN_MLA,
@@ -153,6 +159,21 @@ def _get_backend_priorities(
159
]
160
161
162
+def _backend_cls_path(backend_cls: type[AttentionBackend]) -> str:
163
+ module, qualname = backend_cls.full_cls_name()
164
+ return f"{module}.{qualname}"
165
+
166
+
167
+def _get_attn_backend_class(backend: AttentionBackendEnum) -> type[AttentionBackend]:
168
+ return backend.get_class()
169
+
170
+
171
+class _BackendCandidate(NamedTuple):
172
+ backend_class: type[AttentionBackend]
173
+ backend: AttentionBackendEnum
174
+ priority: int
175
+
176
+
177
def with_nvml_context(fn: Callable[_P, _R]) -> Callable[_P, _R]:
178
@wraps(fn)
179
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
@@ -340,7 +361,7 @@ class CudaPlatformBase(Platform):
361
attn_selector_config: AttentionSelectorConfig,
362
num_heads: int | None = None,
363
) -> tuple[
343
- list[tuple[AttentionBackendEnum, int]],
364
+ list[_BackendCandidate],
365
dict[AttentionBackendEnum, tuple[int, list[str]]],
366
]:
367
valid_backends_priorities = []
@@ -354,7 +375,7 @@ class CudaPlatformBase(Platform):
375
)
376
for priority, backend in enumerate(backend_priorities):
377
try:
357
- backend_class = backend.get_class()
378
+ backend_class = _get_attn_backend_class(backend)
379
invalid_reasons_i = backend_class.validate_configuration(
380
device_capability=device_capability,
381
**attn_selector_config._asdict(),
@@ -364,7 +385,9 @@ class CudaPlatformBase(Platform):
385
if invalid_reasons_i:
386
invalid_reasons[backend] = (priority, invalid_reasons_i)
387
else:
367
- valid_backends_priorities.append((backend, priority))
388
+ valid_backends_priorities.append(
389
+ _BackendCandidate(backend_class, backend, priority)
390
+ )
391
392
return valid_backends_priorities, invalid_reasons
393
@@ -381,7 +404,7 @@ class CudaPlatformBase(Platform):
404
# First try checking just the selected backend, if there is one.
405
if selected_backend is not None:
406
try:
384
- backend_class = selected_backend.get_class()
407
+ backend_class = _get_attn_backend_class(selected_backend)
408
invalid_reasons = backend_class.validate_configuration(
409
device_capability=device_capability,
410
**attn_selector_config._asdict(),
@@ -395,7 +418,7 @@ class CudaPlatformBase(Platform):
418
)
419
else:
420
logger.info("Using %s backend.", selected_backend)
398
- return selected_backend.get_path()
421
+ return _backend_cls_path(backend_class)
422
423
# No selected backend or the selected backend is invalid,
424
# so we try finding a valid backend.
@@ -425,13 +448,13 @@ class CudaPlatformBase(Platform):
448
449
# We have found some valid backends. Select the one with the
450
# highest priority.
428
- sorted_indices = sorted(
429
- range(len(valid_backends_priorities)),
430
- key=lambda i: valid_backends_priorities[i][1],
451
+ selected_candidate = min(
452
+ valid_backends_priorities,
453
+ key=lambda candidate: candidate.priority,
454
)
432
- selected_index = sorted_indices[0]
433
- selected_backend = valid_backends_priorities[selected_index][0]
434
- selected_priority = valid_backends_priorities[selected_index][1]
455
+ selected_backend_class = selected_candidate.backend_class
456
+ selected_backend = selected_candidate.backend
457
+ selected_priority = selected_candidate.priority
458
459
# If the user specified --block-size (but not --attention-backend),
460
# check whether that constraint precluded any higher-priority backends.
@@ -457,10 +480,14 @@ class CudaPlatformBase(Platform):
480
logger.info_once(
481
"Using %s attention backend out of potential backends: %s.",
482
selected_backend.name,
460
- "[" + ", ".join(f"'{b[0].name}'" for b in valid_backends_priorities) + "]",
483
+ "["
484
+ + ", ".join(
485
+ f"'{candidate.backend.name}'" for candidate in valid_backends_priorities
486
+ )
487
+ + "]",
488
)
489
463
- return selected_backend.get_path()
490
+ return _backend_cls_path(selected_backend_class)
491
492
@classmethod
493
def get_supported_vit_attn_backends(cls) -> list[AttentionBackendEnum]:
@@ -635,7 +662,11 @@ class CudaPlatformBase(Platform):
662
@classmethod
663
def support_deep_gemm(cls) -> bool:
664
"""Currently, only Hopper and Blackwell GPUs are supported."""
638
- return cls.is_device_capability(90) or cls.is_device_capability_family(100)
665
+ return (
666
+ cls.is_device_capability(90)
667
+ or cls.is_device_capability_family(100)
668
+ or cls.is_device_capability_family(120)
669
+ )
670
671
@classmethod
672
def is_integrated_gpu(cls, device_id: int = 0) -> bool:
vllm/utils/deep_gemm.py
+144
-3
@@ -5,6 +5,7 @@
5
Users of vLLM should always import **only** these wrappers.
6
"""
7
8
+import contextlib
9
import functools
10
import importlib
11
import os
@@ -37,7 +38,10 @@ def should_auto_disable_deep_gemm(model_type: str | None) -> bool:
38
"""
39
if model_type is None:
40
return False
40
- if not current_platform.is_device_capability_family(100):
41
+ if not (
42
+ current_platform.is_device_capability_family(100)
43
+ or current_platform.is_device_capability_family(120)
44
+ ):
45
return False
46
return model_type in _DEEPGEMM_BLACKWELL_EXCLUDED_MODEL_TYPES
47
@@ -71,7 +75,10 @@ class DeepGemmQuantScaleFMT(Enum):
75
76
cls._oracle_cache = ( # type: ignore
77
cls.UE8M0
74
- if current_platform.is_device_capability_family(100)
78
+ if (
79
+ current_platform.is_device_capability_family(100)
80
+ or current_platform.is_device_capability_family(120)
81
+ )
82
else cls.FLOAT32_CEIL_UE8M0
83
)
84
@@ -138,7 +145,15 @@ _get_paged_mqa_logits_metadata_impl: Callable[..., Any] | None = None
145
_tf32_hc_prenorm_gemm_impl: Callable[..., Any] | None = None
146
_get_mn_major_tma_aligned_tensor_impl: Callable[..., Any] | None = None
147
_get_mk_alignment_for_contiguous_layout_impl: Callable[..., Any] | None = None
148
+_get_theoretical_mk_alignment_for_contiguous_layout_impl: Callable[..., Any] | None = (
149
+ None
150
+)
151
_transform_sf_into_required_layout_impl: Callable[..., Any] | None = None
152
+_pack_ue8m0_to_int_impl: Callable[..., Any] | None = None
153
+_get_mn_major_tma_aligned_packed_ue8m0_tensor_impl: Callable[..., Any] | None = None
154
+_get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl: (
155
+ Callable[..., Any] | None
156
+) = None
157
158
159
@functools.cache
@@ -203,7 +218,11 @@ def _lazy_init() -> None:
218
global _tf32_hc_prenorm_gemm_impl
219
global _get_mn_major_tma_aligned_tensor_impl
220
global _get_mk_alignment_for_contiguous_layout_impl
221
+ global _get_theoretical_mk_alignment_for_contiguous_layout_impl
222
global _transform_sf_into_required_layout_impl
223
+ global _pack_ue8m0_to_int_impl
224
+ global _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl
225
+ global _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl
226
# fast path
227
if (
228
_cublaslt_gemm_nt_impl is not None
@@ -218,6 +237,9 @@ def _lazy_init() -> None:
237
or _tf32_hc_prenorm_gemm_impl is not None
238
or _get_mk_alignment_for_contiguous_layout_impl is not None
239
or _transform_sf_into_required_layout_impl is not None
240
+ or _pack_ue8m0_to_int_impl is not None
241
+ or _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl is not None
242
+ or _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl is not None
243
):
244
return
245
@@ -258,9 +280,19 @@ def _lazy_init() -> None:
280
_get_mk_alignment_for_contiguous_layout_impl = getattr(
281
_dg, "get_mk_alignment_for_contiguous_layout", None
282
)
283
+ _get_theoretical_mk_alignment_for_contiguous_layout_impl = getattr(
284
+ _dg, "get_theoretical_mk_alignment_for_contiguous_layout", None
285
+ )
286
_transform_sf_into_required_layout_impl = getattr(
287
_dg, "transform_sf_into_required_layout", None
288
)
289
+ _pack_ue8m0_to_int_impl = getattr(_dg, "pack_ue8m0_to_int", None)
290
+ _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl = getattr(
291
+ _dg, "get_mn_major_tma_aligned_packed_ue8m0_tensor", None
292
+ )
293
+ _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl = getattr(
294
+ _dg, "get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor", None
295
+ )
296
DeepGemmQuantScaleFMT.init_oracle_cache()
297
298
@@ -280,7 +312,6 @@ def set_num_sms(num_sms: int) -> None:
312
dg.set_num_sms(num_sms)
313
314
283
-@functools.cache
315
def get_mk_alignment_for_contiguous_layout() -> list[int]:
316
_lazy_init()
317
if _get_mk_alignment_for_contiguous_layout_impl is None:
@@ -289,6 +320,70 @@ def get_mk_alignment_for_contiguous_layout() -> list[int]:
320
return [mk_align_size, mk_align_size]
321
322
323
+def get_theoretical_mk_alignment_for_contiguous_layout(
324
+ expected_m: int | None = None,
325
+ num_groups: int | None = None,
326
+) -> int:
327
+ """Per-call optimal M alignment for grouped contiguous GEMMs.
328
+
329
+ `expected_m` is the TOTAL routed tokens (sum across experts, typically
330
+ M × num_topk). `num_groups` is the number of experts on this rank.
331
+ The helper divides to recover per-expert em and picks an alignment based
332
+ on data-driven thresholds (see deep_gemm runtime.hpp comments).
333
+
334
+ Older callers that omit `num_groups` are interpreted as passing already
335
+ per-expert em (legacy behaviour preserved for backward compat).
336
+ """
337
+ _lazy_init()
338
+ if _get_theoretical_mk_alignment_for_contiguous_layout_impl is None:
339
+ return _missing()
340
+ if num_groups is None:
341
+ return _get_theoretical_mk_alignment_for_contiguous_layout_impl(expected_m)
342
+ if num_groups <= 0:
343
+ raise ValueError(f"num_groups must be positive, got {num_groups}")
344
+ try:
345
+ return _get_theoretical_mk_alignment_for_contiguous_layout_impl(
346
+ expected_m, num_groups
347
+ )
348
+ except TypeError:
349
+ per_group_m = None if expected_m is None else cdiv(expected_m, num_groups)
350
+ return _get_theoretical_mk_alignment_for_contiguous_layout_impl(per_group_m)
351
+
352
+
353
+def set_mk_alignment_for_contiguous_layout(value: int) -> None:
354
+ """Set DeepGEMM's BLOCK_M cap for grouped contiguous GEMMs.
355
+
356
+ The DG heuristic constrains BLOCK_M ≤ this value when picking a kernel
357
+ layout. Use this in concert with `compute_aligned_M_and_alignment`'s
358
+ per-call alignment so the workspace's per-expert padding matches the
359
+ kernel's BLOCK_M; a mismatch leads to the scheduler reading the wrong
360
+ expert_id from `m_indices` at `m_block_idx * BLOCK_M` stride and
361
+ OOB-indexing the B-weights tensor (manifests as IMA under CUDA-graph
362
+ replay).
363
+ """
364
+ _lazy_init()
365
+ dg = _import_deep_gemm()
366
+ if dg is None:
367
+ raise RuntimeError("DeepGEMM is not available")
368
+ dg.set_mk_alignment_for_contiguous_layout(value)
369
+
370
+
371
+@contextlib.contextmanager
372
+def mk_alignment_scope(value: int):
373
+ """Temporarily set DeepGEMM's BLOCK_M cap, restoring on exit.
374
+
375
+ Use around a sequence of grouped-contiguous GEMM calls whose workspace
376
+ is padded to `value` (typically the per_call_align returned by
377
+ `compute_aligned_M_and_alignment`).
378
+ """
379
+ prev = get_mk_alignment_for_contiguous_layout()[0]
380
+ set_mk_alignment_for_contiguous_layout(value)
381
+ try:
382
+ yield
383
+ finally:
384
+ set_mk_alignment_for_contiguous_layout(prev)
385
+
386
+
387
def get_col_major_tma_aligned_tensor(x: torch.Tensor) -> torch.Tensor:
388
"""Wrapper for DeepGEMM's get_mn_major_tma_aligned_tensor"""
389
_lazy_init()
@@ -297,6 +392,48 @@ def get_col_major_tma_aligned_tensor(x: torch.Tensor) -> torch.Tensor:
392
return _get_mn_major_tma_aligned_tensor_impl(x)
393
394
395
+def pack_ue8m0_to_int(x: torch.Tensor) -> torch.Tensor:
396
+ """Pack 4 UE8M0 (uint8) scales into one int32.
397
+
398
+ DeepGEMM's SM100/SM120 FP8/FP4 kernels accept either ``float32`` scales
399
+ (legacy format, 4 B/scale) or ``int32`` packed UE8M0 scales (1 B/scale
400
+ after 4:1 packing — 4× smaller than the legacy fp32 representation).
401
+ """
402
+ _lazy_init()
403
+ if _pack_ue8m0_to_int_impl is None:
404
+ return _missing()
405
+ return _pack_ue8m0_to_int_impl(x)
406
+
407
+
408
+def get_mn_major_tma_aligned_packed_ue8m0_tensor(x: torch.Tensor) -> torch.Tensor:
409
+ """Pack UE8M0 (uint8) → int32 with the MN-major TMA-aligned layout the
410
+ DeepGEMM kernels consume directly. 16× smaller than the fp32 legacy SF
411
+ format. Use for non-grouped 2D scale tensors.
412
+ """
413
+ _lazy_init()
414
+ if _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl is None:
415
+ return _missing()
416
+ return _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl(x)
417
+
418
+
419
+def get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor(
420
+ sf: torch.Tensor,
421
+ ks_tensor: torch.Tensor,
422
+ ks: list[int],
423
+ gran_k: int,
424
+) -> torch.Tensor:
425
+ """Grouped (3D, expert-batched) variant of
426
+ ``get_mn_major_tma_aligned_packed_ue8m0_tensor``. Use for MoE weight
427
+ scale tensors of shape ``(num_experts, mn, k_scale)``.
428
+ """
429
+ _lazy_init()
430
+ if _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl is None:
431
+ return _missing()
432
+ return _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl(
433
+ sf, ks_tensor, ks, gran_k
434
+ )
435
+
436
+
437
def cublaslt_gemm_nt(*args, **kwargs):
438
_lazy_init()
439
if _cublaslt_gemm_nt_impl is None:
@@ -601,4 +738,8 @@ __all__ = [
738
"should_use_deepgemm_for_fp8_linear",
739
"get_col_major_tma_aligned_tensor",
740
"get_mk_alignment_for_contiguous_layout",
741
+ "get_theoretical_mk_alignment_for_contiguous_layout",
742
+ "pack_ue8m0_to_int",
743
+ "get_mn_major_tma_aligned_packed_ue8m0_tensor",
744
+ "get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor",
745
]
vllm/utils/flashinfer.py
+33
-9
@@ -72,11 +72,10 @@ def _missing(*_: Any, **__: Any) -> NoReturn:
72
)
73
74
75
-def _missing_dsv4_sparse_mla(*_: Any, **__: Any) -> NoReturn:
75
+def _missing_sparse_mla(*_: Any, **__: Any) -> NoReturn:
76
raise RuntimeError(
77
- "flashinfer.mla.trtllm_batch_decode_sparse_mla_dsv4 is not available. "
78
- "Install a FlashInfer build that includes DeepSeek V4 sparse MLA "
79
- "TRTLLM-GEN support."
77
+ "FlashInfer sparse MLA decode APIs are not available. "
78
+ "Install a FlashInfer build that includes sparse MLA decode support."
79
)
80
81
@@ -149,14 +148,18 @@ flashinfer_b12x_fused_moe = _lazy_import_wrapper(
148
trtllm_fp4_block_scale_moe = _lazy_import_wrapper(
149
"flashinfer", "trtllm_fp4_block_scale_moe"
150
)
152
-# DeepSeek V4 sparse MLA TRTLLM-GEN decode launcher (public wrapper). Handles
153
-# the SWA + compressed KV pools, the concatenated sparse-index matrix, and
154
-# per-tensor FP8 / BF16 inputs with BF16 output.
151
+flashinfer_trtllm_batch_decode_with_kv_cache_mla = _lazy_import_wrapper(
152
+ "flashinfer.decode",
153
+ "trtllm_batch_decode_with_kv_cache_mla",
154
+ fallback_fn=_missing_sparse_mla,
155
+)
156
flashinfer_trtllm_batch_decode_sparse_mla_dsv4 = _lazy_import_wrapper(
156
- "flashinfer.mla",
157
+ "flashinfer.decode",
158
"trtllm_batch_decode_sparse_mla_dsv4",
158
- fallback_fn=_missing_dsv4_sparse_mla,
159
+ fallback_fn=_missing_sparse_mla,
160
)
161
+
162
+
163
# Special case for autotune since it returns a context manager
164
autotune = _lazy_import_wrapper(
165
"flashinfer.autotuner",
@@ -209,6 +212,26 @@ def has_flashinfer_moe() -> bool:
212
)
213
214
215
+@functools.cache
216
+def has_flashinfer_sparse_mla_sm120() -> bool:
217
+ """Return ``True`` if FlashInfer sparse MLA decode support is available."""
218
+ if not has_flashinfer():
219
+ return False
220
+ try:
221
+ from flashinfer.autotuner import autotune
222
+ from flashinfer.decode import (
223
+ trtllm_batch_decode_sparse_mla_dsv4,
224
+ trtllm_batch_decode_with_kv_cache_mla,
225
+ )
226
+ except ImportError:
227
+ return False
228
+ return (
229
+ callable(trtllm_batch_decode_sparse_mla_dsv4)
230
+ and callable(trtllm_batch_decode_with_kv_cache_mla)
231
+ and callable(autotune)
232
+ )
233
+
234
+
235
@functools.cache
236
def has_flashinfer_cutedsl() -> bool:
237
"""Return ``True`` if FlashInfer cutedsl module is available."""
@@ -988,6 +1011,7 @@ __all__ = [
1011
"flashinfer_b12x_fused_moe",
1012
"flashinfer_convert_sf_to_mma_layout",
1013
"trtllm_fp4_block_scale_moe",
1014
+ "flashinfer_trtllm_batch_decode_with_kv_cache_mla",
1015
"flashinfer_trtllm_batch_decode_sparse_mla_dsv4",
1016
"autotune",
1017
"has_flashinfer_moe",
vllm/v1/attention/backend.py
+1
-1
@@ -436,7 +436,7 @@ class CommonAttentionMetadata:
436
positions: torch.Tensor | None = None
437
"""(num_actual_tokens,) token positions. Optional; set when the caller
438
has positions available so that builders can pre-compute position-dependent
439
- metadata (e.g. C128A topk indices for DeepSeek V4)."""
439
+ sparse metadata for DeepSeek V4 C128A layers."""
440
441
is_prefilling: torch.Tensor | None = None
442
"""(batch_size,) bool tensor: True if request is still in prefill phase
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py
+128
-37
@@ -1,23 +1,12 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""FlashInfer MLA Sparse Attention Backend.
4
-
5
-This backend uses the FlashInfer TRT-LLM MLA kernel with sparse_mla_top_k
6
-for models like DeepSeek-V3.2 that use index-based sparse attention.
7
-
8
-For sparse MLA:
9
-- block_tables shape changes from [batch_size, max_num_blocks] (dense)
10
- to [batch_size, q_len_per_request, sparse_mla_top_k] (sparse)
11
-- The sparse indices represent physical cache slot positions to attend to
12
-- sparse_mla_top_k parameter must be set to the topk value
13
-"""
3
+"""FlashInfer sparse MLA attention backend."""
4
5
from dataclasses import dataclass
6
from typing import TYPE_CHECKING, ClassVar
7
8
import numpy as np
9
import torch
20
-from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
10
11
from vllm.config import VllmConfig
12
from vllm.config.cache import CacheDType
@@ -52,34 +41,13 @@ logger = init_logger(__name__)
41
FLASHINFER_MLA_SPARSE_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
42
43
55
-class FlashInferMLASparseBackend(AttentionBackend):
56
- """FlashInfer MLA backend with sparse attention support.
57
-
58
- This backend uses the FlashInfer TRT-LLM MLA kernel with sparse_mla_top_k
59
- for models like DeepSeek-V3.2 that use index-based sparse attention.
60
- """
61
-
62
- supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
63
- supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
64
- "auto",
65
- "float16",
66
- "bfloat16",
67
- "fp8",
68
- "fp8_e4m3",
69
- ]
70
-
71
- @staticmethod
72
- def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
73
- return [32, 64]
44
+class _FlashInferMLASparseBackendBase(AttentionBackend):
45
+ """Common metadata for concrete FlashInfer sparse MLA backends."""
46
47
@staticmethod
48
def get_name() -> str:
49
return "FLASHINFER_MLA_SPARSE"
50
79
- @staticmethod
80
- def get_impl_cls() -> type["FlashInferMLASparseImpl"]:
81
- return FlashInferMLASparseImpl
82
-
51
@staticmethod
52
def get_builder_cls() -> type["FlashInferMLASparseMetadataBuilder"]:
53
return FlashInferMLASparseMetadataBuilder
@@ -96,9 +64,29 @@ class FlashInferMLASparseBackend(AttentionBackend):
64
def is_sparse(cls) -> bool:
65
return True
66
67
+
68
+class FlashInferMLASparseTRTLLMBackend(_FlashInferMLASparseBackendBase):
69
+ """FlashInfer sparse MLA backend using the TRTLLM-gen launcher."""
70
+
71
+ supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
72
+ supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
73
+ "auto",
74
+ "float16",
75
+ "bfloat16",
76
+ "fp8",
77
+ "fp8_e4m3",
78
+ ]
79
+
80
+ @staticmethod
81
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
82
+ return [32, 64]
83
+
84
+ @staticmethod
85
+ def get_impl_cls() -> type[SparseMLAAttentionImpl]:
86
+ return FlashInferMLASparseImpl
87
+
88
@classmethod
89
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
101
- # FlashInfer sparse MLA targets Blackwell (SM 10.x)
90
return capability.major == 10
91
92
@classmethod
@@ -114,10 +102,15 @@ class FlashInferMLASparseBackend(AttentionBackend):
102
use_mm_prefix: bool,
103
device_capability: DeviceCapability,
104
) -> str | None:
117
- # FlashInfer MLA sparse kernel requires qk_nope_head_dim in [128, 192]
105
from vllm.config import get_current_vllm_config
106
107
vllm_config = get_current_vllm_config()
108
+ if kv_cache_dtype == "fp8_ds_mla":
109
+ return (
110
+ "FLASHINFER_MLA_SPARSE SM10 does not support fp8_ds_mla kv-cache dtype"
111
+ )
112
+
113
+ # FlashInfer MLA sparse SM10 kernel requires qk_nope_head_dim in [128, 192].
114
if vllm_config.model_config is not None:
115
hf_text_config = vllm_config.model_config.hf_text_config
116
qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 1)
@@ -146,6 +139,102 @@ class FlashInferMLASparseBackend(AttentionBackend):
139
return "HND"
140
141
142
+class FlashInferMLASparseSM120Backend(_FlashInferMLASparseBackendBase):
143
+ """FlashInfer sparse MLA backend for SM120."""
144
+
145
+ supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
146
+ supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
147
+ "auto",
148
+ "fp8",
149
+ "fp8_e4m3",
150
+ "fp8_ds_mla",
151
+ ]
152
+
153
+ @staticmethod
154
+ def get_name() -> str:
155
+ return "FLASHINFER_MLA_SPARSE_SM120"
156
+
157
+ @staticmethod
158
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
159
+ return [64, 256]
160
+
161
+ @staticmethod
162
+ def get_impl_cls() -> type[SparseMLAAttentionImpl]:
163
+ from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm120 import (
164
+ FlashInferMLASparseSM120Impl,
165
+ )
166
+
167
+ return FlashInferMLASparseSM120Impl
168
+
169
+ @classmethod
170
+ def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
171
+ return capability.major == 12
172
+
173
+ @classmethod
174
+ def supports_combination(
175
+ cls,
176
+ head_size: int,
177
+ dtype: torch.dtype,
178
+ kv_cache_dtype: CacheDType | None,
179
+ block_size: int | None,
180
+ use_mla: bool,
181
+ has_sink: bool,
182
+ use_sparse: bool,
183
+ use_mm_prefix: bool,
184
+ device_capability: DeviceCapability,
185
+ ) -> str | None:
186
+ from vllm.config import get_current_vllm_config
187
+ from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
188
+
189
+ if not has_flashinfer_sparse_mla_sm120():
190
+ return (
191
+ "FLASHINFER_MLA_SPARSE_SM120 requires FlashInfer's "
192
+ "sparse MLA decode API"
193
+ )
194
+ if dtype != torch.bfloat16:
195
+ return "dtype not supported"
196
+ if kv_cache_dtype not in (
197
+ None,
198
+ "auto",
199
+ "fp8",
200
+ "fp8_e4m3",
201
+ "fp8_ds_mla",
202
+ ):
203
+ return "kv_cache_dtype not supported"
204
+ vllm_config = get_current_vllm_config()
205
+ if vllm_config.model_config is not None:
206
+ hf_text_config = vllm_config.model_config.hf_text_config
207
+ index_topk = getattr(hf_text_config, "index_topk", None)
208
+ if index_topk is None:
209
+ return (
210
+ "FLASHINFER_MLA_SPARSE_SM120 requires a model with "
211
+ "index_topk config"
212
+ )
213
+ if int(index_topk) != 2048:
214
+ return (
215
+ "FLASHINFER_MLA_SPARSE_SM120 requires index_topk=2048; "
216
+ f"got {index_topk}"
217
+ )
218
+ return None
219
+
220
+ @staticmethod
221
+ def get_kv_cache_shape(
222
+ num_blocks: int,
223
+ block_size: int,
224
+ num_kv_heads: int, # assumed to be 1 for MLA
225
+ head_size: int,
226
+ cache_dtype_str: str = "auto",
227
+ ) -> tuple[int, ...]:
228
+ if cache_dtype_str in ("auto", "fp8", "fp8_e4m3", "fp8_ds_mla"):
229
+ # fp8_ds_mla packed layout: 512 NoPE + 16 scales + 128 RoPE.
230
+ return (num_blocks, block_size, 656)
231
+ return (num_blocks, block_size, head_size)
232
+
233
+ @classmethod
234
+ def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
235
+ return None
236
+
237
+
238
@dataclass
239
class FlashInferMLASparseMetadata(AttentionMetadata):
240
"""Attention metadata for FlashInfer MLA Sparse backend."""
@@ -353,6 +442,8 @@ class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata
442
if is_quantized_kv_cache(self.kv_cache_dtype):
443
self.bmm2_scale *= layer._k_scale_float
444
445
+ from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
446
+
447
o = trtllm_batch_decode_with_kv_cache_mla(
448
query=q.unsqueeze(1),
449
kv_cache=kv_c_and_k_pe_cache.unsqueeze(1),
vllm/v1/attention/backends/mla/flashinfer_mla_sparse_sm120.py
new
+155
@@ -0,0 +1,155 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""SM120 implementation variant for ``FLASHINFER_MLA_SPARSE_SM120``."""
4
+
5
+from typing import TYPE_CHECKING, cast
6
+
7
+import torch
8
+
9
+from vllm.v1.attention.backend import (
10
+ AttentionLayer,
11
+ AttentionType,
12
+ SparseMLAAttentionImpl,
13
+)
14
+from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
15
+ FlashInferMLASparseMetadata,
16
+ _get_workspace_buffer,
17
+)
18
+from vllm.v1.attention.backends.mla.sparse_utils import (
19
+ triton_convert_req_index_to_global_index,
20
+)
21
+
22
+if TYPE_CHECKING:
23
+ from vllm.model_executor.models.deepseek_v2 import Indexer
24
+
25
+
26
+def _kv_scale_format_for_model(model_type: str | None) -> str:
27
+ if model_type is not None and model_type.startswith("glm"):
28
+ return "arbitrary_fp32"
29
+ return "pow2_fp32"
30
+
31
+
32
+class FlashInferMLASparseSM120Impl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata]):
33
+ """SM120 FlashInfer sparse-MLA implementation."""
34
+
35
+ def __init__(
36
+ self,
37
+ num_heads: int,
38
+ head_size: int,
39
+ scale: float,
40
+ num_kv_heads: int,
41
+ alibi_slopes: list[float] | None,
42
+ sliding_window: int | None,
43
+ kv_cache_dtype: str,
44
+ logits_soft_cap: float | None,
45
+ attn_type: str,
46
+ kv_sharing_target_layer_name: str | None,
47
+ indexer: "Indexer | None" = None,
48
+ **mla_args,
49
+ ) -> None:
50
+ if any([alibi_slopes, sliding_window, logits_soft_cap]):
51
+ raise NotImplementedError(
52
+ "FLASHINFER_MLA_SPARSE_SM120 does not support alibi_slopes / "
53
+ "sliding_window / logits_soft_cap"
54
+ )
55
+ if attn_type != AttentionType.DECODER:
56
+ raise NotImplementedError(
57
+ "FLASHINFER_MLA_SPARSE_SM120 only supports decoder self-attention"
58
+ )
59
+
60
+ self.num_heads = num_heads
61
+ self.head_size = head_size
62
+ self.scale = float(scale)
63
+ self.num_kv_heads = num_kv_heads
64
+ self.kv_cache_dtype = kv_cache_dtype
65
+ if self.kv_cache_dtype != "fp8_ds_mla":
66
+ raise NotImplementedError(
67
+ "FLASHINFER_MLA_SPARSE_SM120 requires the packed fp8_ds_mla "
68
+ f"KV cache layout; got kv_cache_dtype={kv_cache_dtype!r}."
69
+ )
70
+
71
+ self.kv_lora_rank: int = mla_args["kv_lora_rank"]
72
+ self.qk_nope_head_dim: int = mla_args["qk_nope_head_dim"]
73
+ self.qk_rope_head_dim: int = mla_args["qk_rope_head_dim"]
74
+ from vllm.config import get_current_vllm_config
75
+
76
+ vllm_config = get_current_vllm_config()
77
+ model_type = None
78
+ if vllm_config.model_config is not None:
79
+ model_type = getattr(
80
+ vllm_config.model_config.hf_text_config, "model_type", None
81
+ )
82
+ self.kv_scale_format = _kv_scale_format_for_model(model_type)
83
+
84
+ assert indexer is not None, (
85
+ "FLASHINFER_MLA_SPARSE_SM120 requires a sparse-MLA indexer "
86
+ "(model with index_topk in its config)."
87
+ )
88
+ self.topk_indices_buffer: torch.Tensor | None = indexer.topk_indices_buffer
89
+ from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
90
+
91
+ if not has_flashinfer_sparse_mla_sm120():
92
+ raise RuntimeError(
93
+ "FLASHINFER_MLA_SPARSE_SM120 requires FlashInfer's "
94
+ "sparse MLA decode API."
95
+ )
96
+ assert self.topk_indices_buffer is not None
97
+
98
+ self.supports_quant_query_input = False
99
+ self._workspace_buffer: torch.Tensor | None = None
100
+
101
+ def forward_mqa(
102
+ self,
103
+ q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
104
+ kv_c_and_k_pe_cache: torch.Tensor,
105
+ attn_metadata: FlashInferMLASparseMetadata,
106
+ layer: AttentionLayer,
107
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
108
+ if isinstance(q, tuple):
109
+ q = torch.cat(q, dim=-1)
110
+
111
+ num_actual_toks = q.shape[0]
112
+
113
+ assert self.topk_indices_buffer is not None
114
+ topk_indices = self.topk_indices_buffer[:num_actual_toks]
115
+
116
+ topk_indices_physical = cast(
117
+ torch.Tensor,
118
+ triton_convert_req_index_to_global_index(
119
+ attn_metadata.req_id_per_token[:num_actual_toks],
120
+ attn_metadata.block_table,
121
+ topk_indices,
122
+ BLOCK_SIZE=attn_metadata.block_size,
123
+ NUM_TOPK_TOKENS=topk_indices.shape[1],
124
+ ),
125
+ )
126
+
127
+ output = q.new_empty(
128
+ (num_actual_toks, self.num_heads, self.kv_lora_rank),
129
+ dtype=q.dtype,
130
+ )
131
+
132
+ if self._workspace_buffer is None:
133
+ self._workspace_buffer = _get_workspace_buffer(q.device)
134
+
135
+ from vllm.utils.flashinfer import (
136
+ flashinfer_trtllm_batch_decode_with_kv_cache_mla,
137
+ )
138
+
139
+ out = flashinfer_trtllm_batch_decode_with_kv_cache_mla(
140
+ query=q.unsqueeze(1),
141
+ kv_cache=kv_c_and_k_pe_cache.view(torch.uint8).unsqueeze(1),
142
+ workspace_buffer=self._workspace_buffer,
143
+ qk_nope_head_dim=self.qk_nope_head_dim,
144
+ kv_lora_rank=self.kv_lora_rank,
145
+ qk_rope_head_dim=self.qk_rope_head_dim,
146
+ block_tables=topk_indices_physical.unsqueeze(1),
147
+ seq_lens=None,
148
+ max_seq_len=attn_metadata.topk_tokens,
149
+ out=output.unsqueeze(1),
150
+ bmm1_scale=self.scale,
151
+ bmm2_scale=1.0,
152
+ sparse_mla_top_k=attn_metadata.topk_tokens,
153
+ kv_scale_format=self.kv_scale_format,
154
+ )
155
+ return out.squeeze(1), None
vllm/v1/attention/backends/mla/sparse_swa.py
+74
-15
@@ -74,14 +74,14 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
74
# determines the SWA block size of 64 tokens per block.
75
# TODO(yifan): make SWA block size automatically determined and configurable.
76
self.block_size = 64
77
- # uint8: legacy FlashMLA UE8M0 paged layout. bfloat16 / float8_e4m3fn:
78
- # FlashInfer contiguous full-cache layout.
77
+ # uint8: fp8_ds_mla UE8M0 paged layout. bfloat16 / float8_e4m3fn:
78
+ # contiguous full-cache layout.
79
assert self.dtype in (torch.uint8, torch.bfloat16, torch.float8_e4m3fn)
80
81
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
82
- # FlashMLA's UE8M0 paged layout needs 576B alignment; FlashInfer's
83
- # contiguous bf16/fp8 cache uses the natural element-size page.
84
- is_flashmla = self.cache_config.cache_dtype == "fp8_ds_mla"
82
+ # fp8_ds_mla's UE8M0 paged layout needs 576B alignment; contiguous
83
+ # bf16/fp8 cache uses the natural element-size page.
84
+ uses_fp8_ds_mla_layout = self.cache_config.cache_dtype == "fp8_ds_mla"
85
return SlidingWindowMLASpec(
86
block_size=self.block_size,
87
num_kv_heads=1,
@@ -89,7 +89,7 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
89
dtype=self.dtype,
90
sliding_window=self.window_size,
91
cache_dtype_str=self.cache_config.cache_dtype,
92
- alignment=576 if is_flashmla else None,
92
+ alignment=576 if uses_fp8_ds_mla_layout else None,
93
model_version="deepseek_v4",
94
)
95
@@ -164,6 +164,11 @@ class DeepseekSparseSWAMetadata:
164
token_to_req_indices: torch.Tensor | None = None # [num_tokens]
165
decode_swa_indices: torch.Tensor | None = None # [num_decode_tokens, window_size]
166
decode_swa_lens: torch.Tensor | None = None # [num_decode_tokens]
167
+ # Paged-coordinate prefill SWA indices/lens (FP8 paged-direct prefill).
168
+ prefill_swa_indices: torch.Tensor | None = (
169
+ None # [num_prefill_tokens, 1, window_size]
170
+ )
171
+ prefill_swa_lens: torch.Tensor | None = None # [num_prefill_tokens]
172
173
# Number of decode/prefill requests/tokens (batch is reordered: decodes first)
174
num_decodes: int = 0
@@ -343,6 +348,20 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
348
dtype=torch.int32,
349
device=self.device,
350
)
351
+ # Allocated unconditionally — consumer picks paged-direct vs dequant
352
+ # at call time.
353
+ self.prefill_swa_indices = torch.zeros(
354
+ max_tokens,
355
+ 1,
356
+ self.window_size,
357
+ dtype=torch.int32,
358
+ device=self.device,
359
+ )
360
+ self.prefill_swa_lens = torch.zeros(
361
+ max_tokens,
362
+ dtype=torch.int32,
363
+ device=self.device,
364
+ )
365
self.is_valid_token = torch.zeros(
366
max_tokens,
367
dtype=torch.bool,
@@ -402,6 +421,29 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
421
block_table,
422
block_table.stride(0),
423
self.block_size,
424
+ token_offset=0,
425
+ TRITON_BLOCK_SIZE=1024,
426
+ )
427
+
428
+ # Prefill SWA indices live in paged coordinates. `token_offset` lets
429
+ # the kernel read is_valid_token / token_to_req_indices at absolute
430
+ # prefill positions while writing output starting at index 0.
431
+ if num_prefill_tokens > 0:
432
+ prefill_swa_indices = self.prefill_swa_indices[:num_prefill_tokens]
433
+ prefill_swa_lens = self.prefill_swa_lens[:num_prefill_tokens]
434
+ _compute_swa_indices_and_lens_kernel[(num_prefill_tokens,)](
435
+ prefill_swa_indices,
436
+ prefill_swa_indices.stride(0),
437
+ prefill_swa_lens,
438
+ self.window_size,
439
+ query_start_loc,
440
+ seq_lens,
441
+ token_to_req_indices,
442
+ is_valid_token,
443
+ block_table,
444
+ block_table.stride(0),
445
+ self.block_size,
446
+ token_offset=num_decode_tokens,
447
TRITON_BLOCK_SIZE=1024,
448
)
449
@@ -431,6 +473,16 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
473
token_to_req_indices=token_to_req_indices,
474
decode_swa_indices=self.decode_swa_indices[:num_decode_tokens],
475
decode_swa_lens=self.decode_swa_lens[:num_decode_tokens],
476
+ prefill_swa_indices=(
477
+ self.prefill_swa_indices[:num_prefill_tokens]
478
+ if num_prefill_tokens > 0
479
+ else None
480
+ ),
481
+ prefill_swa_lens=(
482
+ self.prefill_swa_lens[:num_prefill_tokens]
483
+ if num_prefill_tokens > 0
484
+ else None
485
+ ),
486
block_size=self.block_size,
487
num_decodes=num_decodes,
488
num_prefills=num_prefills,
@@ -465,6 +517,7 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
517
num_decode_tokens == 0
518
or current_platform.is_rocm()
519
or current_platform.is_xpu()
520
+ or current_platform.is_device_capability_family(120)
521
):
522
return out
523
for layer_type in self._layer_types:
@@ -489,7 +542,7 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
542
Returns a dict of keyword arguments to pass to the
543
DeepseekSparseSWAMetadata constructor.
544
492
- Note: C128A topk indices are computed by the FlashMLASparse builder
545
+ Note: C128A sparse metadata is computed by the FlashMLASparse builder
546
(which owns the C128A block_table), not here.
547
"""
548
result: dict[str, torch.Tensor | int | None] = {}
@@ -539,10 +592,14 @@ def _compute_prefill_metadata_kernel(
592
"""Compute prefill gather_lens in a single pass."""
593
offset = tl.arange(0, BLOCK_SIZE)
594
mask = offset < num_prefills
595
+ # SM12x + Triton 3.6 raises IMA on out-of-bounds address arithmetic for
596
+ # masked-off lanes even though the load mask gates the actual read, so
597
+ # clamp the offset. Caller guarantees num_prefills > 0.
598
+ safe_offset = tl.minimum(offset, num_prefills - 1)
599
543
- seq_len = tl.load(seq_lens_ptr + num_decodes + offset, mask=mask)
544
- qsl_start = tl.load(query_start_loc_ptr + num_decodes + offset, mask=mask)
545
- qsl_end = tl.load(query_start_loc_ptr + num_decodes + offset + 1, mask=mask)
600
+ seq_len = tl.load(seq_lens_ptr + num_decodes + safe_offset, mask=mask)
601
+ qsl_start = tl.load(query_start_loc_ptr + num_decodes + safe_offset, mask=mask)
602
+ qsl_end = tl.load(query_start_loc_ptr + num_decodes + safe_offset + 1, mask=mask)
603
604
query_len = qsl_end - qsl_start
605
prefix_len = seq_len - query_len
@@ -551,7 +608,7 @@ def _compute_prefill_metadata_kernel(
608
tl.store(prefill_gather_lens_ptr + offset, gather_len, mask=mask)
609
610
554
-@triton.jit
611
+@triton.jit(do_not_specialize=["token_offset"])
612
def _compute_swa_indices_and_lens_kernel(
613
swa_indices_ptr,
614
swa_indices_stride,
@@ -564,12 +621,14 @@ def _compute_swa_indices_and_lens_kernel(
621
block_table_ptr,
622
block_table_stride,
623
block_size,
624
+ token_offset,
625
TRITON_BLOCK_SIZE: tl.constexpr,
626
):
569
- token_idx = tl.program_id(0)
627
+ pid = tl.program_id(0)
628
+ token_idx = pid + token_offset
629
is_valid = tl.load(is_valid_token_ptr + token_idx)
630
if not is_valid:
572
- tl.store(swa_lens_ptr + token_idx, 0)
631
+ tl.store(swa_lens_ptr + pid, 0)
632
return
633
634
req_idx = tl.load(token_to_req_indices_ptr + token_idx)
@@ -586,7 +645,7 @@ def _compute_swa_indices_and_lens_kernel(
645
end_pos = pos + 1
646
647
swa_len = end_pos - start_pos
589
- tl.store(swa_lens_ptr + token_idx, swa_len)
648
+ tl.store(swa_lens_ptr + pid, swa_len)
649
650
for i in range(0, window_size, TRITON_BLOCK_SIZE):
651
offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
@@ -602,7 +661,7 @@ def _compute_swa_indices_and_lens_kernel(
661
662
slot_ids = tl.where(offset < swa_len, slot_ids, -1)
663
tl.store(
605
- swa_indices_ptr + token_idx * swa_indices_stride + offset,
664
+ swa_indices_ptr + pid * swa_indices_stride + offset,
665
slot_ids,
666
mask=offset < window_size,
667
)
vllm/v1/attention/backends/registry.py
+5
-1
@@ -71,7 +71,11 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
71
)
72
FLASHINFER_MLA_SPARSE = (
73
"vllm.v1.attention.backends.mla.flashinfer_mla_sparse."
74
- "FlashInferMLASparseBackend"
74
+ "FlashInferMLASparseTRTLLMBackend"
75
+ )
76
+ FLASHINFER_MLA_SPARSE_SM120 = (
77
+ "vllm.v1.attention.backends.mla.flashinfer_mla_sparse."
78
+ "FlashInferMLASparseSM120Backend"
79
)
80
TRITON_MLA = "vllm.v1.attention.backends.mla.triton_mla.TritonMLABackend"
81
CUTLASS_MLA = "vllm.v1.attention.backends.mla.cutlass_mla.CutlassMLABackend"
vllm/v1/attention/ops/flashmla.py
+1
-1
@@ -73,7 +73,7 @@ def is_flashmla_sparse_supported() -> tuple[bool, str | None]:
73
):
74
return (
75
False,
76
- "FlashMLA Sparse is only supported on Hopper and Blackwell devices.",
76
+ "FlashMLA Sparse is only supported on Hopper and Blackwell DC devices.",
77
)
78
return True, None
79
vllm/v1/worker/gpu/warmup.py
+130
@@ -2,12 +2,14 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from collections.abc import Callable
5
+from contextlib import AbstractContextManager, nullcontext
6
from typing import Any
7
8
import numpy as np
9
import torch
10
11
from vllm import PoolingParams, SamplingParams
12
+from vllm.logger import init_logger
13
from vllm.utils.math_utils import cdiv
14
from vllm.v1.core.sched.output import (
15
CachedRequestData,
@@ -18,6 +20,134 @@ from vllm.v1.core.sched.output import (
20
from vllm.v1.request import Request
21
from vllm.v1.worker.gpu.model_runner import GPUModelRunner
22
23
+logger = init_logger(__name__)
24
+
25
+
26
+def run_mixed_prefill_decode_warmup(
27
+ model_runner: GPUModelRunner,
28
+ worker_execute_model: Callable[[SchedulerOutput], Any],
29
+ worker_sample_tokens: Callable[[GrammarOutput | None], Any],
30
+ num_tokens: int,
31
+ *,
32
+ mixed_step_context: AbstractContextManager[object] | None = None,
33
+ req_id_prefix: str = "_v2_mixed_warmup",
34
+) -> bool:
35
+ """Run a V2 mixed prefill+decode step through normal scheduler inputs."""
36
+ if model_runner.is_pooling_model or num_tokens < 3:
37
+ return False
38
+
39
+ decode_req_id = f"{req_id_prefix}_decode_"
40
+ prefill_req_id = f"{req_id_prefix}_prefill_"
41
+ decode_prompt_len = 2
42
+ decode_scheduled_tokens = 1
43
+ prefill_len = num_tokens - decode_scheduled_tokens
44
+ decode_token_ids = list(range(decode_prompt_len))
45
+ prefill_token_ids = list(range(prefill_len))
46
+
47
+ kv_cache_groups = model_runner.kv_cache_config.kv_cache_groups
48
+ num_kv_cache_groups = len(kv_cache_groups)
49
+ group_block_sizes = [g.kv_cache_spec.block_size for g in kv_cache_groups]
50
+ decode_prefill_block_counts = [
51
+ cdiv(decode_prompt_len, block_size) for block_size in group_block_sizes
52
+ ]
53
+ decode_block_counts = [
54
+ cdiv(decode_prompt_len + decode_scheduled_tokens, block_size)
55
+ for block_size in group_block_sizes
56
+ ]
57
+ decode_block_deltas = [
58
+ decode - prefill
59
+ for decode, prefill in zip(decode_block_counts, decode_prefill_block_counts)
60
+ ]
61
+ prefill_block_counts = [
62
+ cdiv(prefill_len, block_size) for block_size in group_block_sizes
63
+ ]
64
+ required_blocks = sum(decode_block_counts) + sum(prefill_block_counts)
65
+ if model_runner.kv_cache_config.num_blocks <= required_blocks:
66
+ logger.warning(
67
+ "Skipping V2 mixed prefill+decode warmup because only %d KV blocks "
68
+ "are available for %d required warmup blocks.",
69
+ model_runner.kv_cache_config.num_blocks,
70
+ required_blocks,
71
+ )
72
+ return False
73
+
74
+ next_block_id = 1
75
+
76
+ def _alloc_blocks(num_blocks: int) -> list[int]:
77
+ nonlocal next_block_id
78
+ block_ids = list(range(next_block_id, next_block_id + num_blocks))
79
+ next_block_id += num_blocks
80
+ return block_ids
81
+
82
+ sampling_params = SamplingParams(max_tokens=2, temperature=0.0)
83
+
84
+ decode_prefill_output = SchedulerOutput.make_empty()
85
+ decode_prefill_output.scheduled_new_reqs = [
86
+ NewRequestData(
87
+ req_id=decode_req_id,
88
+ prompt_token_ids=decode_token_ids,
89
+ mm_features=[],
90
+ sampling_params=sampling_params,
91
+ pooling_params=None,
92
+ block_ids=tuple(_alloc_blocks(n) for n in decode_prefill_block_counts),
93
+ num_computed_tokens=0,
94
+ lora_request=None,
95
+ prefill_token_ids=decode_token_ids,
96
+ ),
97
+ ]
98
+ decode_prefill_output.num_scheduled_tokens = {
99
+ decode_req_id: decode_prompt_len,
100
+ }
101
+ decode_prefill_output.total_num_scheduled_tokens = decode_prompt_len
102
+ decode_prefill_output.num_common_prefix_blocks = [0] * num_kv_cache_groups
103
+
104
+ decode_new_blocks = tuple(_alloc_blocks(n) for n in decode_block_deltas)
105
+ cached_decode_req = CachedRequestData.make_empty()
106
+ cached_decode_req.req_ids = [decode_req_id]
107
+ cached_decode_req.num_computed_tokens = [decode_prompt_len]
108
+ cached_decode_req.num_output_tokens = [1]
109
+ cached_decode_req.new_block_ids = [
110
+ decode_new_blocks if any(decode_block_deltas) else None
111
+ ]
112
+
113
+ mixed_output = SchedulerOutput.make_empty()
114
+ mixed_output.scheduled_cached_reqs = cached_decode_req
115
+ mixed_output.scheduled_new_reqs = [
116
+ NewRequestData(
117
+ req_id=prefill_req_id,
118
+ prompt_token_ids=prefill_token_ids,
119
+ mm_features=[],
120
+ sampling_params=sampling_params,
121
+ pooling_params=None,
122
+ block_ids=tuple(_alloc_blocks(n) for n in prefill_block_counts),
123
+ num_computed_tokens=0,
124
+ lora_request=None,
125
+ prefill_token_ids=prefill_token_ids,
126
+ ),
127
+ ]
128
+ mixed_output.num_scheduled_tokens = {
129
+ decode_req_id: decode_scheduled_tokens,
130
+ prefill_req_id: prefill_len,
131
+ }
132
+ mixed_output.total_num_scheduled_tokens = num_tokens
133
+ mixed_output.num_common_prefix_blocks = [0] * num_kv_cache_groups
134
+
135
+ cleanup_output = SchedulerOutput.make_empty()
136
+ cleanup_output.finished_req_ids = {decode_req_id, prefill_req_id}
137
+
138
+ context = mixed_step_context or nullcontext()
139
+ model_runner.kv_connector.set_disabled(True)
140
+ try:
141
+ worker_execute_model(decode_prefill_output)
142
+ worker_sample_tokens(None)
143
+ with context:
144
+ worker_execute_model(mixed_output)
145
+ worker_sample_tokens(None)
146
+ worker_execute_model(cleanup_output)
147
+ finally:
148
+ model_runner.kv_connector.set_disabled(False)
149
+ return True
150
+
151
152
@torch.inference_mode()
153
def warmup_kernels(