main
py 767 lines 27.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """FlashInfer sparse MLA attention backend."""
4
5 from dataclasses import dataclass
6 from typing import TYPE_CHECKING, ClassVar
7
8 import torch
9
10 from vllm import envs
11 from vllm.config import VllmConfig
12 from vllm.config.cache import CacheDType
13 from vllm.logger import init_logger
14 from vllm.model_executor.layers.attention.sparse_mla_attention import (
15 SparseMLACommonImpl,
16 SparseMLACommonMetadata,
17 SparseMLACommonMetadataBuilder,
18 )
19 from vllm.platforms import current_platform
20 from vllm.platforms.interface import DeviceCapability
21 from vllm.utils.torch_utils import is_quantized_kv_cache
22 from vllm.v1.attention.backend import (
23 AttentionBackend,
24 AttentionCGSupport,
25 AttentionLayer,
26 AttentionType,
27 MLAAttentionImpl,
28 MultipleOf,
29 )
30 from vllm.v1.attention.backends.mla.index_group import HiSparseMLAIndexGroup
31 from vllm.v1.attention.backends.mla.sparse_utils import (
32 flat_kv_row_view,
33 prepare_sparse_mla_safe_lengths,
34 triton_convert_req_index_to_global_index,
35 triton_filter_and_convert_dcp_index,
36 )
37 from vllm.v1.kv_cache_interface import AttentionSpec
38
39 if TYPE_CHECKING:
40 from vllm.model_executor.models.deepseek_v2 import Indexer
41
42 logger = init_logger(__name__)
43
44
45 class _FlashInferMLASparseBackendBase(AttentionBackend):
46 """Common metadata for concrete FlashInfer sparse MLA backends."""
47
48 @staticmethod
49 def get_name() -> str:
50 return "FLASHINFER_MLA_SPARSE"
51
52 @staticmethod
53 def get_builder_cls() -> type["FlashInferMLASparseMetadataBuilder"]:
54 return FlashInferMLASparseMetadataBuilder
55
56 @classmethod
57 def get_supported_head_sizes(cls) -> list[int]:
58 # 576 = 512 NoPE + 64 RoPE (with-rope layout); 512 = 512 NoPE only
59 # (no-rope layout, qk_rope_head_dim == 0). Both share D_V = 512 and are
60 # served by the TRTLLM-GEN sparse MLA kernel.
61 return [512, 576]
62
63 @classmethod
64 def is_mla(cls) -> bool:
65 return True
66
67 @classmethod
68 def is_sparse(cls) -> bool:
69 return True
70
71
72 class FlashInferMLASparseTRTLLMBackend(_FlashInferMLASparseBackendBase):
73 """FlashInfer sparse MLA backend using the TRTLLM-gen launcher."""
74
75 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
76 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
77 "auto",
78 "float16",
79 "bfloat16",
80 "fp8",
81 "fp8_e4m3",
82 ]
83
84 @staticmethod
85 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
86 return [32, 64]
87
88 @staticmethod
89 def get_impl_cls() -> type[MLAAttentionImpl]:
90 return FlashInferMLASparseImpl
91
92 @staticmethod
93 def get_builder_cls() -> type["FlashInferMLASparseTRTLLMMetadataBuilder"]:
94 return FlashInferMLASparseTRTLLMMetadataBuilder
95
96 @classmethod
97 def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
98 return capability.major == 10
99
100 @classmethod
101 def supports_combination(
102 cls,
103 head_size: int,
104 dtype: torch.dtype,
105 kv_cache_dtype: CacheDType | None,
106 block_size: int | None,
107 use_mla: bool,
108 has_sink: bool,
109 use_sparse: bool,
110 use_mm_prefix: bool,
111 device_capability: DeviceCapability,
112 ) -> str | None:
113 from vllm.config import get_current_vllm_config
114
115 vllm_config = get_current_vllm_config()
116 parallel_config = vllm_config.parallel_config
117 if (
118 parallel_config.prefill_context_parallel_size > 1
119 and parallel_config.decode_context_parallel_size > 1
120 ):
121 return (
122 "FLASHINFER_MLA_SPARSE does not support combined PCP+DCP; "
123 "use FLASHMLA_SPARSE, which gathers each DCP KV shard before "
124 "running the rank-local PCP prefill queries"
125 )
126 if kv_cache_dtype == "fp8_ds_mla":
127 return (
128 "FLASHINFER_MLA_SPARSE SM10 does not support fp8_ds_mla kv-cache dtype"
129 )
130
131 # FlashInfer MLA sparse SM10 kernel requires qk_nope_head_dim in [128, 192].
132 if vllm_config.model_config is not None:
133 hf_text_config = vllm_config.model_config.hf_text_config
134 qk_nope_head_dim = hf_text_config.qk_nope_head_dim
135 qk_rope_head_dim = hf_text_config.qk_rope_head_dim
136 kv_lora_rank = hf_text_config.kv_lora_rank
137 if qk_rope_head_dim == 0:
138 # Native no-rope MLA: FlashInfer only ships the one shape
139 # (nope_mla_dimensions in flashinfer.mla._core).
140 if qk_nope_head_dim != 256 or kv_lora_rank != 512:
141 return (
142 "FlashInfer native no-rope MLA requires "
143 "qk_nope_head_dim=256 and kv_lora_rank=512, but got "
144 f"qk_nope_head_dim={qk_nope_head_dim}, "
145 f"kv_lora_rank={kv_lora_rank}"
146 )
147 elif qk_nope_head_dim not in [128, 192]:
148 return (
149 "FlashInfer MLA Sparse kernel requires qk_nope_head_dim "
150 f"in [128, 192], but got {qk_nope_head_dim}"
151 )
152 # Check for index_topk which indicates sparse model
153 if not hasattr(hf_text_config, "index_topk"):
154 return "FlashInfer MLA Sparse requires model with index_topk config"
155 return None
156
157
158 class FlashInferMLASparseSM120Backend(_FlashInferMLASparseBackendBase):
159 """FlashInfer sparse MLA backend for SM120."""
160
161 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
162 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
163 "auto",
164 "fp8",
165 "fp8_e4m3",
166 "fp8_ds_mla",
167 ]
168
169 @staticmethod
170 def get_name() -> str:
171 return "FLASHINFER_MLA_SPARSE_SM120"
172
173 @staticmethod
174 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
175 return [64, 256]
176
177 @staticmethod
178 def get_impl_cls() -> type[MLAAttentionImpl]:
179 from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm120 import (
180 FlashInferMLASparseSM120Impl,
181 )
182
183 return FlashInferMLASparseSM120Impl
184
185 @classmethod
186 def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
187 return capability.major == 12
188
189 @classmethod
190 def supports_combination(
191 cls,
192 head_size: int,
193 dtype: torch.dtype,
194 kv_cache_dtype: CacheDType | None,
195 block_size: int | None,
196 use_mla: bool,
197 has_sink: bool,
198 use_sparse: bool,
199 use_mm_prefix: bool,
200 device_capability: DeviceCapability,
201 ) -> str | None:
202 from vllm.config import get_current_vllm_config
203 from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
204
205 if not has_flashinfer_sparse_mla_sm120():
206 return (
207 "FLASHINFER_MLA_SPARSE_SM120 requires FlashInfer's "
208 "sparse MLA decode API"
209 )
210 if dtype != torch.bfloat16:
211 return "dtype not supported"
212 if kv_cache_dtype not in (
213 None,
214 "auto",
215 "fp8",
216 "fp8_e4m3",
217 "fp8_ds_mla",
218 ):
219 return "kv_cache_dtype not supported"
220 vllm_config = get_current_vllm_config()
221 if vllm_config.model_config is not None:
222 hf_text_config = vllm_config.model_config.hf_text_config
223 index_topk = getattr(hf_text_config, "index_topk", None)
224 if index_topk is None:
225 return (
226 "FLASHINFER_MLA_SPARSE_SM120 requires a model with "
227 "index_topk config"
228 )
229 if int(index_topk) != 2048:
230 return (
231 "FLASHINFER_MLA_SPARSE_SM120 requires index_topk=2048; "
232 f"got {index_topk}"
233 )
234 return None
235
236
237 @dataclass
238 class FlashInferMLASparseMetadata(SparseMLACommonMetadata):
239 """Attention metadata for FlashInfer MLA Sparse backend."""
240
241
242 class FlashInferMLASparseMetadataBuilder(
243 SparseMLACommonMetadataBuilder[FlashInferMLASparseMetadata]
244 ):
245 """Builder for FlashInfer MLA Sparse attention metadata."""
246
247 metadata_cls = FlashInferMLASparseMetadata
248 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
249
250 def __init__(
251 self,
252 kv_cache_spec: AttentionSpec,
253 layer_names: list[str],
254 vllm_config: VllmConfig,
255 device: torch.device,
256 ) -> None:
257 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
258
259 num_q_heads = vllm_config.model_config.get_num_attention_heads(
260 vllm_config.parallel_config
261 )
262 threshold = {8: 128, 16: 128, 32: 128, 64: 256, 128: 1024}.get(
263 num_q_heads, 1024
264 )
265 self._init_reorder_batch_threshold(
266 threshold,
267 supports_spec_as_decode=True,
268 supports_dcp_with_varlen=True,
269 )
270
271
272 class FlashInferMLASparseTRTLLMMetadataBuilder(FlashInferMLASparseMetadataBuilder):
273 """Metadata builder for the SM100 TRT-LLM sparse MLA kernel."""
274
275 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
276
277 def __init__(
278 self,
279 kv_cache_spec: AttentionSpec,
280 layer_names: list[str],
281 vllm_config: VllmConfig,
282 device: torch.device,
283 ) -> None:
284 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
285
286 # Under DCP the workspace must hold the trtllm-gen softmax-stats slab.
287 # The buffer address is baked into CUDA graphs, so it has to reach its
288 # final size here, before the first forward/capture (no lazy regrow).
289 if self.dcp_world_size > 1:
290 num_q_heads = vllm_config.model_config.get_num_attention_heads(
291 vllm_config.parallel_config
292 )
293 _get_workspace_buffer(
294 device,
295 _required_workspace_bytes(
296 self.dcp_world_size,
297 num_q_heads,
298 vllm_config.scheduler_config.max_num_batched_tokens,
299 ),
300 )
301
302
303 # Global workspace buffer (lazily initialized)
304 _fi_sparse_workspace: torch.Tensor | None = None
305
306 # trtllm-gen carves a softmax-stats slab from the workspace whenever LSE is
307 # requested (FlashInfer csrc/trtllm_fmha_kernel_launcher.cu, unchanged from
308 # v0.6.14 through current main):
309 # sizeof(float2) * num_qo_heads * batch_size * round_up(max_q_len, 256)
310 # + 1 MiB guard
311 # forward_mqa always passes q_len == 1, so each (head, token) pair costs
312 # round_up(1, 256) == 256 slots.
313 _TRTLLM_GEN_SOFTMAX_STAT_BYTES = 8 # sizeof(float2)
314 _TRTLLM_GEN_SOFTMAX_SLOTS_PER_TOKEN = 256 # round_up(q_len=1, 256)
315 _TRTLLM_GEN_SOFTMAX_GUARD_BYTES = 1024 * 1024
316
317 # Keep in sync with the VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE default in
318 # vllm/envs.py.
319 _DEFAULT_WORKSPACE_BUFFER_SIZE = 394 * 1024 * 1024
320
321
322 def compute_trtllm_sparse_mla_workspace_bytes(
323 base_workspace_bytes: int,
324 dcp_world_size: int,
325 num_heads_per_rank: int,
326 max_num_batched_tokens: int,
327 ) -> int:
328 """Workspace bytes needed by the trtllm-gen sparse MLA decode kernel.
329
330 Under DCP the query is all-gathered across the DCP group in the head dim
331 and the kernel is asked for LSE, so trtllm-gen carves the softmax-stats
332 slab described above from the workspace before its (batch-independent)
333 counter and scratch regions. ``base_workspace_bytes`` must stay available
334 for those regions, so the slab is added on top of it (see #50781).
335
336 Without DCP no LSE is requested, no slab is carved, and the base size is
337 returned unchanged.
338 """
339 if dcp_world_size <= 1:
340 return base_workspace_bytes
341 softmax_bytes = (
342 _TRTLLM_GEN_SOFTMAX_STAT_BYTES
343 * (num_heads_per_rank * dcp_world_size)
344 * max_num_batched_tokens
345 * _TRTLLM_GEN_SOFTMAX_SLOTS_PER_TOKEN
346 + _TRTLLM_GEN_SOFTMAX_GUARD_BYTES
347 )
348 return base_workspace_bytes + softmax_bytes
349
350
351 def _required_workspace_bytes(
352 dcp_world_size: int,
353 num_heads_per_rank: int,
354 max_num_batched_tokens: int,
355 ) -> int:
356 """Resolve the workspace size, honoring an explicit env override."""
357 computed = compute_trtllm_sparse_mla_workspace_bytes(
358 _DEFAULT_WORKSPACE_BUFFER_SIZE,
359 dcp_world_size,
360 num_heads_per_rank,
361 max_num_batched_tokens,
362 )
363 if not envs.is_set("VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE"):
364 return computed
365 env_bytes = envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE
366 if env_bytes < computed:
367 logger.warning_once(
368 "VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE=%d is below the %d bytes "
369 "computed for the sparse MLA workspace with dcp_world_size=%d, "
370 "%d heads/rank and max_num_batched_tokens=%d. Respecting the "
371 "override, but the trtllm-gen kernel may crash with a workspace "
372 "overflow; set VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE=%d or unset "
373 "it to use the computed size.",
374 env_bytes,
375 computed,
376 dcp_world_size,
377 num_heads_per_rank,
378 max_num_batched_tokens,
379 computed,
380 )
381 return env_bytes
382
383
384 def _get_workspace_buffer(
385 device: torch.device, min_bytes: int | None = None
386 ) -> torch.Tensor:
387 global _fi_sparse_workspace
388 required = (
389 min_bytes
390 if min_bytes is not None
391 else envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE
392 )
393 if _fi_sparse_workspace is None or _fi_sparse_workspace.numel() < required:
394 # FlashInfer's CuteDSL MLA-decode tactic requires an int8 workspace;
395 # the trtllm-gen path views it as uint8, so int8 is safe for all backends.
396 _fi_sparse_workspace = torch.zeros(
397 required,
398 dtype=torch.int8,
399 device=device,
400 )
401 return _fi_sparse_workspace
402
403
404 class FlashInferMLASparseImpl(SparseMLACommonImpl[FlashInferMLASparseMetadata]):
405 """FlashInfer MLA Sparse implementation.
406
407 Uses the TRT-LLM MLA kernel with sparse_mla_top_k parameter for
408 sparse attention computation.
409 """
410
411 can_return_lse_for_decode: bool = True
412 supports_dcp: bool = True
413 lse_base_on_e: bool = False
414
415 def __init__(
416 self,
417 num_heads: int,
418 head_size: int,
419 scale: float,
420 num_kv_heads: int,
421 alibi_slopes: list[float] | None,
422 sliding_window: int | None,
423 kv_cache_dtype: str,
424 logits_soft_cap: float | None,
425 attn_type: str,
426 kv_sharing_target_layer_name: str | None,
427 # MLA Specific Arguments
428 topk_indices_buffer: torch.Tensor | None = None,
429 indexer: "Indexer | None" = None,
430 **mla_args,
431 ) -> None:
432 unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap]
433 if any(unsupported_features):
434 raise NotImplementedError(
435 "FlashInferMLASparseImpl does not support one of the following: "
436 "alibi_slopes, sliding_window, logits_soft_cap"
437 )
438
439 if attn_type != AttentionType.DECODER:
440 raise NotImplementedError(
441 "Encoder self-attention and "
442 "encoder/decoder cross-attention "
443 "are not implemented for "
444 "FlashInferMLASparseImpl"
445 )
446
447 super().__init__(
448 num_heads,
449 head_size,
450 scale,
451 num_kv_heads,
452 alibi_slopes,
453 sliding_window,
454 kv_cache_dtype,
455 logits_soft_cap,
456 attn_type,
457 kv_sharing_target_layer_name,
458 indexer=indexer,
459 topk_indices_buffer=topk_indices_buffer,
460 **mla_args,
461 )
462
463 self._workspace_buffer: torch.Tensor | None = None
464 self.bmm1_scale: float | None = None
465 self.bmm2_scale: float | None = None
466
467 # Native no-rope MLA additionally requires a per-query-token active
468 # top-k length tensor.
469 self.is_nope_mla = self.qk_rope_head_dim == 0
470
471 # fp8 query quantization is required when using fp8 kv_cache,
472 # as the TRTLLM-GEN sparse MLA kernel requires matching dtypes
473 # for query and kv_cache (mixed bf16+fp8 is not supported).
474 self.supports_quant_query_input = True
475
476 def forward_mqa(
477 self,
478 q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
479 kv_c_and_k_pe_cache: torch.Tensor,
480 attn_metadata: FlashInferMLASparseMetadata,
481 layer: AttentionLayer,
482 ) -> tuple[torch.Tensor, torch.Tensor | None]:
483 if isinstance(q, tuple):
484 ql_nope, q_pe = q
485 if q_pe.shape[-1] == 0 and ql_nope.is_contiguous():
486 q = ql_nope
487 else:
488 q = torch.cat(q, dim=-1)
489
490 num_actual_toks = q.shape[0]
491
492 assert self.topk_indices_buffer is not None
493 topk_indices = self.topk_indices_buffer[:num_actual_toks]
494
495 self._prepare_mqa_kernel(layer, q.device)
496
497 index_group = self.index_group
498 if isinstance(index_group, HiSparseMLAIndexGroup):
499 num_decode_tokens = attn_metadata.num_decode_tokens
500 decode_out: torch.Tensor | None = None
501 decode_lse: torch.Tensor | None = None
502 if num_decode_tokens > 0:
503 physical_topk, valid_counts = (
504 index_group.convert_logical_to_physical_topk(
505 self.index_group_index,
506 topk_indices[:num_decode_tokens],
507 attn_metadata,
508 block_stride_rows=None,
509 return_valid_counts=True,
510 )
511 )
512 decode_out, decode_lse = self._run_mqa_kernel(
513 q[:num_decode_tokens],
514 index_group.physical_kv_cache(self.index_group_index).view(
515 kv_c_and_k_pe_cache.dtype
516 ),
517 physical_topk,
518 valid_counts,
519 )
520 if num_decode_tokens == num_actual_toks:
521 return decode_out, decode_lse
522
523 cache = index_group.cache(self.index_group_index)
524 if num_decode_tokens == 0 and cache.all_context_pages_resident:
525 physical_topk, valid_counts = (
526 index_group.convert_logical_to_physical_topk(
527 self.index_group_index,
528 topk_indices,
529 attn_metadata,
530 block_stride_rows=None,
531 return_valid_counts=True,
532 )
533 )
534 return self._run_mqa_kernel(
535 q,
536 index_group.physical_kv_cache(self.index_group_index).view(
537 kv_c_and_k_pe_cache.dtype
538 ),
539 physical_topk,
540 valid_counts,
541 )
542
543 prefill_cache, block_table, req_ids = index_group.stage_prefill_rows(
544 self.index_group_index, kv_c_and_k_pe_cache, attn_metadata
545 )
546 prefill_indices, prefill_lens = triton_convert_req_index_to_global_index(
547 req_ids,
548 block_table,
549 topk_indices[num_decode_tokens:],
550 BLOCK_SIZE=attn_metadata.block_size,
551 NUM_TOPK_TOKENS=topk_indices.shape[1],
552 return_valid_counts=True,
553 )
554 prefill_out, prefill_lse = self._run_mqa_kernel(
555 q[num_decode_tokens:],
556 prefill_cache,
557 prefill_indices,
558 prefill_lens,
559 )
560 if decode_out is None:
561 return prefill_out, prefill_lse
562 output = torch.cat((decode_out, prefill_out))
563 if decode_lse is None:
564 return output, None
565 assert prefill_lse is not None
566 return output, torch.cat((decode_lse, prefill_lse))
567
568 _, block_stride_rows = flat_kv_row_view(
569 kv_c_and_k_pe_cache, attn_metadata.block_size
570 )
571
572 if self.dcp_world_size > 1:
573 topk_indices_physical, seq_lens = triton_filter_and_convert_dcp_index(
574 attn_metadata.req_id_per_token[:num_actual_toks],
575 attn_metadata.block_table,
576 topk_indices,
577 dcp_size=self.dcp_world_size,
578 dcp_rank=self.dcp_rank,
579 cp_kv_cache_interleave_size=(attn_metadata.cp_kv_cache_interleave_size),
580 BLOCK_SIZE=attn_metadata.block_size,
581 BLOCK_STRIDE_ROWS=block_stride_rows,
582 NUM_TOPK_TOKENS=topk_indices.shape[1],
583 return_valid_counts=True,
584 )
585 else:
586 topk_indices_physical, seq_lens = self._convert_logical_to_physical_topk(
587 topk_indices,
588 attn_metadata,
589 block_stride_rows=block_stride_rows,
590 return_valid_counts=True,
591 )
592
593 return self._run_mqa_kernel(
594 q,
595 kv_c_and_k_pe_cache,
596 topk_indices_physical,
597 seq_lens,
598 )
599
600 def _prepare_mqa_kernel(
601 self,
602 layer: AttentionLayer,
603 device: torch.device,
604 ) -> None:
605 if self._workspace_buffer is None:
606 self._workspace_buffer = _get_workspace_buffer(device)
607
608 if self.bmm1_scale is None:
609 self.bmm1_scale = self.scale
610 if is_quantized_kv_cache(self.kv_cache_dtype):
611 self.bmm1_scale *= layer._q_scale_float * layer._k_scale_float
612 if self.bmm2_scale is None:
613 self.bmm2_scale = 1.0
614 if is_quantized_kv_cache(self.kv_cache_dtype):
615 self.bmm2_scale *= layer._k_scale_float
616
617 def autotune_hisparse_decode(self, layer: AttentionLayer) -> None:
618 """Autotune the largest legal HiSparse decode batch."""
619 assert isinstance(self.index_group, HiSparseMLAIndexGroup)
620 cache = self.index_group.cache(self.index_group_index)
621 assert self.topk_indices_buffer is not None
622
623 runtime = cache.runtime
624 kv_cache = runtime.hot.attention_cache
625 num_tokens = runtime.max_num_reqs
626 topk_tokens = self.topk_indices_buffer.shape[1]
627 self._prepare_mqa_kernel(layer, kv_cache.device)
628
629 q_dtype = (
630 current_platform.fp8_dtype()
631 if is_quantized_kv_cache(self.kv_cache_dtype)
632 else kv_cache.dtype
633 )
634 q = torch.zeros(
635 (
636 num_tokens,
637 self.num_heads,
638 self.kv_lora_rank + self.qk_rope_head_dim,
639 ),
640 dtype=q_dtype,
641 device=kv_cache.device,
642 )
643 topk_indices = (
644 torch.arange(
645 topk_tokens,
646 dtype=torch.int32,
647 device=kv_cache.device,
648 )
649 .expand(num_tokens, -1)
650 .contiguous()
651 )
652 seq_lens = torch.full(
653 (num_tokens,),
654 topk_tokens,
655 dtype=torch.int32,
656 device=kv_cache.device,
657 )
658 self._run_mqa_kernel(q, kv_cache, topk_indices, seq_lens)
659
660 def _run_mqa_kernel(
661 self,
662 q: torch.Tensor,
663 kv_cache: torch.Tensor,
664 topk_indices: torch.Tensor,
665 seq_lens: torch.Tensor,
666 ) -> tuple[torch.Tensor, torch.Tensor | None]:
667 assert self._workspace_buffer is not None
668 assert self.bmm1_scale is not None
669 assert self.bmm2_scale is not None
670
671 from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
672
673 kv_cache = kv_cache.view(q.dtype)
674
675 # Single-token sparse decode. trtllm-gen requires the q_len_per_request
676 # dim, but the sparse attention mask is fully per-token (each query token
677 # carries its own top-k index row), so unsqueeze is sufficient and
678 # correct. The MTP/multi-token q_len grouping is a perf-only layout and is
679 # deferred until MTP is validated end-to-end for this backend.
680 query = q.unsqueeze(1)
681 block_tables = topk_indices.unsqueeze(1)
682 seq_lens_arg = seq_lens
683
684 # page_table width = topk buffer width, which kpool widens past
685 # index_topk (topk_tokens) and rounds up to a multiple of 128. The
686 # kernel treats sparse_mla_top_k as the page-table *capacity* and bounds
687 # the active per-query length by ``seq_lens`` (the compacted valid
688 # count), so the -1 padding slots past seq_lens are never attended to.
689 # Use the actual buffer width instead of the fixed topk_tokens, which
690 # mismatches the page_table when index_kpool > 1.
691 sparse_topk_capacity = topk_indices.shape[1]
692
693 extra_kwargs: dict[str, torch.Tensor] = {}
694 needs_empty_query_guard = self.need_to_return_lse_for_decode or isinstance(
695 self.index_group, HiSparseMLAIndexGroup
696 )
697 if self.is_nope_mla:
698 # Resident TP queries have nonempty selections. Preserve empty-query
699 # handling for DCP's local selections and host-backed HiSparse.
700 topk_lens = seq_lens
701 if needs_empty_query_guard:
702 topk_lens = prepare_sparse_mla_safe_lengths(topk_indices, seq_lens)
703 extra_kwargs["sparse_mla_top_k_lens"] = topk_lens
704
705 kernel_out = trtllm_batch_decode_with_kv_cache_mla(
706 query=query,
707 kv_cache=kv_cache.unsqueeze(1),
708 workspace_buffer=self._workspace_buffer,
709 qk_nope_head_dim=self.qk_nope_head_dim,
710 kv_lora_rank=self.kv_lora_rank,
711 qk_rope_head_dim=self.qk_rope_head_dim,
712 block_tables=block_tables,
713 seq_lens=seq_lens_arg,
714 max_seq_len=sparse_topk_capacity,
715 bmm1_scale=self.bmm1_scale,
716 bmm2_scale=self.bmm2_scale,
717 sparse_mla_top_k=sparse_topk_capacity,
718 return_lse=self.need_to_return_lse_for_decode,
719 **extra_kwargs,
720 )
721 if self.need_to_return_lse_for_decode:
722 assert isinstance(kernel_out, tuple)
723 o, lse = kernel_out
724 else:
725 assert isinstance(kernel_out, torch.Tensor)
726 o = kernel_out
727 lse = None
728
729 out = o.view(-1, o.shape[-2], o.shape[-1])
730 if lse is not None:
731 lse = self._normalize_lse(lse, out.shape[0], out.shape[1])
732 if self.is_nope_mla and needs_empty_query_guard:
733 empty_queries = seq_lens == 0
734 if lse is not None:
735 # DCP combine already suppresses outputs with zero LSE weight.
736 lse.masked_fill_(empty_queries[:, None], float("-inf"))
737 else:
738 out.masked_fill_(empty_queries[:, None, None], 0.0)
739 elif lse is not None:
740 empty_rows = (topk_indices == -1).all(dim=-1)
741 out.masked_fill_(empty_rows.view(-1, 1, 1), 0.0)
742 lse.masked_fill_(empty_rows.view(-1, 1), float("-inf"))
743 return out, lse
744
745 @staticmethod
746 def _normalize_lse(
747 lse: torch.Tensor,
748 num_tokens: int,
749 num_heads: int,
750 ) -> torch.Tensor:
751 # FlashInfer returns the decode LSE either as 2D (num_tokens, num_heads)
752 # or 3D ((num_tokens, num_heads, 1) / (num_tokens, 1, num_heads)).
753 # Collapse all of these to the (num_tokens, num_heads) the shared DCP
754 # reducer expects.
755 if lse.dim() == 3:
756 if lse.shape[-1] == 1:
757 lse = lse.squeeze(-1)
758 elif lse.shape[1] == 1:
759 lse = lse.squeeze(1)
760 elif lse.shape[0] * lse.shape[1] == num_tokens:
761 lse = lse.reshape(num_tokens, lse.shape[-1])
762 if lse.shape != (num_tokens, num_heads):
763 raise RuntimeError(
764 "Unexpected FlashInfer sparse MLA LSE shape: "
765 f"{tuple(lse.shape)}, expected ({num_tokens}, {num_heads})."
766 )
767 return lse