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