main
py 943 lines 37.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """DeepSeek V4 FlashInfer sparse MLA backend."""
4
5 from typing import TYPE_CHECKING, ClassVar, cast
6
7 import torch
8
9 from vllm.config import VllmConfig
10 from vllm.config.cache import CacheDType
11 from vllm.forward_context import get_forward_context
12 from vllm.models.deepseek_v4.nvidia.ops.o_proj import compute_fp8_einsum_recipe
13 from vllm.models.deepseek_v41.attention import DeepseekV4Attention
14 from vllm.models.deepseek_v41.common.ops import (
15 build_flashinfer_mixed_sparse_indices,
16 compute_global_topk_indices_and_lens,
17 )
18 from vllm.models.deepseek_v41.nvidia.ops.o_proj import (
19 dsv41_o_proj,
20 register_dsv41_o_proj_warmup,
21 )
22 from vllm.models.deepseek_v41.sparse_mla import (
23 DeepseekV4FlashMLAMetadata,
24 DeepseekV4SparseMLABackend,
25 DeepseekV4SparseMLAMetadataBuilder,
26 DeepseekV41SparseSWAMetadataBuilder,
27 )
28 from vllm.platforms.interface import DeviceCapability
29 from vllm.utils.flashinfer import flashinfer_trtllm_batch_decode_sparse_mla_dsv4
30 from vllm.v1.attention.backend import (
31 AttentionCGSupport,
32 CommonAttentionMetadata,
33 MultipleOf,
34 )
35 from vllm.v1.attention.backends.mla.compressor_utils import (
36 get_dspark_swa_index_width,
37 )
38 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWABackend
39
40 if TYPE_CHECKING:
41 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
42
43 _FLASHINFER_DSV4_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
44 _flashinfer_dsv4_workspace_by_device: dict[torch.device, torch.Tensor] = {}
45
46
47 def _get_flashinfer_dsv4_workspace(device: torch.device) -> torch.Tensor:
48 workspace = _flashinfer_dsv4_workspace_by_device.get(device)
49 if workspace is None:
50 workspace = torch.zeros(
51 _FLASHINFER_DSV4_WORKSPACE_BUFFER_SIZE,
52 dtype=torch.uint8,
53 device=device,
54 )
55 _flashinfer_dsv4_workspace_by_device[device] = workspace
56 return workspace
57
58
59 def _packed_block_span(pool: torch.Tensor) -> int:
60 """Per-block stride of ``pool`` in tokens (``stride(0)//stride(-2)``): ==
61 block_size for unpacked KV, larger when packed (#44577). Raises if not
62 token-aligned."""
63 block_stride = pool.stride(0)
64 token_stride = pool.stride(-2)
65 if block_stride % token_stride != 0:
66 raise NotImplementedError(
67 "FLASHINFER_MLA_SPARSE_DSV4 packed KV requires the per-block stride "
68 f"({block_stride}) to be a multiple of the per-token stride "
69 f"({token_stride}); this layout is not supported yet."
70 )
71 return block_stride // token_stride
72
73
74 # Sparse MLA h_q counts accepted natively (flashinfer>=0.6.14, #3545).
75 _SPARSE_MLA_SUPPORTED_Q_HEADS = (8, 16, 32, 64, 128)
76
77
78 def _pad_to_supported_q_heads(num_heads: int) -> int:
79 for supported in _SPARSE_MLA_SUPPORTED_Q_HEADS:
80 if num_heads <= supported:
81 return supported
82 raise ValueError(
83 f"DeepseekV4 FlashInfer MLA Sparse does not support {num_heads} heads "
84 "(sparse MLA kernel requires h_q in {8, 16, 32, 64, 128})."
85 )
86
87
88 def _required_sm120_sparse_topk(vllm_config: VllmConfig, window_size: int) -> int:
89 """Return the SM120 DSV4 SWA specialization needed by this model."""
90 if not vllm_config.attention_config.use_non_causal:
91 return window_size
92 speculative_config = vllm_config.speculative_config
93 if speculative_config is None:
94 return window_size
95 return get_dspark_swa_index_width(
96 window_size,
97 speculative_config.num_speculative_tokens,
98 )
99
100
101 class DeepseekV4FlashInferMLASparseBackend(DeepseekV4SparseMLABackend):
102 """FlashInfer backend using the DSv4 sparse metadata/cache layout.
103
104 Inherits the base and backend reuses its``DeepseekV4SparseMLAMetadataBuilder``
105 """
106
107 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
108 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
109 "auto",
110 "bfloat16",
111 "fp8",
112 "fp8_e4m3",
113 "fp8_ds_mla",
114 ]
115
116 @staticmethod
117 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
118 return [128]
119
120 @staticmethod
121 def get_name() -> str:
122 return "FLASHINFER_MLA_SPARSE_DSV41"
123
124 @classmethod
125 def get_supported_head_sizes(cls) -> list[int]:
126 return [512]
127
128 @classmethod
129 def supports_sink(cls) -> bool:
130 return True
131
132 @classmethod
133 def is_sparse(cls) -> bool:
134 return True
135
136 @classmethod
137 def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
138 return capability.major in [10, 12]
139
140 @classmethod
141 def supports_combination(
142 cls,
143 head_size: int,
144 dtype: torch.dtype,
145 kv_cache_dtype: CacheDType | None,
146 block_size: int | None,
147 use_mla: bool,
148 has_sink: bool,
149 use_sparse: bool,
150 use_mm_prefix: bool,
151 device_capability: DeviceCapability,
152 ) -> str | None:
153 if device_capability.major == 10:
154 if kv_cache_dtype == "fp8_ds_mla":
155 return (
156 "FLASHINFER_MLA_SPARSE_DSV4 SM10x uses the plain "
157 "per-tensor FP8 KV layout, not fp8_ds_mla"
158 )
159 if kv_cache_dtype not in (None, "auto", "bfloat16", "fp8", "fp8_e4m3"):
160 return "kv_cache_dtype not supported"
161 return None
162 if device_capability.major == 12:
163 if kv_cache_dtype not in ("fp8", "fp8_e4m3", "fp8_ds_mla"):
164 return "kv_cache_dtype not supported"
165 from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
166
167 if not has_flashinfer_sparse_mla_sm120():
168 return (
169 "FLASHINFER_MLA_SPARSE_DSV4 SM120 requires FlashInfer's "
170 "sparse MLA decode API"
171 )
172 return None
173 return "FLASHINFER_MLA_SPARSE_DSV4 requires SM10x or SM12x"
174
175 @staticmethod
176 def get_builder_cls() -> type["DeepseekV4FlashInferSparseMLAMetadataBuilder"]:
177 return DeepseekV4FlashInferSparseMLAMetadataBuilder
178
179
180 class DeepseekV4FlashInferSparseMLAMetadataBuilder(DeepseekV4SparseMLAMetadataBuilder):
181 """Varlen-capable metadata builder for the FlashInfer sparse MLA backend."""
182
183 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
184
185
186 class DeepseekSparseSWAFlashInferMetadataBuilder(DeepseekV41SparseSWAMetadataBuilder):
187 """SWA metadata for the FlashInfer sparse decode path (varlen decode)."""
188
189 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
190
191 def __init__(self, *args, **kwargs):
192 super().__init__(*args, **kwargs)
193 # Graphs retain these addresses while each build refreshes their contents.
194 self._decode_topk_lens = torch.empty(
195 self._max_tokens, dtype=torch.int32, device=self.device
196 )
197 self._decode_seq_lens = torch.empty_like(self._decode_topk_lens)
198
199 def build(
200 self,
201 common_prefix_len: int,
202 common_attn_metadata: CommonAttentionMetadata,
203 fast_build: bool = False,
204 replay_start: torch.Tensor | None = None,
205 ) -> "DeepseekSparseSWAMetadata":
206 metadata = super().build(
207 common_prefix_len, common_attn_metadata, fast_build, replay_start
208 )
209 num_tokens = metadata.num_decode_tokens
210 if not common_attn_metadata.causal and num_tokens > 0:
211 assert metadata.decode_swa_lens is not None
212 assert metadata.seq_lens is not None
213 assert metadata.token_to_req_indices is not None
214 topk_lens = self._decode_topk_lens[:num_tokens]
215 seq_lens = self._decode_seq_lens[:num_tokens]
216 torch.clamp(metadata.decode_swa_lens, min=self.window_size, out=topk_lens)
217 torch.index_select(
218 metadata.seq_lens,
219 0,
220 metadata.token_to_req_indices[:num_tokens],
221 out=seq_lens,
222 )
223 metadata.flashinfer_decode_topk_lens = topk_lens
224 metadata.flashinfer_decode_seq_lens = seq_lens
225 return metadata
226
227
228 class DeepseekSparseSWAFlashInferBackend(DeepseekSparseSWABackend):
229 @staticmethod
230 def get_builder_cls() -> type[DeepseekSparseSWAFlashInferMetadataBuilder]:
231 return DeepseekSparseSWAFlashInferMetadataBuilder
232
233
234 class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
235 """FlashInfer TRTLLM-gen sparse MLA attention layer for SM100 DeepSeek V4."""
236
237 backend_cls = DeepseekV4FlashInferMLASparseBackend
238 swa_backend_cls = DeepseekSparseSWAFlashInferBackend
239 use_fp8_ds_mla_layout: ClassVar[bool] = False
240
241 @classmethod
242 def get_padded_num_q_heads(cls, num_heads: int) -> int:
243 return _pad_to_supported_q_heads(num_heads)
244
245 def _o_proj(self, attn_out: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
246 return dsv41_o_proj(self, attn_out, positions)
247
248 def __init__(self, *args, **kwargs) -> None:
249 super().__init__(*args, **kwargs)
250 self._einsum_recipe, self._tma_aligned_scales = compute_fp8_einsum_recipe(
251 self._o_proj_block_size
252 )
253 register_dsv41_o_proj_warmup(self)
254 # Per-tensor FP8 scale buffers + precomputed scalar BMM scales. Only the
255 # per-tensor FP8 cache path consumes these; bf16 reads ``self.scale``.
256 if self.kv_cache_torch_dtype != torch.float8_e4m3fn:
257 return
258 fp8_q_scale = 1.0
259 fp8_kv_scale = 1.0
260 self.register_buffer(
261 "_flashinfer_fp8_q_scale",
262 torch.tensor([fp8_q_scale], dtype=torch.float32),
263 persistent=False,
264 )
265 self.register_buffer(
266 "_flashinfer_fp8_q_scale_inv",
267 torch.tensor([1.0 / fp8_q_scale], dtype=torch.float32),
268 persistent=False,
269 )
270 self.register_buffer(
271 "_flashinfer_fp8_kv_scale",
272 torch.tensor([fp8_kv_scale], dtype=torch.float32),
273 persistent=False,
274 )
275 # TRTLLM-gen takes scalar scale args on a distinct C++ path vs
276 # one-element tensors, so these are Python floats.
277 self._flashinfer_fp8_bmm1_scale = self.scale * fp8_q_scale * fp8_kv_scale
278 self._flashinfer_fp8_bmm2_scale = fp8_kv_scale
279
280 def forward_mqa(
281 self,
282 q: torch.Tensor,
283 kv: torch.Tensor,
284 positions: torch.Tensor,
285 output: torch.Tensor,
286 ) -> None:
287 # The TRTLLM-gen kernel requires h_q in {64, 128}, so the output buffer
288 # is allocated at the padded head count while q arrives at the local
289 # head count; _forward pads q to match before the launcher.
290 assert output.shape[0] == q.shape[0] and output.shape[-1] == q.shape[-1], (
291 f"output buffer shape {output.shape} incompatible with q shape {q.shape}"
292 )
293 assert output.shape[1] >= q.shape[1], (
294 f"output heads {output.shape[1]} must be >= q heads {q.shape[1]}"
295 )
296 # Per-tensor FP8 q produces a bf16 attention output.
297 expected_output_dtype = (
298 torch.bfloat16 if q.dtype == torch.float8_e4m3fn else q.dtype
299 )
300 assert output.dtype == expected_output_dtype, (
301 f"output dtype {output.dtype} must match expected {expected_output_dtype} "
302 f"for q dtype {q.dtype}"
303 )
304
305 forward_context = get_forward_context()
306 attn_metadata = forward_context.attn_metadata
307 if attn_metadata is None:
308 # Warmup dummy run: FlashInfer reads the cache directly and lazily
309 # allocates its workspace, so nothing to reserve here.
310 output.zero_()
311 return
312
313 assert isinstance(attn_metadata, dict)
314 # Compressed-cache metadata lives on the kv-source layer's prefix;
315 # consumers share that cache and its block table.
316 flashmla_metadata = cast(
317 DeepseekV4FlashMLAMetadata | None,
318 attn_metadata.get(self.compressed_cache_prefix)
319 if self.compressed_cache_prefix is not None
320 else None,
321 )
322 swa_metadata = cast(
323 "DeepseekSparseSWAMetadata | None",
324 attn_metadata.get(self.swa_cache_layer.prefix),
325 )
326 assert swa_metadata is not None
327
328 swa_only = self.compress_ratio == 0
329 # SWA-only layers have no compressed KV cache; consumers read the kv
330 # source's cache.
331 self_kv_cache = None if swa_only else self._compressed_kv_cache()
332 swa_kv_cache = self.swa_cache_layer.kv_cache
333
334 self._forward(
335 q=q,
336 kv_cache=self_kv_cache,
337 swa_k_cache=swa_kv_cache,
338 swa_metadata=swa_metadata,
339 attn_metadata=flashmla_metadata,
340 swa_only=swa_only,
341 output=output,
342 )
343
344 def _build_sparse_index_metadata(
345 self,
346 kv_cache: torch.Tensor | None,
347 swa_k_cache: torch.Tensor,
348 swa_metadata: "DeepseekSparseSWAMetadata",
349 attn_metadata: DeepseekV4FlashMLAMetadata | None,
350 swa_only: bool,
351 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
352 """Build the combined sparse-index tensors for the mixed batch.
353
354 Returns ``(compressed_kv_cache, seq_lens, sparse_indices,
355 sparse_topk_lens)``.
356 """
357 num_decodes = swa_metadata.num_decodes
358 num_prefills = swa_metadata.num_prefills
359 num_decode_tokens = swa_metadata.num_decode_tokens
360 num_prefill_tokens = swa_metadata.num_prefill_tokens
361 num_reqs = num_decodes + num_prefills
362 num_tokens = num_decode_tokens + num_prefill_tokens
363
364 assert swa_metadata.seq_lens is not None
365 assert swa_metadata.query_start_loc is not None
366 assert swa_metadata.token_to_req_indices is not None
367 assert swa_metadata.decode_swa_indices is not None
368 assert swa_metadata.block_table is not None
369 assert swa_metadata.replay_start is not None
370
371 decode_swa_indices = swa_metadata.decode_swa_indices.reshape(
372 num_decode_tokens, swa_metadata.decode_swa_width
373 )
374 decode_compressed_topk_lens = None
375 decode_compressed_indices_are_local = False
376 decode_is_valid_token = None
377
378 if swa_only:
379 assert self.topk_indices_buffer is not None
380 compressed_kv_cache = swa_k_cache
381 decode_compressed_indices = None
382 prefill_topk_indices = self.topk_indices_buffer[
383 num_decode_tokens:num_tokens, :0
384 ]
385 compressed_block_table = None
386 compressed_block_size = swa_metadata.block_size
387 top_k = 0
388 else:
389 assert kv_cache is not None
390 assert attn_metadata is not None
391 assert self.topk_indices_buffer is not None
392 assert swa_metadata.is_valid_token is not None
393 compressed_kv_cache = kv_cache
394 compressed_block_table = attn_metadata.block_table[:num_reqs]
395 compressed_block_size = attn_metadata.block_size // self.compress_ratio
396
397 # Local indices filled by the index-source layer's indexer.
398 if num_prefill_tokens > 0:
399 prefill_topk_indices = self.topk_indices_buffer[
400 num_decode_tokens:num_tokens
401 ]
402 top_k = prefill_topk_indices.shape[-1]
403 else:
404 prefill_topk_indices = self.topk_indices_buffer[:0, :0]
405 top_k = 0
406
407 decode_compressed_indices_are_local = True
408 decode_is_valid_token = swa_metadata.is_valid_token[:num_decode_tokens]
409 if num_decode_tokens > 0:
410 decode_compressed_indices = self.topk_indices_buffer[:num_decode_tokens]
411 else:
412 # Keep the logical width aligned with the mixed-batch case so
413 # pure-prefill steps reuse the same Triton specialization.
414 decode_compressed_indices = prefill_topk_indices[:0]
415
416 query_start_loc = swa_metadata.query_start_loc[: num_reqs + 1]
417 seq_lens = swa_metadata.seq_lens[:num_reqs]
418 assert seq_lens.dtype == torch.int32
419 # SWA-only layers all build the same mixed sparse indices, so the first
420 # one caches them for the step; indexer layers depend on their own topk
421 # indices and stay uncached.
422 cached_sparse = (
423 swa_metadata.flashinfer_sparse_index_cache.get("swa_only")
424 if swa_only
425 else None
426 )
427 if cached_sparse is None:
428 swa_block_span = _packed_block_span(swa_k_cache)
429 compressed_block_span = _packed_block_span(compressed_kv_cache)
430 sparse_indices, sparse_topk_lens = build_flashinfer_mixed_sparse_indices(
431 decode_swa_indices,
432 decode_compressed_indices,
433 decode_compressed_topk_lens,
434 prefill_topk_indices[:num_prefill_tokens],
435 query_start_loc,
436 seq_lens,
437 swa_metadata.token_to_req_indices[:num_tokens],
438 swa_metadata.block_table[:num_reqs],
439 swa_metadata.block_size,
440 compressed_block_table,
441 compressed_block_size,
442 self.window_size,
443 self.compress_ratio,
444 top_k,
445 decode_compressed_indices_are_local=decode_compressed_indices_are_local,
446 decode_is_valid_token=decode_is_valid_token,
447 swa_block_span=swa_block_span,
448 compressed_block_span=compressed_block_span,
449 replay_start=swa_metadata.replay_start[:num_reqs],
450 )
451 if swa_only:
452 swa_metadata.flashinfer_sparse_index_cache["swa_only"] = (
453 sparse_indices,
454 sparse_topk_lens,
455 )
456 else:
457 sparse_indices, sparse_topk_lens = cached_sparse
458 return compressed_kv_cache, seq_lens, sparse_indices, sparse_topk_lens
459
460 def _forward(
461 self,
462 q: torch.Tensor,
463 kv_cache: torch.Tensor | None,
464 swa_k_cache: torch.Tensor,
465 swa_metadata: "DeepseekSparseSWAMetadata",
466 attn_metadata: DeepseekV4FlashMLAMetadata | None,
467 swa_only: bool,
468 output: torch.Tensor,
469 ) -> None:
470 assert self.kv_cache_torch_dtype in (torch.bfloat16, torch.float8_e4m3fn)
471 num_decodes = swa_metadata.num_decodes
472 num_prefills = swa_metadata.num_prefills
473 num_decode_tokens = swa_metadata.num_decode_tokens
474 num_prefill_tokens = swa_metadata.num_prefill_tokens
475 num_reqs = num_decodes + num_prefills
476 num_tokens = num_decode_tokens + num_prefill_tokens
477 if num_tokens == 0:
478 return
479
480 (
481 compressed_kv_cache,
482 seq_lens,
483 sparse_indices,
484 sparse_topk_lens,
485 ) = self._build_sparse_index_metadata(
486 kv_cache=kv_cache,
487 swa_k_cache=swa_k_cache,
488 swa_metadata=swa_metadata,
489 attn_metadata=attn_metadata,
490 swa_only=swa_only,
491 )
492
493 # CUDA graph execution can pad q/output past the scheduled token count;
494 # restrict to the real tokens (the launcher validates sparse indices).
495 query = q[:num_tokens]
496 output = output[:num_tokens]
497 bmm1_scale: float | torch.Tensor = self.scale
498 bmm2_scale: float | torch.Tensor = 1.0
499 if self.kv_cache_torch_dtype == torch.float8_e4m3fn:
500 assert query.dtype == torch.float8_e4m3fn
501 bmm1_scale = self._flashinfer_fp8_bmm1_scale
502 bmm2_scale = self._flashinfer_fp8_bmm2_scale
503 else:
504 assert query.dtype == torch.bfloat16
505 query = query.contiguous()
506
507 # The TRTLLM-gen sparse-MLA kernel requires h_q in {64, 128}; zero-pad
508 # the query heads to the allocated output head count. Padded heads attend
509 # to the shared KV and are sliced off downstream (output is padded too).
510 padded_heads = output.shape[1]
511 if query.shape[1] < padded_heads:
512 padded_query = query.new_zeros(
513 (query.shape[0], padded_heads, query.shape[2])
514 )
515 padded_query[:, : query.shape[1], :] = query
516 query = padded_query
517
518 workspace = _get_flashinfer_dsv4_workspace(q.device)
519 query_start_loc = swa_metadata.query_start_loc
520 query_start_loc_cpu = swa_metadata.query_start_loc_cpu
521 assert query_start_loc is not None and query_start_loc_cpu is not None
522
523 # Keep the TRTLLM-gen decode/prefill split: the launcher is tuned for
524 # uniform-q batches, and this avoids flattening mixed batches into one call.
525 if num_decode_tokens > 0:
526 decode_query = query[:num_decode_tokens]
527 decode_output = output[:num_decode_tokens]
528 decode_cu = query_start_loc[: num_decodes + 1]
529 decode_seq_lens = seq_lens[:num_decodes]
530 decode_topk_lens = sparse_topk_lens[:num_decode_tokens]
531 max_decode_query_len = swa_metadata.max_decode_query_len
532 if swa_metadata.decode_swa_width > self.window_size:
533 # DSpark's non-causal window extends past the fixed 128 SWA
534 # columns into the aliased compressed pool. Exclude padding,
535 # and expose the full block to each query instead of letting
536 # TRTLLM derive a causal SWA length from its query position.
537 assert swa_only
538 assert swa_metadata.flashinfer_decode_topk_lens is not None
539 assert swa_metadata.flashinfer_decode_seq_lens is not None
540 decode_topk_lens = swa_metadata.flashinfer_decode_topk_lens
541 decode_seq_lens = swa_metadata.flashinfer_decode_seq_lens
542 decode_query = decode_query.unsqueeze(1)
543 decode_output = decode_output.unsqueeze(1)
544 decode_cu = None
545 max_decode_query_len = 1
546 flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
547 query=decode_query,
548 swa_kv_cache=swa_k_cache,
549 workspace_buffer=workspace,
550 sparse_indices=sparse_indices[:num_decode_tokens],
551 sparse_indices_are_storage_offsets=True,
552 compressed_kv_cache=compressed_kv_cache,
553 sparse_topk_lens=decode_topk_lens,
554 seq_lens=decode_seq_lens,
555 out=decode_output,
556 bmm1_scale=bmm1_scale,
557 bmm2_scale=bmm2_scale,
558 sinks=self.attn_sink,
559 cum_seq_lens_q=decode_cu,
560 max_q_len=max_decode_query_len,
561 )
562
563 if num_prefill_tokens > 0:
564 # The prefill query view re-anchors at offset 0, so rebase the
565 # cumulative query offsets to start at 0.
566 prefill_cu = (
567 query_start_loc[num_decodes : num_reqs + 1]
568 - query_start_loc[num_decodes]
569 )
570 prefill_cu_cpu = query_start_loc_cpu[num_decodes : num_reqs + 1]
571 prefill_lens_cpu = prefill_cu_cpu[1:] - prefill_cu_cpu[:-1]
572 flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
573 query=query[num_decode_tokens:num_tokens],
574 swa_kv_cache=swa_k_cache,
575 workspace_buffer=workspace,
576 sparse_indices=sparse_indices[num_decode_tokens:num_tokens],
577 sparse_indices_are_storage_offsets=True,
578 compressed_kv_cache=compressed_kv_cache,
579 sparse_topk_lens=sparse_topk_lens[num_decode_tokens:num_tokens],
580 seq_lens=seq_lens[num_decodes:num_reqs],
581 out=output[num_decode_tokens:num_tokens],
582 bmm1_scale=bmm1_scale,
583 bmm2_scale=bmm2_scale,
584 sinks=self.attn_sink,
585 cum_seq_lens_q=prefill_cu,
586 max_q_len=int(prefill_lens_cpu.max().item()),
587 )
588
589
590 class DeepseekV4FlashInferSM120Attention(DeepseekV4Attention):
591 """DeepSeek V4 sparse MLA attention through FlashInfer's SM120 kernels."""
592
593 backend_cls = DeepseekV4FlashInferMLASparseBackend
594 swa_backend_cls = DeepseekSparseSWAFlashInferBackend
595 use_fp8_ds_mla_layout: ClassVar[bool] = True
596
597 @staticmethod
598 def _get_workspace(device: torch.device) -> torch.Tensor:
599 return _get_flashinfer_dsv4_workspace(device)
600
601 @staticmethod
602 def _as_sparse_cache(kv_cache: torch.Tensor) -> torch.Tensor:
603 if kv_cache.dtype == torch.float8_e4m3fn:
604 kv_cache = kv_cache.view(torch.uint8)
605 if kv_cache.dim() == 4:
606 return kv_cache
607 return kv_cache.unsqueeze(-2)
608
609 @classmethod
610 def get_padded_num_q_heads(cls, num_heads: int) -> int:
611 return _pad_to_supported_q_heads(num_heads)
612
613 def _o_proj(self, attn_out: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
614 return dsv41_o_proj(self, attn_out, positions)
615
616 def __init__(self, vllm_config: VllmConfig, *args, **kwargs) -> None:
617 super().__init__(vllm_config, *args, **kwargs)
618 from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120_config
619
620 required_topk = _required_sm120_sparse_topk(vllm_config, self.window_size)
621 if not has_flashinfer_sparse_mla_sm120_config(self.padded_heads, required_topk):
622 raise RuntimeError(
623 "FLASHINFER_MLA_SPARSE_DSV4 on SM120 requires a FlashInfer "
624 "DSV4 sparse MLA decode specialization for "
625 f"(num_q_heads={self.padded_heads}, top_k={required_topk}). "
626 "Install a FlashInfer build containing "
627 "flashinfer-ai/flashinfer#4380."
628 )
629 self._einsum_recipe, self._tma_aligned_scales = compute_fp8_einsum_recipe(
630 self._o_proj_block_size
631 )
632 # Per-tensor FP8 cache path scales.
633 if self.kv_cache_torch_dtype != torch.float8_e4m3fn:
634 return
635 fp8_q_scale = 1.0
636 fp8_kv_scale = 1.0
637 self.register_buffer(
638 "_flashinfer_fp8_q_scale",
639 torch.tensor([fp8_q_scale], dtype=torch.float32),
640 persistent=False,
641 )
642 self.register_buffer(
643 "_flashinfer_fp8_q_scale_inv",
644 torch.tensor([1.0 / fp8_q_scale], dtype=torch.float32),
645 persistent=False,
646 )
647 self.register_buffer(
648 "_flashinfer_fp8_kv_scale",
649 torch.tensor([fp8_kv_scale], dtype=torch.float32),
650 persistent=False,
651 )
652 # FlashInfer expects scalar scale arguments for this path.
653 self._flashinfer_fp8_bmm1_scale = self.scale * fp8_q_scale * fp8_kv_scale
654 self._flashinfer_fp8_bmm2_scale = fp8_kv_scale
655
656 def _reserve_empty_forward_workspace(self) -> None:
657 self._get_workspace(
658 torch.device("cuda", torch.accelerator.current_device_index())
659 )
660
661 def _forward_sparse_impl(
662 self,
663 q: torch.Tensor,
664 output: torch.Tensor,
665 flashmla_metadata: DeepseekV4FlashMLAMetadata | None,
666 swa_metadata: "DeepseekSparseSWAMetadata",
667 self_kv_cache: torch.Tensor | None,
668 swa_kv_cache: torch.Tensor,
669 swa_only: bool,
670 ) -> None:
671 num_decode_tokens = swa_metadata.num_decode_tokens
672 if swa_metadata.num_prefills > 0:
673 self._forward_prefill(
674 q=q[num_decode_tokens:],
675 compressed_k_cache=self_kv_cache,
676 swa_k_cache=swa_kv_cache,
677 output=output[num_decode_tokens:],
678 attn_metadata=flashmla_metadata,
679 swa_metadata=swa_metadata,
680 )
681 if swa_metadata.num_decodes > 0:
682 self._forward_decode(
683 q=q[:num_decode_tokens],
684 kv_cache=self_kv_cache,
685 swa_metadata=swa_metadata,
686 attn_metadata=flashmla_metadata,
687 swa_only=swa_only,
688 output=output[:num_decode_tokens],
689 )
690
691 def forward_mqa(
692 self,
693 q: torch.Tensor,
694 kv: torch.Tensor,
695 positions: torch.Tensor,
696 output: torch.Tensor,
697 ) -> None:
698 # Output may be padded to backend-supported head counts.
699 assert output.shape[0] == q.shape[0] and output.shape[-1] == q.shape[-1], (
700 f"output buffer shape {output.shape} incompatible with q shape {q.shape}"
701 )
702 assert output.shape[1] >= q.shape[1], (
703 f"output heads {output.shape[1]} must be >= q heads {q.shape[1]}"
704 )
705 # Per-tensor FP8 q produces a bf16 attention output.
706 expected_output_dtype = (
707 torch.bfloat16 if q.dtype == torch.float8_e4m3fn else q.dtype
708 )
709 assert output.dtype == expected_output_dtype, (
710 f"output dtype {output.dtype} must match expected {expected_output_dtype} "
711 f"for q dtype {q.dtype}"
712 )
713
714 forward_context = get_forward_context()
715 attn_metadata = forward_context.attn_metadata
716 if attn_metadata is None:
717 self._reserve_empty_forward_workspace()
718 output.zero_()
719 return
720
721 assert isinstance(attn_metadata, dict)
722 # Compressed-cache metadata lives on the kv-source layer's prefix;
723 # consumers share that cache and its block table.
724 flashmla_metadata = cast(
725 DeepseekV4FlashMLAMetadata | None,
726 attn_metadata.get(self.compressed_cache_prefix)
727 if self.compressed_cache_prefix is not None
728 else None,
729 )
730 swa_metadata = cast(
731 "DeepseekSparseSWAMetadata | None",
732 attn_metadata.get(self.swa_cache_layer.prefix),
733 )
734 assert swa_metadata is not None
735
736 swa_only = self.compress_ratio == 0
737 # SWA-only layers have no compressed KV cache; consumers read the kv
738 # source's cache.
739 self_kv_cache = None if swa_only else self._compressed_kv_cache()
740 swa_kv_cache = self.swa_cache_layer.kv_cache
741
742 self._forward_sparse_impl(
743 q=q,
744 output=output,
745 flashmla_metadata=flashmla_metadata,
746 swa_metadata=swa_metadata,
747 self_kv_cache=self_kv_cache,
748 swa_kv_cache=swa_kv_cache,
749 swa_only=swa_only,
750 )
751
752 def _prepare_query(self, q: torch.Tensor, output: torch.Tensor) -> torch.Tensor:
753 if self.kv_cache_torch_dtype == torch.float8_e4m3fn:
754 assert q.dtype == torch.float8_e4m3fn
755 q = q.to(torch.bfloat16)
756 else:
757 assert q.dtype == torch.bfloat16
758 padded_heads = output.shape[1]
759 if q.shape[1] < padded_heads:
760 padded_query = q.new_zeros((q.shape[0], padded_heads, q.shape[2]))
761 padded_query[:, : q.shape[1], :] = q
762 q = padded_query
763 return q.contiguous()
764
765 def _forward_decode(
766 self,
767 q: torch.Tensor,
768 kv_cache: torch.Tensor | None,
769 swa_metadata: "DeepseekSparseSWAMetadata",
770 attn_metadata: DeepseekV4FlashMLAMetadata | None,
771 swa_only: bool,
772 output: torch.Tensor,
773 ) -> None:
774 num_decodes = swa_metadata.num_decodes
775 num_decode_tokens = swa_metadata.num_decode_tokens
776
777 extra_sparse_indices = None
778 extra_sparse_lengths = None
779 if not swa_only:
780 if attn_metadata is None:
781 raise RuntimeError(
782 "Sparse MLA metadata is required for compressed layers."
783 )
784 if swa_metadata.is_valid_token is None:
785 raise RuntimeError(
786 "SWA validity metadata is required for compressed layers."
787 )
788 if self.topk_indices_buffer is None:
789 raise RuntimeError(
790 "Compressed-layer decode requires top-k indices from the indexer."
791 )
792 # Local indices filled by the index-source layer's indexer.
793 is_valid = swa_metadata.is_valid_token[:num_decode_tokens]
794 block_size = attn_metadata.block_size // self.compress_ratio
795 global_indices, extra_sparse_lengths = compute_global_topk_indices_and_lens(
796 self.topk_indices_buffer[:num_decode_tokens],
797 swa_metadata.token_to_req_indices,
798 attn_metadata.block_table[:num_decodes],
799 block_size,
800 is_valid,
801 )
802 extra_sparse_indices = global_indices.view(num_decode_tokens, 1, -1)
803
804 swa_indices = swa_metadata.decode_swa_indices
805 swa_lens = swa_metadata.decode_swa_lens
806 assert swa_indices is not None
807 assert swa_lens is not None
808 q = self._prepare_query(q, output)
809 swa_cache = self._as_sparse_cache(self.swa_cache_layer.kv_cache)
810 extra_cache = self._as_sparse_cache(kv_cache) if kv_cache is not None else None
811 if extra_cache is not None and extra_sparse_indices is None:
812 raise RuntimeError(
813 "Compressed sparse MLA decode requires compressed sparse indices."
814 )
815 flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
816 query=q,
817 swa_kv_cache=swa_cache,
818 workspace_buffer=self._get_workspace(q.device),
819 sparse_indices=swa_indices,
820 compressed_kv_cache=extra_cache,
821 out=output,
822 bmm1_scale=self.scale,
823 sinks=self.attn_sink,
824 kv_layout="NHD",
825 swa_topk_lens=swa_lens,
826 extra_sparse_indices=extra_sparse_indices,
827 extra_sparse_topk_lens=extra_sparse_lengths,
828 )
829
830 def _forward_prefill(
831 self,
832 q: torch.Tensor,
833 compressed_k_cache: torch.Tensor | None,
834 swa_k_cache: torch.Tensor,
835 output: torch.Tensor,
836 attn_metadata: DeepseekV4FlashMLAMetadata | None,
837 swa_metadata: "DeepseekSparseSWAMetadata",
838 ) -> None:
839 swa_only = self.compress_ratio == 0
840
841 num_prefills = swa_metadata.num_prefills
842 num_decodes = swa_metadata.num_decodes
843 num_decode_tokens = swa_metadata.num_decode_tokens
844 num_prefill_tokens = swa_metadata.num_prefill_tokens
845
846 query_start_loc_cpu = swa_metadata.query_start_loc_cpu
847 assert query_start_loc_cpu is not None
848 prefill_token_base = query_start_loc_cpu[num_decodes]
849
850 extra_sparse_indices: torch.Tensor | None = None
851 extra_sparse_lengths: torch.Tensor | None = None
852 if not swa_only:
853 if self.topk_indices_buffer is None:
854 raise RuntimeError(
855 "Compressed-layer prefill requires top-k indices from the indexer."
856 )
857 if attn_metadata is None:
858 raise RuntimeError("Compressed-layer prefill metadata is missing.")
859 if swa_metadata.token_to_req_indices is None:
860 raise RuntimeError(
861 "Compressed-layer prefill request mapping is missing."
862 )
863 if swa_metadata.is_valid_token is None:
864 raise RuntimeError(
865 "Compressed-layer prefill validity metadata is missing."
866 )
867 # Local indices filled by the index-source layer's indexer.
868 local_topk_indices = self.topk_indices_buffer[
869 num_decode_tokens : num_decode_tokens + num_prefill_tokens
870 ]
871 prefill_token_slice = slice(
872 num_decode_tokens, num_decode_tokens + num_prefill_tokens
873 )
874 block_size = attn_metadata.block_size // self.compress_ratio
875 extra_sparse_indices, extra_sparse_lengths = (
876 compute_global_topk_indices_and_lens(
877 local_topk_indices,
878 swa_metadata.token_to_req_indices[prefill_token_slice],
879 attn_metadata.block_table,
880 block_size,
881 swa_metadata.is_valid_token[prefill_token_slice],
882 )
883 )
884
885 assert swa_metadata.prefill_swa_indices is not None
886 assert swa_metadata.prefill_swa_lens is not None
887
888 q = self._prepare_query(q, output)
889 swa_kv_paged = self._as_sparse_cache(swa_k_cache)
890 if swa_only:
891 extra_kv_paged = None
892 else:
893 if compressed_k_cache is None:
894 raise RuntimeError(
895 "Compressed sparse MLA layers require their compressed KV cache."
896 )
897 extra_kv_paged = self._as_sparse_cache(compressed_k_cache)
898
899 num_chunks = (
900 num_prefills + self.PREFILL_CHUNK_SIZE - 1
901 ) // self.PREFILL_CHUNK_SIZE
902 for chunk_idx in range(num_chunks):
903 chunk_start = chunk_idx * self.PREFILL_CHUNK_SIZE
904 chunk_end = min(chunk_start + self.PREFILL_CHUNK_SIZE, num_prefills)
905 query_start = (
906 query_start_loc_cpu[num_decodes + chunk_start] - prefill_token_base
907 )
908 query_end = (
909 query_start_loc_cpu[num_decodes + chunk_end] - prefill_token_base
910 )
911
912 extra_sparse_indices_chunk = (
913 extra_sparse_indices[query_start:query_end]
914 if extra_sparse_indices is not None
915 else None
916 )
917 extra_sparse_lengths_chunk = (
918 extra_sparse_lengths[query_start:query_end]
919 if extra_sparse_lengths is not None
920 else None
921 )
922
923 q_chunk = q[query_start:query_end]
924 swa_indices_chunk = swa_metadata.prefill_swa_indices[query_start:query_end]
925 swa_lens_chunk = swa_metadata.prefill_swa_lens[query_start:query_end]
926 if extra_kv_paged is not None and extra_sparse_indices_chunk is None:
927 raise RuntimeError(
928 "Compressed sparse MLA prefill requires compressed sparse indices."
929 )
930 flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
931 query=q_chunk,
932 swa_kv_cache=swa_kv_paged,
933 workspace_buffer=self._get_workspace(q.device),
934 sparse_indices=swa_indices_chunk,
935 compressed_kv_cache=extra_kv_paged,
936 out=output[query_start:query_end],
937 bmm1_scale=self.scale,
938 sinks=self.attn_sink,
939 kv_layout="NHD",
940 swa_topk_lens=swa_lens_chunk,
941 extra_sparse_indices=extra_sparse_indices_chunk,
942 extra_sparse_topk_lens=extra_sparse_lengths_chunk,
943 )