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

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

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

Synced Nov 1, 18:45 UTC

[DSV4] Decouple DS V4 Sparse MLA Metadata from DS V3.2 (#44699)

Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>

Woosuk Kwon committed Jun 6, 2026 at 17:37 UTC 2a983c79acdbe78fa62e67eaa87ed4bbb7c8257e
7 files changed +449 -333
docs/design/attention_backends.md
+1 -1
@@ -240,5 +240,5 @@ default on NVIDIA is `FLASHMLA_SPARSE_DSV4`.
240 | Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
241 | ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
242 | `FLASHINFER_MLA_SPARSE_DSV4` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
243 -| `FLASHMLA_SPARSE_DSV4` | fp16, bf16 | `auto` | 256 | 512 | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
243 +| `FLASHMLA_SPARSE_DSV4` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla`, `fp8` | 256 | 512 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
244 | `ROCM_FLASHMLA_SPARSE_DSV4` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
vllm/models/deepseek_v4/amd/rocm.py
+8 -10
@@ -9,17 +9,15 @@ import torch
9 from vllm.forward_context import get_forward_context
10 from vllm.models.deepseek_v4.attention import DeepseekV4Attention
11 from vllm.models.deepseek_v4.common.ops import dequantize_and_gather_k_cache
12 -from vllm.models.deepseek_v4.nvidia.flashmla import (
13 - DeepseekV4FlashMLASparseBackend,
12 +from vllm.models.deepseek_v4.sparse_mla import (
13 + DeepseekV4FlashMLABackend,
14 + DeepseekV4FlashMLAMetadata,
15 + DeepseekV4FlashMLAMetadataBuilder,
16 )
17 from vllm.triton_utils import tl, triton
18 from vllm.v1.attention.backend import (
19 CommonAttentionMetadata,
20 )
19 -from vllm.v1.attention.backends.mla.flashmla_sparse import (
20 - FlashMLASparseMetadata,
21 - FlashMLASparseMetadataBuilder,
22 -)
21 from vllm.v1.attention.backends.mla.sparse_swa import (
22 DeepseekSparseSWAMetadata,
23 DeepseekSparseSWAMetadataBuilder,
@@ -445,7 +443,7 @@ def _copy_ragged_to_graph_buffers(
443
444
445 @dataclass
448 -class DeepseekV4ROCMAiterMLASparseMetadata(FlashMLASparseMetadata):
446 +class DeepseekV4ROCMAiterMLASparseMetadata(DeepseekV4FlashMLAMetadata):
447 """ROCm-specific DeepSeek V4 metadata carrying ragged decode topk."""
448
449 c128a_decode_topk_ragged_indices: torch.Tensor | None = None
@@ -458,12 +456,12 @@ class DeepseekV4ROCMAiterSparseSWAMetadata(DeepseekSparseSWAMetadata):
456 decode_swa_ragged_indptr: torch.Tensor | None = None
457
458
461 -class DeepseekV4ROCMAiterMLASparseMetadataBuilder(FlashMLASparseMetadataBuilder):
459 +class DeepseekV4ROCMAiterMLASparseMetadataBuilder(DeepseekV4FlashMLAMetadataBuilder):
460 def __init__(self, *args, **kwargs):
461 super().__init__(*args, **kwargs)
462 self.c128a_decode_topk_ragged_indices_buffer: torch.Tensor | None = None
463 self.c128a_decode_topk_ragged_indptr_buffer: torch.Tensor | None = None
466 - if self.is_deepseek_v4 and self.compress_ratio == 128:
464 + if self.compress_ratio == 128:
465 max_tokens = self.vllm_config.scheduler_config.max_num_batched_tokens
466 self.c128a_decode_topk_ragged_indices_buffer = torch.empty(
467 max_tokens * self.c128a_max_compressed,
@@ -569,7 +567,7 @@ class DeepseekV4ROCMAiterSparseSWAMetadataBuilder(DeepseekSparseSWAMetadataBuild
567 )
568
569
572 -class DeepseekV4ROCMAiterMLASparseBackend(DeepseekV4FlashMLASparseBackend):
570 +class DeepseekV4ROCMAiterMLASparseBackend(DeepseekV4FlashMLABackend):
571 @staticmethod
572 def get_name() -> str:
573 return "ROCM_FLASHMLA_SPARSE_DSV4"
vllm/models/deepseek_v4/nvidia/flashinfer_sparse.py
+13 -10
@@ -18,13 +18,15 @@ from vllm.models.deepseek_v4.attention import DeepseekV4Attention
18 from vllm.models.deepseek_v4.common.ops import (
19 build_flashinfer_mixed_sparse_indices,
20 )
21 -from vllm.models.deepseek_v4.nvidia.flashmla import DeepseekV4FlashMLASparseBackend
21 from vllm.models.deepseek_v4.nvidia.ops.o_proj import (
22 compute_fp8_einsum_recipe,
23 deep_gemm_fp8_o_proj,
24 )
25 +from vllm.models.deepseek_v4.sparse_mla import (
26 + DeepseekV4FlashMLABackend,
27 + DeepseekV4FlashMLAMetadata,
28 +)
29 from vllm.utils.flashinfer import flashinfer_trtllm_batch_decode_sparse_mla_dsv4
27 -from vllm.v1.attention.backends.mla.flashmla_sparse import FlashMLASparseMetadata
30
31 if TYPE_CHECKING:
32 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
@@ -47,13 +49,14 @@ def _get_flashinfer_dsv4_workspace(device: torch.device) -> torch.Tensor:
49 return workspace
50
51
50 -class DeepseekV4FlashInferMLASparseBackend(DeepseekV4FlashMLASparseBackend):
52 +class DeepseekV4FlashInferMLASparseBackend(DeepseekV4FlashMLABackend):
53 """Shares the FlashMLA V4 metadata/cache pipeline; swaps the attention impl.
54
53 - Inheriting from the FlashMLA V4 backend reuses its ``FlashMLASparseMetadata``
54 - builder (which the V4 sparse-index pipeline needs — the V3.2 FlashInfer
55 - builder lacks the ``c128a_*`` fields), 256-token blocks, head_size 512, and
56 - the (num_blocks, block_size, 512) cache shape for non-``fp8_ds_mla`` dtypes.
55 + Inheriting from the FlashMLA V4 backend reuses its
56 + ``DeepseekV4FlashMLAMetadata`` builder (which the V4 sparse-index
57 + pipeline needs — the V3.2 FlashInfer builder lacks the ``c128a_*`` fields),
58 + 256-token blocks, head_size 512, and the (num_blocks, block_size, 512) cache
59 + shape for non-``fp8_ds_mla`` dtypes.
60 """
61
62 @staticmethod
@@ -162,7 +165,7 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
165
166 assert isinstance(attn_metadata, dict)
167 flashmla_metadata = cast(
165 - FlashMLASparseMetadata | None, attn_metadata.get(self.prefix)
168 + DeepseekV4FlashMLAMetadata | None, attn_metadata.get(self.prefix)
169 )
170 swa_metadata = cast(
171 "DeepseekSparseSWAMetadata | None",
@@ -190,7 +193,7 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
193 kv_cache: torch.Tensor | None,
194 swa_k_cache: torch.Tensor,
195 swa_metadata: "DeepseekSparseSWAMetadata",
193 - attn_metadata: FlashMLASparseMetadata | None,
196 + attn_metadata: DeepseekV4FlashMLAMetadata | None,
197 swa_only: bool,
198 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
199 """Build the combined sparse-index tensors for the mixed batch.
@@ -310,7 +313,7 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
313 kv_cache: torch.Tensor | None,
314 swa_k_cache: torch.Tensor,
315 swa_metadata: "DeepseekSparseSWAMetadata",
313 - attn_metadata: FlashMLASparseMetadata | None,
316 + attn_metadata: DeepseekV4FlashMLAMetadata | None,
317 swa_only: bool,
318 output: torch.Tensor,
319 ) -> None:
vllm/models/deepseek_v4/nvidia/flashmla.py
+7 -39
@@ -16,10 +16,9 @@ from vllm.models.deepseek_v4.nvidia.ops.o_proj import (
16 compute_fp8_einsum_recipe,
17 deep_gemm_fp8_o_proj,
18 )
19 -from vllm.v1.attention.backend import MultipleOf
20 -from vllm.v1.attention.backends.mla.flashmla_sparse import (
21 - FlashMLASparseBackend,
22 - FlashMLASparseMetadata,
19 +from vllm.models.deepseek_v4.sparse_mla import (
20 + DeepseekV4FlashMLABackend,
21 + DeepseekV4FlashMLAMetadata,
22 )
23 from vllm.v1.attention.ops.flashmla import (
24 flash_mla_sparse_fwd,
@@ -31,41 +30,10 @@ if TYPE_CHECKING:
30 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
31
32
34 -class DeepseekV4FlashMLASparseBackend(FlashMLASparseBackend):
35 - @staticmethod
36 - def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
37 - return [256]
38 -
39 - @staticmethod
40 - def get_name() -> str:
41 - return "FLASHMLA_SPARSE_DSV4"
42 -
43 - @classmethod
44 - def get_supported_head_sizes(cls) -> list[int]:
45 - # DeepSeek V4 layout: 448 NoPE + 64 RoPE = 512 (overrides the
46 - # V3.2 default of 576 from FlashMLASparseBackend).
47 - return [512]
48 -
49 - @staticmethod
50 - def get_kv_cache_shape(
51 - num_blocks: int,
52 - block_size: int,
53 - num_kv_heads: int,
54 - head_size: int,
55 - cache_dtype_str: str = "auto",
56 - ) -> tuple[int, ...]:
57 - if cache_dtype_str == "fp8_ds_mla":
58 - # DeepseekV4 main MLA: 584B per token (448 NoPE + 128 RoPE + 8 fp8 scale).
59 - # head_size passed in is the semantic head_dim (512).
60 - return (num_blocks, block_size, 584)
61 - else:
62 - return (num_blocks, block_size, head_size)
63 -
64 -
33 class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
34 """FlashMLA sparse MLA attention layer for DeepSeek V4 (CUDA)."""
35
68 - backend_cls = DeepseekV4FlashMLASparseBackend
36 + backend_cls = DeepseekV4FlashMLABackend
37
38 def __init__(self, *args, **kwargs) -> None:
39 super().__init__(*args, **kwargs)
@@ -135,7 +103,7 @@ class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
103
104 assert isinstance(attn_metadata, dict)
105 flashmla_metadata = cast(
138 - FlashMLASparseMetadata | None, attn_metadata.get(self.prefix)
106 + DeepseekV4FlashMLAMetadata | None, attn_metadata.get(self.prefix)
107 )
108 swa_metadata = cast(
109 "DeepseekSparseSWAMetadata | None",
@@ -179,7 +147,7 @@ class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
147 q: torch.Tensor,
148 kv_cache: torch.Tensor | None, # Only used when compress_ratio > 1
149 swa_metadata: "DeepseekSparseSWAMetadata",
182 - attn_metadata: FlashMLASparseMetadata | None,
150 + attn_metadata: DeepseekV4FlashMLAMetadata | None,
151 swa_only: bool,
152 output: torch.Tensor,
153 ) -> None:
@@ -273,7 +241,7 @@ class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
241 compressed_k_cache: torch.Tensor | None, # Only used when compress_ratio > 1
242 swa_k_cache: torch.Tensor,
243 output: torch.Tensor,
276 - attn_metadata: FlashMLASparseMetadata | None,
244 + attn_metadata: DeepseekV4FlashMLAMetadata | None,
245 swa_metadata: "DeepseekSparseSWAMetadata",
246 ) -> None:
247 swa_only = attn_metadata is None
vllm/models/deepseek_v4/sparse_mla.py new
+416
@@ -0,0 +1,416 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""DeepSeek-V4 FlashMLA sparse backend, metadata, and metadata builder."""
4 +
5 +from dataclasses import dataclass
6 +from typing import Any, ClassVar
7 +
8 +import numpy as np
9 +import torch
10 +
11 +from vllm.config import VllmConfig
12 +from vllm.config.cache import CacheDType
13 +from vllm.platforms.interface import DeviceCapability
14 +from vllm.triton_utils import tl, triton
15 +from vllm.utils.math_utils import cdiv
16 +from vllm.v1.attention.backend import (
17 + AttentionBackend,
18 + AttentionCGSupport,
19 + AttentionMetadata,
20 + AttentionMetadataBuilder,
21 + CommonAttentionMetadata,
22 + MultipleOf,
23 +)
24 +from vllm.v1.attention.backends.mla.compressor_utils import get_compressed_slot_mapping
25 +from vllm.v1.attention.backends.utils import split_decodes_and_prefills
26 +from vllm.v1.kv_cache_interface import AttentionSpec
27 +
28 +# Pad C128A topk width to this alignment. 128 covers both h_q=64 (B_TOPK=64) and
29 +# h_q=128 (B_TOPK=128). FlashMLA decode asserts extra_topk % B_TOPK == 0;
30 +# unaligned widths (e.g. 17 = ceil(2136/128)) crash the sm100 head64 kernel.
31 +# Padded slots stay -1 and decode_lens caps them via topk_length, so the pad is a
32 +# no-op at kernel level. Mirrors _SPARSE_PREFILL_TOPK_ALIGNMENT in cache_utils.py.
33 +_C128A_TOPK_ALIGNMENT = 128
34 +
35 +
36 +class DeepseekV4FlashMLABackend(AttentionBackend):
37 + """DeepSeek-V4 sparse-MLA backend.
38 +
39 + Subclasses ``AttentionBackend`` directly (not the V3.2
40 + ``FlashMLASparseBackend``): DeepSeek-V4 runs its own attention layer
41 + (``DeepseekV4Attention``), so it does not reuse the V3.2 builder or impl, and
42 + only needs to declare its own metadata builder, KV-cache layout, and the
43 + sparse-MLA capability flags.
44 + """
45 +
46 + supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
47 + supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
48 + "auto",
49 + "bfloat16",
50 + "fp8_ds_mla",
51 + "fp8", # alias for fp8_ds_mla
52 + ]
53 +
54 + @staticmethod
55 + def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
56 + return [256]
57 +
58 + @staticmethod
59 + def get_name() -> str:
60 + return "FLASHMLA_SPARSE_DSV4"
61 +
62 + @staticmethod
63 + def get_builder_cls() -> type["DeepseekV4FlashMLAMetadataBuilder"]:
64 + return DeepseekV4FlashMLAMetadataBuilder
65 +
66 + @staticmethod
67 + def get_impl_cls() -> type[Any]:
68 + # DeepSeek-V4 runs its attention through ``DeepseekV4Attention.forward``,
69 + # not the generic ``Attention``/``MLAAttention`` layer, so the backend's
70 + # impl class is never instantiated.
71 + raise NotImplementedError(
72 + "DeepseekV4FlashMLABackend has no separate impl class; DeepSeek-V4 "
73 + "attention runs through DeepseekV4Attention."
74 + )
75 +
76 + @classmethod
77 + def get_supported_head_sizes(cls) -> list[int]:
78 + # DeepSeek V4 layout: 448 NoPE + 64 RoPE = 512.
79 + return [512]
80 +
81 + @classmethod
82 + def is_mla(cls) -> bool:
83 + return True
84 +
85 + @classmethod
86 + def is_sparse(cls) -> bool:
87 + return True
88 +
89 + @classmethod
90 + def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
91 + return capability.major in [9, 10]
92 +
93 + @staticmethod
94 + def get_kv_cache_shape(
95 + num_blocks: int,
96 + block_size: int,
97 + num_kv_heads: int,
98 + head_size: int,
99 + cache_dtype_str: str = "auto",
100 + ) -> tuple[int, ...]:
101 + if cache_dtype_str == "fp8_ds_mla":
102 + # DeepseekV4 main MLA: 584B per token (448 NoPE + 128 RoPE + 8 fp8 scale).
103 + # head_size passed in is the semantic head_dim (512).
104 + return (num_blocks, block_size, 584)
105 + else:
106 + return (num_blocks, block_size, head_size)
107 +
108 +
109 +@dataclass
110 +class DeepseekV4FlashMLAMetadata(AttentionMetadata):
111 + num_reqs: int
112 + max_query_len: int
113 + max_seq_len: int
114 +
115 + num_actual_tokens: int # Number of tokens excluding padding.
116 + query_start_loc: torch.Tensor
117 + slot_mapping: torch.Tensor
118 +
119 + block_table: torch.Tensor
120 + req_id_per_token: torch.Tensor
121 + block_size: int
122 + topk_tokens: int
123 +
124 + # Pre-computed C128A metadata (compress_ratio == 128 only).
125 + # Decode: global slot ids + valid-entry counts (fused from positions).
126 + c128a_global_decode_topk_indices: torch.Tensor | None = None
127 + c128a_decode_topk_lens: torch.Tensor | None = None
128 + # Prefill: local topk indices (used by combine_topk_swa_indices).
129 + c128a_prefill_topk_indices: torch.Tensor | None = None
130 +
131 +
132 +class DeepseekV4FlashMLAMetadataBuilder(
133 + AttentionMetadataBuilder[DeepseekV4FlashMLAMetadata]
134 +):
135 + _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
136 +
137 + def __init__(
138 + self,
139 + kv_cache_spec: AttentionSpec,
140 + layer_names: list[str],
141 + vllm_config: VllmConfig,
142 + device: torch.device,
143 + ) -> None:
144 + super().__init__(kv_cache_spec, layer_names, vllm_config, device)
145 + self.model_config = vllm_config.model_config
146 + # Classify single-token queries (plus num_speculative_tokens via
147 + # supports_spec_as_decode=True) as decodes; longer queries go to prefill.
148 + self._init_reorder_batch_threshold(1, supports_spec_as_decode=True)
149 + self.topk_tokens = self.model_config.hf_config.index_topk
150 +
151 + max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens
152 + self.req_id_per_token_buffer = torch.empty(
153 + (max_num_batched_tokens,), dtype=torch.int32, device=device
154 + )
155 +
156 + assert hasattr(self.kv_cache_spec, "compress_ratio")
157 + self.compress_ratio = self.kv_cache_spec.compress_ratio
158 +
159 + # Pre-allocate compressed slot mapping buffer for CUDA graph address
160 + # stability when compress_ratio > 1.
161 + if self.compress_ratio > 1:
162 + self.compressed_slot_mapping_buffer = torch.empty(
163 + max_num_batched_tokens, dtype=torch.int64, device=device
164 + )
165 +
166 + # Pre-allocate C128A topk buffers for CUDA graph address stability.
167 + if self.compress_ratio == 128:
168 + c128a_max_compressed = cdiv(
169 + self.model_config.max_model_len, self.compress_ratio
170 + )
171 + c128a_max_compressed = (
172 + cdiv(c128a_max_compressed, _C128A_TOPK_ALIGNMENT)
173 + * _C128A_TOPK_ALIGNMENT
174 + )
175 + # Stored so _build_c128a_metadata passes it as the kernel's
176 + # max_compressed_tokens, matching the buffer stride. Otherwise the
177 + # kernel's default 8192 iterates past row width and spills writes
178 + # into adjacent rows (present in both decode and prefill branches of
179 + # _build_c128a_topk_metadata_kernel).
180 + self.c128a_max_compressed = c128a_max_compressed
181 + self.c128a_global_decode_buffer = torch.empty(
182 + (max_num_batched_tokens, c128a_max_compressed),
183 + dtype=torch.int32,
184 + device=device,
185 + )
186 + self.c128a_decode_lens_buffer = torch.empty(
187 + max_num_batched_tokens, dtype=torch.int32, device=device
188 + )
189 + self.c128a_prefill_buffer = torch.empty(
190 + (max_num_batched_tokens, c128a_max_compressed),
191 + dtype=torch.int32,
192 + device=device,
193 + )
194 +
195 + def build(
196 + self,
197 + common_prefix_len: int,
198 + common_attn_metadata: CommonAttentionMetadata,
199 + fast_build: bool = False,
200 + ) -> DeepseekV4FlashMLAMetadata:
201 + cm = common_attn_metadata
202 + num_tokens = cm.num_actual_tokens
203 + starts = np.asarray(cm.query_start_loc_cpu, dtype=np.int32)
204 + seg_lengths = np.diff(starts)
205 + req_id_per_token = np.repeat(
206 + np.arange(seg_lengths.shape[0], dtype=np.int32), seg_lengths
207 + )
208 + # Zero-fill for cudagraphs
209 + self.req_id_per_token_buffer.fill_(0)
210 + self.req_id_per_token_buffer[: req_id_per_token.shape[0]].copy_(
211 + torch.from_numpy(req_id_per_token), non_blocking=True
212 + )
213 + req_id_per_token = self.req_id_per_token_buffer[:num_tokens]
214 +
215 + slot_mapping = cm.slot_mapping
216 + if self.compress_ratio > 1:
217 + slot_mapping = get_compressed_slot_mapping(
218 + cm.num_actual_tokens,
219 + cm.query_start_loc,
220 + cm.seq_lens,
221 + cm.block_table_tensor.clamp(min=0),
222 + int(self.kv_cache_spec.storage_block_size),
223 + self.compress_ratio,
224 + out=self.compressed_slot_mapping_buffer,
225 + )
226 +
227 + c128a_fields: dict[str, torch.Tensor | None] = {}
228 + if self.compress_ratio == 128:
229 + c128a_fields = self._build_c128a_metadata(cm, req_id_per_token)
230 +
231 + return DeepseekV4FlashMLAMetadata(
232 + num_reqs=cm.num_reqs,
233 + max_query_len=cm.max_query_len,
234 + max_seq_len=cm.max_seq_len,
235 + num_actual_tokens=cm.num_actual_tokens,
236 + query_start_loc=cm.query_start_loc,
237 + slot_mapping=slot_mapping,
238 + block_table=cm.block_table_tensor,
239 + req_id_per_token=req_id_per_token,
240 + block_size=self.kv_cache_spec.block_size,
241 + topk_tokens=self.topk_tokens,
242 + c128a_global_decode_topk_indices=c128a_fields.get(
243 + "c128a_global_decode_topk_indices"
244 + ),
245 + c128a_decode_topk_lens=c128a_fields.get("c128a_decode_topk_lens"),
246 + c128a_prefill_topk_indices=c128a_fields.get("c128a_prefill_topk_indices"),
247 + )
248 +
249 + def _build_c128a_metadata(
250 + self,
251 + cm: CommonAttentionMetadata,
252 + req_id_per_token: torch.Tensor,
253 + ) -> dict[str, torch.Tensor | None]:
254 + """Pre-compute C128A topk indices for DeepseekV4 (compress_ratio >= 128)."""
255 + # Must match SWA's decode split (no `require_uniform=True`) so
256 + # `c128a_global_decode_topk_indices.shape[0]` lines up with q in
257 + # `_forward_decode`. The per-token C128A kernel handles non-uniform
258 + # query lengths.
259 + (num_decodes, _, num_decode_tokens, num_prefill_tokens) = (
260 + split_decodes_and_prefills(
261 + cm,
262 + decode_threshold=self.reorder_batch_threshold or 1,
263 + )
264 + )
265 +
266 + num_total = num_decode_tokens + num_prefill_tokens
267 + if num_total == 0:
268 + return {}
269 +
270 + assert cm.positions is not None, (
271 + "positions is required for C128A metadata build"
272 + )
273 + block_size = self.kv_cache_spec.block_size // self.compress_ratio
274 + global_decode, decode_lens, prefill_local = build_c128a_topk_metadata(
275 + cm.positions[:num_total],
276 + self.compress_ratio,
277 + num_decode_tokens,
278 + req_id_per_token,
279 + cm.block_table_tensor[:num_decodes],
280 + block_size,
281 + cm.slot_mapping,
282 + self.c128a_global_decode_buffer,
283 + self.c128a_decode_lens_buffer,
284 + self.c128a_prefill_buffer,
285 + max_compressed_tokens=self.c128a_max_compressed,
286 + )
287 +
288 + result: dict[str, torch.Tensor | None] = {}
289 + if num_decode_tokens > 0:
290 + result["c128a_global_decode_topk_indices"] = global_decode.view(
291 + num_decode_tokens, 1, -1
292 + )
293 + result["c128a_decode_topk_lens"] = decode_lens
294 + if num_prefill_tokens > 0:
295 + result["c128a_prefill_topk_indices"] = prefill_local
296 + return result
297 +
298 +
299 +def build_c128a_topk_metadata(
300 + positions: torch.Tensor,
301 + compress_ratio: int,
302 + num_decode_tokens: int,
303 + token_to_req_indices: torch.Tensor,
304 + block_table: torch.Tensor,
305 + block_size: int,
306 + slot_mapping: torch.Tensor,
307 + global_decode_buffer: torch.Tensor,
308 + decode_lens_buffer: torch.Tensor,
309 + prefill_buffer: torch.Tensor,
310 + max_compressed_tokens: int = 8192,
311 +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
312 + """Single kernel for all C128A tokens (decode + prefill).
313 +
314 + Decode tokens: position → block_table lookup → global slot ids + topk_lens.
315 + Prefill tokens: position → local indices [0, ..., n-1, -1, ...].
316 +
317 + Writes into pre-allocated buffers for CUDA graph address stability.
318 + Returns slices of the buffers.
319 + """
320 + num_tokens = positions.shape[0]
321 + num_prefill_tokens = num_tokens - num_decode_tokens
322 +
323 + global_decode = global_decode_buffer[:num_decode_tokens]
324 + decode_lens = decode_lens_buffer[:num_decode_tokens]
325 + prefill_local = prefill_buffer[:num_prefill_tokens]
326 +
327 + if num_tokens == 0:
328 + return global_decode, decode_lens, prefill_local
329 +
330 + _build_c128a_topk_metadata_kernel[(num_tokens,)](
331 + global_decode_buffer,
332 + global_decode_buffer.stride(0),
333 + decode_lens_buffer,
334 + prefill_buffer,
335 + prefill_buffer.stride(0),
336 + positions,
337 + compress_ratio,
338 + max_compressed_tokens,
339 + num_decode_tokens,
340 + token_to_req_indices,
341 + block_table,
342 + block_table.stride(0),
343 + block_size,
344 + slot_mapping,
345 + BLOCK_SIZE=1024,
346 + )
347 + return global_decode, decode_lens, prefill_local
348 +
349 +
350 +@triton.jit
351 +def _build_c128a_topk_metadata_kernel(
352 + # Decode outputs
353 + global_decode_ptr,
354 + global_decode_stride,
355 + decode_lens_ptr,
356 + # Prefill output
357 + prefill_local_ptr,
358 + prefill_local_stride,
359 + # Inputs
360 + positions_ptr,
361 + compress_ratio,
362 + max_compressed_tokens,
363 + num_decode_tokens,
364 + token_to_req_indices_ptr,
365 + block_table_ptr,
366 + block_table_stride,
367 + block_size,
368 + slot_mapping_ptr,
369 + BLOCK_SIZE: tl.constexpr,
370 +):
371 + token_idx = tl.program_id(0)
372 + position = tl.load(positions_ptr + token_idx)
373 + num_compressed = (position + 1) // compress_ratio
374 + num_compressed = tl.minimum(num_compressed, max_compressed_tokens)
375 + is_decode = token_idx < num_decode_tokens
376 +
377 + if is_decode:
378 + # --- Decode: block-table lookup → global slot ids + count ---
379 + is_valid_token = tl.load(slot_mapping_ptr + token_idx) >= 0
380 + req_idx = tl.load(token_to_req_indices_ptr + token_idx)
381 + count = tl.zeros((), dtype=tl.int32)
382 + for i in range(0, max_compressed_tokens, BLOCK_SIZE):
383 + offset = i + tl.arange(0, BLOCK_SIZE)
384 + mask = offset < max_compressed_tokens
385 + is_valid = offset < num_compressed
386 +
387 + block_indices = offset // block_size
388 + block_numbers = tl.load(
389 + block_table_ptr + req_idx * block_table_stride + block_indices,
390 + mask=mask & is_valid,
391 + )
392 + block_offsets = offset % block_size
393 + slot_ids = block_numbers * block_size + block_offsets
394 + slot_ids = tl.where(is_valid, slot_ids, -1)
395 + tl.store(
396 + global_decode_ptr + token_idx * global_decode_stride + offset,
397 + slot_ids,
398 + mask=mask,
399 + )
400 + count += tl.sum(is_valid.to(tl.int32), axis=0)
401 +
402 + tl.store(
403 + decode_lens_ptr + token_idx,
404 + tl.where(is_valid_token, count, 0),
405 + )
406 + else:
407 + # --- Prefill: write local indices ---
408 + pfx_idx = token_idx - num_decode_tokens
409 + for i in range(0, max_compressed_tokens, BLOCK_SIZE):
410 + offset = i + tl.arange(0, BLOCK_SIZE)
411 + mask = offset < max_compressed_tokens
412 + tl.store(
413 + prefill_local_ptr + pfx_idx * prefill_local_stride + offset,
414 + tl.where(offset < num_compressed, offset, -1),
415 + mask=mask,
416 + )
vllm/v1/attention/backends/mla/flashmla_sparse.py
+3 -272
@@ -15,8 +15,6 @@ from vllm.model_executor.layers.attention.mla_attention import (
15 )
16 from vllm.platforms import current_platform
17 from vllm.platforms.interface import DeviceCapability
18 -from vllm.triton_utils import tl, triton
19 -from vllm.utils.math_utils import cdiv
18 from vllm.utils.platform_utils import num_compute_units
19 from vllm.utils.torch_utils import is_quantized_kv_cache
20 from vllm.v1.attention.backend import (
@@ -29,7 +27,6 @@ from vllm.v1.attention.backend import (
27 MultipleOf,
28 SparseMLAAttentionImpl,
29 )
32 -from vllm.v1.attention.backends.mla.compressor_utils import get_compressed_slot_mapping
30 from vllm.v1.attention.backends.mla.sparse_utils import (
31 triton_convert_req_index_to_global_index,
32 )
@@ -118,9 +115,6 @@ class FlashMLASparseBackend(AttentionBackend):
115 @classmethod
116 def get_supported_head_sizes(cls) -> list[int]:
117 # DeepSeek V3.2 layout: 512 NoPE + 64 RoPE = 576.
121 - # DeepSeek V4 uses 448 NoPE + 64 RoPE = 512 and overrides this in
122 - # vllm/models/deepseek_v4/nvidia/flashmla.py:
123 - # DeepseekV4FlashMLASparseBackend.get_supported_head_sizes.
118 return [576]
119
120 @classmethod
@@ -223,13 +217,6 @@ class FlashMLASparseMetadata(AttentionMetadata):
217 fp8_extra_metadata: FP8SeparatePrefillDecode | FP8KernelMetadata | None = None
218 fp8_use_mixed_batch: bool = False
219
226 - # Pre-computed C128A metadata (DeepseekV4 only, compress_ratio == 128).
227 - # Decode: global slot ids + valid-entry counts (fused from positions).
228 - c128a_global_decode_topk_indices: torch.Tensor | None = None
229 - c128a_decode_topk_lens: torch.Tensor | None = None
230 - # Prefill: local topk indices (used by combine_topk_swa_indices).
231 - c128a_prefill_topk_indices: torch.Tensor | None = None
232 -
220
221 def get_prefill_workspace_size(max_model_len: int):
222 # NOTE(Lucas): 5 is a magic number for controlling the prefill buffer size.
@@ -325,68 +312,6 @@ class FlashMLASparseMetadataBuilder(AttentionMetadataBuilder[FlashMLASparseMetad
312 device=device,
313 )
314
328 - # DeepseekV4: has compress_ratios in hf_config.
329 - hf_config = vllm_config.model_config.hf_config
330 - self.is_deepseek_v4 = (
331 - hasattr(hf_config, "compress_ratios") and len(hf_config.compress_ratios) > 0
332 - )
333 - self.compress_ratio = 1
334 - if self.is_deepseek_v4:
335 - assert hasattr(self.kv_cache_spec, "compress_ratio")
336 - self.compress_ratio = self.kv_cache_spec.compress_ratio
337 - # Pre-allocate compressed slot mapping buffer for CUDA graph
338 - # address stability when compress_ratio > 1.
339 - if self.compress_ratio > 1:
340 - max_num_batched_tokens = (
341 - vllm_config.scheduler_config.max_num_batched_tokens
342 - )
343 - self.compressed_slot_mapping_buffer = torch.empty(
344 - max_num_batched_tokens,
345 - dtype=torch.int64,
346 - device=self.device,
347 - )
348 -
349 - # Pre-allocate C128A topk buffers for CUDA graph address stability.
350 - if self.compress_ratio == 128:
351 - max_num_batched_tokens = (
352 - vllm_config.scheduler_config.max_num_batched_tokens
353 - )
354 - # Pad to B_TOPK alignment (128 covers both h_q=64 B_TOPK=64 and
355 - # h_q=128 B_TOPK=128). FlashMLA decode asserts extra_topk % B_TOPK
356 - # == 0; unaligned widths (e.g. 17 = ceil(2136/128)) crash the
357 - # sm100 head64 kernel. Padded slots stay -1 and decode_lens caps
358 - # them via topk_length, so the pad is a no-op at kernel level.
359 - # Mirrors _SPARSE_PREFILL_TOPK_ALIGNMENT in cache_utils.py.
360 - _C128A_TOPK_ALIGNMENT = 128
361 - c128a_max_compressed = cdiv(
362 - self.model_config.max_model_len, self.compress_ratio
363 - )
364 - c128a_max_compressed = (
365 - cdiv(c128a_max_compressed, _C128A_TOPK_ALIGNMENT)
366 - * _C128A_TOPK_ALIGNMENT
367 - )
368 - # Stored so _build_c128a_metadata passes it as the kernel's
369 - # max_compressed_tokens, matching the buffer stride. Otherwise
370 - # the kernel's default 8192 iterates past row width and spills
371 - # writes into adjacent rows (present in both decode and prefill
372 - # branches of _build_c128a_topk_metadata_kernel).
373 - self.c128a_max_compressed = c128a_max_compressed
374 - self.c128a_global_decode_buffer = torch.empty(
375 - (max_num_batched_tokens, c128a_max_compressed),
376 - dtype=torch.int32,
377 - device=self.device,
378 - )
379 - self.c128a_decode_lens_buffer = torch.empty(
380 - max_num_batched_tokens,
381 - dtype=torch.int32,
382 - device=self.device,
383 - )
384 - self.c128a_prefill_buffer = torch.empty(
385 - (max_num_batched_tokens, c128a_max_compressed),
386 - dtype=torch.int32,
387 - device=self.device,
388 - )
389 -
315 def _build_fp8_mixed_decode_prefill(
316 self,
317 common_attn_metadata: CommonAttentionMetadata,
@@ -582,109 +507,35 @@ class FlashMLASparseMetadataBuilder(AttentionMetadataBuilder[FlashMLASparseMetad
507 )
508 req_id_per_token = self.req_id_per_token_buffer[:num_tokens]
509
585 - slot_mapping = cm.slot_mapping
586 - if self.compress_ratio > 1:
587 - slot_mapping = get_compressed_slot_mapping(
588 - common_attn_metadata.num_actual_tokens,
589 - common_attn_metadata.query_start_loc,
590 - common_attn_metadata.seq_lens,
591 - common_attn_metadata.block_table_tensor.clamp(min=0),
592 - int(self.kv_cache_spec.storage_block_size),
593 - self.compress_ratio,
594 - out=self.compressed_slot_mapping_buffer,
595 - )
596 -
510 fp8_extra_metadata: (
511 FlashMLASparseMetadata.FP8SeparatePrefillDecode
512 | FlashMLASparseMetadata.FP8KernelMetadata
513 | None
514 ) = None
602 - fp8_use_mixed_batch = (
603 - self.num_heads < MIN_HEADS_FOR_BF16_PREFILL and not self.is_deepseek_v4
604 - )
605 - # DeepseekV4 has its own attention impl (DeepseekV4Attention) that does not
606 - # consume fp8_extra_metadata. Skipping the build here avoids a
607 - # forced D2H sync on seq_lens that would otherwise fire on every
608 - # prefill-bearing step, lifting GPU utilization on long-prefill
609 - # workloads (e.g. LongBench) from ~83% to ~100%.
610 - if self.use_fp8_kv_cache and not self.is_deepseek_v4:
515 + fp8_use_mixed_batch = self.num_heads < MIN_HEADS_FOR_BF16_PREFILL
516 + if self.use_fp8_kv_cache:
517 if fp8_use_mixed_batch:
518 fp8_extra_metadata = self._build_fp8_mixed_decode_prefill(cm)
519 else:
520 fp8_extra_metadata = self._build_fp8_separate_prefill_decode(cm)
521
616 - # Pre-compute C128A topk indices for DeepseekV4.
617 - c128a_fields = {}
618 - if self.is_deepseek_v4 and self.compress_ratio == 128:
619 - c128a_fields = self._build_c128a_metadata(cm, req_id_per_token)
620 -
522 metadata = FlashMLASparseMetadata(
523 num_reqs=cm.num_reqs,
524 max_query_len=cm.max_query_len,
525 max_seq_len=cm.max_seq_len,
526 num_actual_tokens=cm.num_actual_tokens,
527 query_start_loc=cm.query_start_loc,
627 - slot_mapping=slot_mapping,
528 + slot_mapping=cm.slot_mapping,
529 block_table=cm.block_table_tensor,
530 req_id_per_token=req_id_per_token,
531 block_size=self.kv_cache_spec.block_size,
532 topk_tokens=self.topk_tokens,
533 fp8_extra_metadata=fp8_extra_metadata,
534 fp8_use_mixed_batch=fp8_use_mixed_batch,
634 - **c128a_fields,
535 )
536
537 return metadata
538
639 - def _build_c128a_metadata(
640 - self,
641 - cm: CommonAttentionMetadata,
642 - req_id_per_token: torch.Tensor,
643 - ) -> dict[str, torch.Tensor | None]:
644 - """Pre-compute C128A topk indices for DeepseekV4 (compress_ratio >= 128)."""
645 - # Must match SWA's decode split (no `require_uniform=True`) so
646 - # `c128a_global_decode_topk_indices.shape[0]` lines up with q in
647 - # `_forward_decode`. The per-token C128A kernel handles non-uniform
648 - # query lengths.
649 - (num_decodes, _, num_decode_tokens, num_prefill_tokens) = (
650 - split_decodes_and_prefills(
651 - cm,
652 - decode_threshold=self.reorder_batch_threshold or 1,
653 - )
654 - )
655 -
656 - num_total = num_decode_tokens + num_prefill_tokens
657 - if num_total == 0:
658 - return {}
659 -
660 - assert cm.positions is not None, (
661 - "positions is required for C128A metadata build"
662 - )
663 - block_size = self.kv_cache_spec.block_size // self.compress_ratio
664 - global_decode, decode_lens, prefill_local = build_c128a_topk_metadata(
665 - cm.positions[:num_total],
666 - self.compress_ratio,
667 - num_decode_tokens,
668 - req_id_per_token,
669 - cm.block_table_tensor[:num_decodes],
670 - block_size,
671 - cm.slot_mapping,
672 - self.c128a_global_decode_buffer,
673 - self.c128a_decode_lens_buffer,
674 - self.c128a_prefill_buffer,
675 - max_compressed_tokens=self.c128a_max_compressed,
676 - )
677 -
678 - result: dict[str, torch.Tensor | None] = {}
679 - if num_decode_tokens > 0:
680 - result["c128a_global_decode_topk_indices"] = global_decode.view(
681 - num_decode_tokens, 1, -1
682 - )
683 - result["c128a_decode_topk_lens"] = decode_lens
684 - if num_prefill_tokens > 0:
685 - result["c128a_prefill_topk_indices"] = prefill_local
686 - return result
687 -
539
540 class FlashMLASparseImpl(SparseMLAAttentionImpl[FlashMLASparseMetadata]):
541 @staticmethod
@@ -1027,123 +878,3 @@ class FlashMLASparseImpl(SparseMLAAttentionImpl[FlashMLASparseMetadata]):
878 )
879
880 return attn_out, None
1030 -
1031 -
1032 -def build_c128a_topk_metadata(
1033 - positions: torch.Tensor,
1034 - compress_ratio: int,
1035 - num_decode_tokens: int,
1036 - token_to_req_indices: torch.Tensor,
1037 - block_table: torch.Tensor,
1038 - block_size: int,
1039 - slot_mapping: torch.Tensor,
1040 - global_decode_buffer: torch.Tensor,
1041 - decode_lens_buffer: torch.Tensor,
1042 - prefill_buffer: torch.Tensor,
1043 - max_compressed_tokens: int = 8192,
1044 -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
1045 - """Single kernel for all C128A tokens (decode + prefill).
1046 -
1047 - Decode tokens: position → block_table lookup → global slot ids + topk_lens.
1048 - Prefill tokens: position → local indices [0, ..., n-1, -1, ...].
1049 -
1050 - Writes into pre-allocated buffers for CUDA graph address stability.
1051 - Returns slices of the buffers.
1052 - """
1053 - num_tokens = positions.shape[0]
1054 - num_prefill_tokens = num_tokens - num_decode_tokens
1055 -
1056 - global_decode = global_decode_buffer[:num_decode_tokens]
1057 - decode_lens = decode_lens_buffer[:num_decode_tokens]
1058 - prefill_local = prefill_buffer[:num_prefill_tokens]
1059 -
1060 - if num_tokens == 0:
1061 - return global_decode, decode_lens, prefill_local
1062 -
1063 - _build_c128a_topk_metadata_kernel[(num_tokens,)](
1064 - global_decode_buffer,
1065 - global_decode_buffer.stride(0),
1066 - decode_lens_buffer,
1067 - prefill_buffer,
1068 - prefill_buffer.stride(0),
1069 - positions,
1070 - compress_ratio,
1071 - max_compressed_tokens,
1072 - num_decode_tokens,
1073 - token_to_req_indices,
1074 - block_table,
1075 - block_table.stride(0),
1076 - block_size,
1077 - slot_mapping,
1078 - BLOCK_SIZE=1024,
1079 - )
1080 - return global_decode, decode_lens, prefill_local
1081 -
1082 -
1083 -@triton.jit
1084 -def _build_c128a_topk_metadata_kernel(
1085 - # Decode outputs
1086 - global_decode_ptr,
1087 - global_decode_stride,
1088 - decode_lens_ptr,
1089 - # Prefill output
1090 - prefill_local_ptr,
1091 - prefill_local_stride,
1092 - # Inputs
1093 - positions_ptr,
1094 - compress_ratio,
1095 - max_compressed_tokens,
1096 - num_decode_tokens,
1097 - token_to_req_indices_ptr,
1098 - block_table_ptr,
1099 - block_table_stride,
1100 - block_size,
1101 - slot_mapping_ptr,
1102 - BLOCK_SIZE: tl.constexpr,
1103 -):
1104 - token_idx = tl.program_id(0)
1105 - position = tl.load(positions_ptr + token_idx)
1106 - num_compressed = (position + 1) // compress_ratio
1107 - num_compressed = tl.minimum(num_compressed, max_compressed_tokens)
1108 - is_decode = token_idx < num_decode_tokens
1109 -
1110 - if is_decode:
1111 - # --- Decode: block-table lookup → global slot ids + count ---
1112 - is_valid_token = tl.load(slot_mapping_ptr + token_idx) >= 0
1113 - req_idx = tl.load(token_to_req_indices_ptr + token_idx)
1114 - count = tl.zeros((), dtype=tl.int32)
1115 - for i in range(0, max_compressed_tokens, BLOCK_SIZE):
1116 - offset = i + tl.arange(0, BLOCK_SIZE)
1117 - mask = offset < max_compressed_tokens
1118 - is_valid = offset < num_compressed
1119 -
1120 - block_indices = offset // block_size
1121 - block_numbers = tl.load(
1122 - block_table_ptr + req_idx * block_table_stride + block_indices,
1123 - mask=mask & is_valid,
1124 - )
1125 - block_offsets = offset % block_size
1126 - slot_ids = block_numbers * block_size + block_offsets
1127 - slot_ids = tl.where(is_valid, slot_ids, -1)
1128 - tl.store(
1129 - global_decode_ptr + token_idx * global_decode_stride + offset,
1130 - slot_ids,
1131 - mask=mask,
1132 - )
1133 - count += tl.sum(is_valid.to(tl.int32), axis=0)
1134 -
1135 - tl.store(
1136 - decode_lens_ptr + token_idx,
1137 - tl.where(is_valid_token, count, 0),
1138 - )
1139 - else:
1140 - # --- Prefill: write local indices ---
1141 - pfx_idx = token_idx - num_decode_tokens
1142 - for i in range(0, max_compressed_tokens, BLOCK_SIZE):
1143 - offset = i + tl.arange(0, BLOCK_SIZE)
1144 - mask = offset < max_compressed_tokens
1145 - tl.store(
1146 - prefill_local_ptr + pfx_idx * prefill_local_stride + offset,
1147 - tl.where(offset < num_compressed, offset, -1),
1148 - mask=mask,
1149 - )
vllm/v1/attention/backends/registry.py
+1 -1
@@ -78,7 +78,7 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
78 )
79 # DeepSeek V4 sparse MLA backends (model-driven; selected via the V4 layer).
80 FLASHMLA_SPARSE_DSV4 = (
81 - "vllm.models.deepseek_v4.nvidia.flashmla.DeepseekV4FlashMLASparseBackend"
81 + "vllm.models.deepseek_v4.sparse_mla.DeepseekV4FlashMLABackend"
82 )
83 FLASHINFER_MLA_SPARSE_DSV4 = (
84 "vllm.models.deepseek_v4.nvidia.flashinfer_sparse."