[M3] Improve indexer for long-context decode (sm100) (#48582)

Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>

Thien Tran committed Jul 17, 2026 at 09:12 UTC fe784ff22e630a31fd798f392b01e0a75c18f047
7 files changed +756 -63
tests/kernels/attention/test_minimax_m3.py
+118 -8
@@ -185,6 +185,43 @@ def _assert_topk_indices_equal_unordered(
185 assert set(actual_row) == set(expected_row)
186
187
188 +def _reference_decode_index_score(
189 + idx_q: torch.Tensor,
190 + index_kv_cache: torch.Tensor,
191 + block_table: torch.Tensor,
192 + seq_lens: torch.Tensor,
193 + decode_query_len: int,
194 + score_block_stride: int,
195 +) -> torch.Tensor:
196 + total_q, num_idx_heads, _ = idx_q.shape
197 + out = torch.full(
198 + (num_idx_heads, total_q, score_block_stride),
199 + -float("inf"),
200 + device=idx_q.device,
201 + dtype=torch.float32,
202 + )
203 + for req_id, seq_len in enumerate(seq_lens.tolist()):
204 + num_blocks = (seq_len + BLOCK_SIZE - 1) // BLOCK_SIZE
205 + token_start = req_id * decode_query_len
206 + q = idx_q[token_start : token_start + decode_query_len].float()
207 + pages = block_table[req_id, :num_blocks]
208 + k = index_kv_cache[pages].reshape(num_blocks * BLOCK_SIZE, -1).float()
209 + score = torch.einsum("qhd,kd->hqk", q, k)
210 + q_pos = (
211 + seq_len
212 + - decode_query_len
213 + + torch.arange(decode_query_len, device=idx_q.device)
214 + )
215 + k_pos = torch.arange(k.shape[0], device=idx_q.device)
216 + score.masked_fill_(k_pos[None, :] > q_pos[:, None], -float("inf"))
217 + out[:, token_start : token_start + decode_query_len, :num_blocks] = (
218 + score.reshape(num_idx_heads, decode_query_len, num_blocks, BLOCK_SIZE)
219 + .max(dim=3)
220 + .values
221 + )
222 + return out
223 +
224 +
225 def test_prefill_index_topk_correctness():
226 topk = 6
227 init_blocks = 0
@@ -383,8 +420,8 @@ def test_fmha_sm100_indexer_matches_reference(q_lens, prefix_lens, index_dtype):
420 _assert_topk_indices_equal_unordered(actual, expected)
421
422
386 -# Full impl-level parity: drive both MiniMaxM3IndexerMSAImpl (fmha_sm100 score +
387 -# Triton top-k) and MiniMaxM3IndexerTritonImpl through their real metadata
423 +# Full impl-level parity: drive both MiniMaxM3IndexerMSAImpl (fmha/CuteDSL score
424 +# + unified top-k) and MiniMaxM3IndexerTritonImpl through their real metadata
425 # builders on the SAME CommonAttentionMetadata + index cache, and assert the
426 # selected blocks agree. This exercises all the metadata the impl/kernels consume
427 # (decode/prefill split, cu_seqlens_q rebasing, prefix_lens, kv_indices gather,
@@ -395,7 +432,8 @@ def test_fmha_sm100_indexer_matches_reference(q_lens, prefix_lens, index_dtype):
432 reason="fmha_sm100 indexer requires SM100 (Blackwell).",
433 )
434 @pytest.mark.parametrize("topk", [8, 16])
398 -def test_msa_indexer_impl_matches_triton(topk, monkeypatch):
435 +@pytest.mark.parametrize("index_dtype", [torch.bfloat16, torch.float8_e4m3fn])
436 +def test_msa_indexer_impl_matches_triton(topk, index_dtype, monkeypatch):
437 import vllm.models.minimax_m3.common.indexer as indexer_mod
438 from tests.v1.attention.utils import (
439 BatchSpec,
@@ -439,13 +477,13 @@ def test_msa_indexer_impl_matches_triton(topk, monkeypatch):
477 block_table = common.block_table_tensor
478 num_pages = int(block_table.max().item()) + 1
479 index_cache = torch.zeros(
442 - num_pages, BLOCK_SIZE, head_dim, device=device, dtype=DTYPE
480 + num_pages, BLOCK_SIZE, head_dim, device=device, dtype=index_dtype
481 )
482 for r, seq_len in enumerate(batch.seq_lens):
483 for b in range((seq_len + BLOCK_SIZE - 1) // BLOCK_SIZE):
484 index_cache[block_table[r, b]] = float(b + 1)
485 index_q = torch.ones(
448 - num_tokens, num_idx_heads * head_dim, device=device, dtype=DTYPE
486 + num_tokens, num_idx_heads * head_dim, device=device, dtype=index_dtype
487 )
488
489 spec = MLAAttentionSpec(
@@ -588,9 +626,8 @@ def test_decode_index_topk_correctness(
626 )
627 @pytest.mark.parametrize("num_idx_heads", [1, 4])
628 def test_decode_index_topk_fp8(num_idx_heads: int):
591 - """The fp8 (e4m3) indexer cache feeds the Triton decode kernel on the MSA
592 - path. The kernel must score in fp32 (no scaling) so its top-k matches a
593 - reference computed from the dequantized fp8 values."""
629 + """The standalone Triton path must score FP8 inputs in FP32 so its top-k
630 + matches a reference computed from the dequantized FP8 values."""
631 torch.manual_seed(0)
632 topk, init_blocks, local_blocks, head_dim = 8, 0, 1, 128
633 decode_query_len = 1
@@ -641,6 +678,79 @@ def test_decode_index_topk_fp8(num_idx_heads: int):
678 _assert_topk_indices_equal_unordered(actual, expected)
679
680
681 +@pytest.mark.skipif(
682 + not current_platform.is_device_capability_family(100),
683 + reason="CuteDSL index decode score requires Blackwell.",
684 +)
685 +@pytest.mark.parametrize(
686 + ("dtype", "decode_query_len", "max_decode_query_len"),
687 + [
688 + (torch.bfloat16, 1, 1),
689 + (torch.bfloat16, 3, 8),
690 + (torch.float8_e4m3fn, 1, 1),
691 + (torch.float8_e4m3fn, 3, 8),
692 + (torch.float8_e4m3fn, 8, 8),
693 + ],
694 +)
695 +def test_decode_index_score_cutedsl_correctness(
696 + dtype: torch.dtype,
697 + decode_query_len: int,
698 + max_decode_query_len: int,
699 +):
700 + pytest.importorskip("cutlass")
701 + from vllm.models.minimax_m3.nvidia.ops import (
702 + minimax_m3_index_decode_score_cutedsl,
703 + )
704 +
705 + torch.manual_seed(0)
706 + init_blocks, local_blocks = 0, 0
707 + num_idx_heads, head_dim = 4, 128
708 + active_seq_lens = torch.tensor((1025, 4097), device="cuda", dtype=torch.int32)
709 + batch = active_seq_lens.numel()
710 + total_q = batch * decode_query_len
711 + max_seq_len = int(active_seq_lens.max())
712 + max_blocks = (max_seq_len + BLOCK_SIZE - 1) // BLOCK_SIZE
713 + score_block_stride = ((max_blocks + 15) // 16) * 16
714 + num_pages = batch * max_blocks
715 + block_table = torch.randperm(num_pages, device="cuda", dtype=torch.int32).reshape(
716 + batch, max_blocks
717 + )
718 + idx_q = torch.randn(total_q, num_idx_heads, head_dim, device="cuda").to(dtype)
719 + index_kv_cache = torch.randn(num_pages, BLOCK_SIZE, head_dim, device="cuda").to(
720 + dtype
721 + )
722 + unified_score = torch.full(
723 + (total_q, num_idx_heads, score_block_stride),
724 + -float("inf"),
725 + device="cuda",
726 + dtype=torch.float32,
727 + )
728 + score = unified_score.transpose(0, 1)
729 +
730 + minimax_m3_index_decode_score_cutedsl(
731 + idx_q,
732 + index_kv_cache,
733 + block_table,
734 + active_seq_lens,
735 + max_seq_len=max_seq_len,
736 + init_blocks=init_blocks,
737 + local_blocks=local_blocks,
738 + num_kv_heads=num_idx_heads,
739 + decode_query_len=decode_query_len,
740 + max_decode_query_len=max_decode_query_len,
741 + score_out=score,
742 + )
743 + expected = _reference_decode_index_score(
744 + idx_q,
745 + index_kv_cache,
746 + block_table,
747 + active_seq_lens,
748 + decode_query_len,
749 + score_block_stride,
750 + )
751 + torch.testing.assert_close(score, expected)
752 +
753 +
754 # Sparse attention kernels.
755 def _reference_sparse_attn(
756 q: torch.Tensor,
vllm/cute_utils/__init__.py
+48 -13
@@ -1,11 +1,33 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -from cutlass import BFloat16, Float32, Int64, Uint32, cute
3 +import torch
4 +from cutlass import (
5 + BFloat16,
6 + Float8E4M3FN,
7 + Float16,
8 + Float32,
9 + Int32,
10 + Int64,
11 + Uint32,
12 + cute,
13 +)
14 from cutlass._mlir import ir
15 from cutlass._mlir.dialects import llvm, vector
16 from cutlass.cute.nvgpu import cpasync
17 from cutlass.cutlass_dsl import T, dsl_user_op
18
19 +_TORCH_TO_CUTE_DTYPE = {
20 + torch.bfloat16: BFloat16,
21 + torch.float8_e4m3fn: Float8E4M3FN,
22 +}
23 +
24 +_CUTE_TO_PTX_DTYPE = {
25 + BFloat16: "bf16",
26 + Float16: "f16",
27 + Float8E4M3FN: "e4m3",
28 + Float32: "f32",
29 +}
30 +
31 # https://github.com/NVIDIA/cutlass/blob/v4.3.2/include/cute/arch/copy_sm90_desc.hpp#L193-L197
32 EVICT_NORMAL = Int64(0x1000000000000000)
33 EVICT_FIRST = Int64(0x12F0000000000000)
@@ -63,22 +85,35 @@ def fence_before_tma_store(*, loc=None, ip=None):
85
86
87 @dsl_user_op
66 -def mma_bf16(
67 - a: cute.TensorSSA, b: cute.TensorSSA, c: cute.TensorSSA, *, loc=None, ip=None
68 -):
69 - if a.element_type == BFloat16:
70 - a = cute.recast_tensor(a, Uint32)
71 - if b.element_type == BFloat16:
72 - b = cute.recast_tensor(b, Uint32)
73 -
74 - mlir_ty = Float32.mlir_type
88 +def mma_sync(a, b, c: cute.Tensor, *, loc=None, ip=None):
89 + a_ty = _CUTE_TO_PTX_DTYPE[a.element_type]
90 + b_ty = _CUTE_TO_PTX_DTYPE[b.element_type]
91 + c_ty = _CUTE_TO_PTX_DTYPE[c.element_type]
92 + mlir_ty = c.element_type.mlir_type
93 + K = 256 // a.element_type.width # 32B
94 +
95 + # Recast expects tensor-backed fragments; materialize SSA fragments here so
96 + # callsites can pass converted FP8 fragments directly.
97 + if isinstance(a, cute.TensorSSA):
98 + a_ = cute.make_rmem_tensor_like(a)
99 + a_.store(a, loc=loc, ip=ip)
100 + a = a_
101 + if isinstance(b, cute.TensorSSA):
102 + b_ = cute.make_rmem_tensor_like(b)
103 + b_.store(b, loc=loc, ip=ip)
104 + b = b_
105 +
106 + a = cute.recast_tensor(a, Int32, loc=loc, ip=ip)
107 + b = cute.recast_tensor(b, Int32, loc=loc, ip=ip)
108 out = llvm.inline_asm(
109 llvm.StructType.get_literal([mlir_ty] * 4),
110 [a[i].ir_value(loc=loc, ip=ip) for i in range(4)]
111 + [b[i].ir_value(loc=loc, ip=ip) for i in range(2)]
112 + [c[i].ir_value(loc=loc, ip=ip) for i in range(4)],
80 - "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 "
81 - "{$0, $1, $2, $3}, {$4, $5, $6, $7}, {$8, $9}, "
113 + f"mma.sync.aligned.m16n8k{K}.row.col.{c_ty}.{a_ty}.{b_ty}.{c_ty} "
114 + "{$0, $1, $2, $3}, "
115 + "{$4, $5, $6, $7}, "
116 + "{$8, $9}, "
117 "{$10, $11, $12, $13};",
118 "=f,=f,=f,=f,r,r,r,r,r,r,f,f,f,f",
119 has_side_effects=False,
@@ -92,7 +127,7 @@ def mma_bf16(
127 loc=loc,
128 ip=ip,
129 )
95 - return cute.TensorSSA(vec, 4, Float32)
130 + return cute.TensorSSA(vec, 4, c.element_type)
131
132
133 def _bf16x2_unary(asm: str, a: Uint32, *, loc=None, ip=None) -> Uint32:
vllm/cute_utils/cvt.py
+26
@@ -84,6 +84,32 @@ def fp8x4_to_bf16x4(x: Uint32, *, loc=None, ip=None) -> cute.TensorSSA:
84 return cute.TensorSSA(vec, 2, Uint32)
85
86
87 +@dsl_user_op
88 +def fp8x4_to_fp16x4(x: Uint32, *, loc=None, ip=None) -> cute.TensorSSA:
89 + out = llvm.inline_asm(
90 + llvm.StructType.get_literal([T.i32()] * 2),
91 + [x.ir_value(loc=loc, ip=ip)],
92 + "{\n\t"
93 + ".reg .b16 lo, hi;\n\t"
94 + "mov.b32 {lo, hi}, $2;\n\t"
95 + "cvt.rn.f16x2.e4m3x2 $0, lo;\n\t"
96 + "cvt.rn.f16x2.e4m3x2 $1, hi;\n\t"
97 + "}\n",
98 + "=r,=r,r",
99 + has_side_effects=False,
100 + is_align_stack=False,
101 + loc=loc,
102 + ip=ip,
103 + )
104 + vec = vector.from_elements(
105 + ir.VectorType.get([2], T.i32(), loc=loc),
106 + [llvm.extractvalue(T.i32(), out, [i], loc=loc, ip=ip) for i in range(2)],
107 + loc=loc,
108 + ip=ip,
109 + )
110 + return cute.TensorSSA(vec, 2, Uint32)
111 +
112 +
113 @dsl_user_op
114 def fp32x4_to_fp8x4(
115 a0: Float32,
vllm/model_executor/layers/mamba/ops/gdn_chunk_cutedsl/kernel_kkt_inv_uw.py
+63 -36
@@ -16,7 +16,7 @@ from vllm.cute_utils import (
16 _tcgen05,
17 cvt,
18 fence_before_tma_store,
19 - mma_bf16,
19 + mma_sync,
20 simple_tma_copy,
21 )
22
@@ -536,14 +536,16 @@ class Sm100ChunkUWKernel:
536 zeros_f32.fill(0.0)
537
538 Ai_bf16 = cute.make_rmem_tensor(8, BFloat16)
539 - mma_B_bf16 = cute.make_rmem_tensor(8, BFloat16)
540 - M_bf16 = cute.make_rmem_tensor(8, BFloat16)
539 + mma_B_bf16 = cute.make_rmem_tensor((4, 2), BFloat16)
540 + M_bf16 = cute.make_rmem_tensor((4, 2), BFloat16)
541 acc = cute.make_rmem_tensor((4, 2), Float32)
542
543 - # share the same storage
543 + # ldmatrix copies require rank-1 destination views.
544 + mma_B_bf16_ldsm = cute.group_modes(mma_B_bf16, 0, 2)
545 +
546 + # Packed aliases for BF16x2 arithmetic.
547 Ai = cute.recast_tensor(Ai_bf16, Uint32)
545 - mma_B = cute.logical_divide(cute.recast_tensor(mma_B_bf16, Uint32), 2)
546 - M = cute.logical_divide(cute.recast_tensor(M_bf16, Uint32), 2)
548 + M = cute.recast_tensor(M_bf16, Uint32)
549
550 # construct rmem-backed identity matrix
551 eye = cute.make_rmem_tensor(4, Uint32)
@@ -563,7 +565,11 @@ class Sm100ChunkUWKernel:
565 Ai_f32 = cute.logical_divide(cvt.bf16x2_to_fp32x2(Ai), 4)
566
567 # M is holding -(I+A), stay constant throughout the iterations
566 - cute.copy(ldsm_trans_atom, sA_ldsm[None, (warp_id_, warp_id_)], M_bf16)
568 + cute.copy(
569 + ldsm_trans_atom,
570 + sA_ldsm[None, (warp_id_, warp_id_)],
571 + cute.group_modes(M_bf16, 0, 2),
572 + )
573 for i in cutlass.range_constexpr(4):
574 M[i] = _bf16x2_sub(_bf16x2_neg(eye[i]), M[i])
575
@@ -572,8 +578,8 @@ class Sm100ChunkUWKernel:
578 # First MMA: -AiM = Ai @ (-M)
579 cute.copy(stsm_atom, Ai_bf16, sA_ldsm[None, (warp_id_, warp_id_)])
580 cute.arch.sync_warp()
575 - acc[None, 0] = mma_bf16(Ai, M[None, 0], zeros_f32)
576 - acc[None, 1] = mma_bf16(Ai, M[None, 1], zeros_f32)
581 + acc[None, 0] = mma_sync(Ai_bf16, M_bf16[None, 0], zeros_f32)
582 + acc[None, 1] = mma_sync(Ai_bf16, M_bf16[None, 1], zeros_f32)
583 Ai_bf16.store(acc.load().to(BFloat16))
584
585 # Second MMA: Ai_new = 2Ai + (-AiM) @ Ai
@@ -582,10 +588,14 @@ class Sm100ChunkUWKernel:
588 cute.copy(
589 ldsm_trans_atom,
590 sA_ldsm[None, (warp_id_, warp_id_)],
585 - mma_B_bf16,
591 + mma_B_bf16_ldsm,
592 + )
593 + Ai_f32[None, 0] = mma_sync(
594 + Ai_bf16, mma_B_bf16[None, 0], Ai_f32[None, 0]
595 + )
596 + Ai_f32[None, 1] = mma_sync(
597 + Ai_bf16, mma_B_bf16[None, 1], Ai_f32[None, 1]
598 )
587 - Ai_f32[None, 0] = mma_bf16(Ai, mma_B[None, 0], Ai_f32[None, 0])
588 - Ai_f32[None, 1] = mma_bf16(Ai, mma_B[None, 1], Ai_f32[None, 1])
599 Ai_bf16.store(Ai_f32.load().to(BFloat16))
600
601 cute.copy(stsm_atom, Ai_bf16, sAi_ldsm[None, (warp_id_, warp_id_)])
@@ -613,23 +623,24 @@ class Sm100ChunkUWKernel:
623 neg_Ai = cute.make_rmem_tensor(4, Uint32)
624 for i in cutlass.range_constexpr(4):
625 neg_Ai[i] = _bf16x2_neg(Ai[i])
626 + neg_Ai_bf16 = cute.recast_tensor(neg_Ai, BFloat16)
627
628 cute.copy(
629 ldsm_trans_atom,
630 sA_ldsm[None, (warp_id_, warp_id_ - 1)],
620 - mma_B_bf16,
631 + mma_B_bf16_ldsm,
632 )
622 - acc[None, 0] = mma_bf16(neg_Ai, mma_B[None, 0], zeros_f32)
623 - acc[None, 1] = mma_bf16(neg_Ai, mma_B[None, 1], zeros_f32)
633 + acc[None, 0] = mma_sync(neg_Ai_bf16, mma_B_bf16[None, 0], zeros_f32)
634 + acc[None, 1] = mma_sync(neg_Ai_bf16, mma_B_bf16[None, 1], zeros_f32)
635 Ai_bf16.store(acc.load().to(BFloat16))
636
637 cute.copy(
638 ldsm_trans_atom,
639 sAi_ldsm[None, (warp_id_ - 1, warp_id_ - 1)],
629 - mma_B_bf16,
640 + mma_B_bf16_ldsm,
641 )
631 - acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
632 - acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
642 + acc[None, 0] = mma_sync(Ai_bf16, mma_B_bf16[None, 0], zeros_f32)
643 + acc[None, 1] = mma_sync(Ai_bf16, mma_B_bf16[None, 1], zeros_f32)
644 Ai_bf16.store(acc.load().to(BFloat16))
645 store_ab_abg(
646 acc,
@@ -660,10 +671,10 @@ class Sm100ChunkUWKernel:
671 cute.copy(
672 ldsm_trans_atom,
673 sAi_ldsm[None, (tile_col, tile_col)],
663 - mma_B_bf16,
674 + mma_B_bf16_ldsm,
675 )
665 - acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
666 - acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
676 + acc[None, 0] = mma_sync(Ai_bf16, mma_B_bf16[None, 0], zeros_f32)
677 + acc[None, 1] = mma_sync(Ai_bf16, mma_B_bf16[None, 1], zeros_f32)
678
679 cute.copy(
680 ldsm_atom, sA_ldsm[None, (warp_id_, tile_col + 1)], Ai_bf16
@@ -671,10 +682,10 @@ class Sm100ChunkUWKernel:
682 cute.copy(
683 ldsm_trans_atom,
684 sAi_ldsm[None, (tile_col + 1, tile_col)],
674 - mma_B_bf16,
685 + mma_B_bf16_ldsm,
686 )
676 - acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], acc[None, 0])
677 - acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], acc[None, 1])
687 + acc[None, 0] = mma_sync(Ai_bf16, mma_B_bf16[None, 0], acc[None, 0])
688 + acc[None, 1] = mma_sync(Ai_bf16, mma_B_bf16[None, 1], acc[None, 1])
689
690 tmp = cute.make_rmem_tensor(8, BFloat16)
691 tmp.store(acc.load().to(BFloat16))
@@ -687,10 +698,10 @@ class Sm100ChunkUWKernel:
698 cute.copy(
699 ldsm_trans_atom,
700 sAi_ldsm[None, (warp_id_, tile_col)],
690 - mma_B_bf16,
701 + mma_B_bf16_ldsm,
702 )
692 - acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
693 - acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
703 + acc[None, 0] = mma_sync(Ai_bf16, mma_B_bf16[None, 0], zeros_f32)
704 + acc[None, 1] = mma_sync(Ai_bf16, mma_B_bf16[None, 1], zeros_f32)
705 tmp.store(acc.load().to(BFloat16))
706 cute.copy(stsm_atom, tmp, sAi_ldsm[None, (warp_id_, tile_col)])
707 store_ab_abg(
@@ -708,15 +719,27 @@ class Sm100ChunkUWKernel:
719 # warp3: Ai30 = -Ai33 @ (A30 @ Ai00 + A31 @ Ai10 + A32 @ Ai20)
720 if warp_id_ == 3:
721 cute.copy(ldsm_atom, sA_ldsm[None, (3, 0)], Ai_bf16)
711 - cute.copy(ldsm_trans_atom, sAi_ldsm[None, (0, 0)], mma_B_bf16)
712 - acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
713 - acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
722 + cute.copy(
723 + ldsm_trans_atom,
724 + sAi_ldsm[None, (0, 0)],
725 + mma_B_bf16_ldsm,
726 + )
727 + acc[None, 0] = mma_sync(Ai_bf16, mma_B_bf16[None, 0], zeros_f32)
728 + acc[None, 1] = mma_sync(Ai_bf16, mma_B_bf16[None, 1], zeros_f32)
729
730 for i in cutlass.range_constexpr(1, 3):
731 cute.copy(ldsm_atom, sA_ldsm[None, (3, i)], Ai_bf16)
717 - cute.copy(ldsm_trans_atom, sAi_ldsm[None, (i, 0)], mma_B_bf16)
718 - acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], acc[None, 0])
719 - acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], acc[None, 1])
732 + cute.copy(
733 + ldsm_trans_atom,
734 + sAi_ldsm[None, (i, 0)],
735 + mma_B_bf16_ldsm,
736 + )
737 + acc[None, 0] = mma_sync(
738 + Ai_bf16, mma_B_bf16[None, 0], acc[None, 0]
739 + )
740 + acc[None, 1] = mma_sync(
741 + Ai_bf16, mma_B_bf16[None, 1], acc[None, 1]
742 + )
743
744 tmp = cute.make_rmem_tensor(8, BFloat16)
745 tmp.store(acc.load().to(BFloat16))
@@ -726,9 +749,13 @@ class Sm100ChunkUWKernel:
749 cute.copy(ldsm_atom, sAi_ldsm[None, (3, 3)], Ai_bf16)
750 for i in cutlass.range_constexpr(4):
751 Ai[i] = _bf16x2_neg(Ai[i])
729 - cute.copy(ldsm_trans_atom, sAi_ldsm[None, (3, 0)], mma_B_bf16)
730 - acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
731 - acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
752 + cute.copy(
753 + ldsm_trans_atom,
754 + sAi_ldsm[None, (3, 0)],
755 + mma_B_bf16_ldsm,
756 + )
757 + acc[None, 0] = mma_sync(Ai_bf16, mma_B_bf16[None, 0], zeros_f32)
758 + acc[None, 1] = mma_sync(Ai_bf16, mma_B_bf16[None, 1], zeros_f32)
759 tmp.store(acc.load().to(BFloat16))
760 cute.copy(stsm_atom, tmp, sAi_ldsm[None, (3, 0)])
761 store_ab_abg(
vllm/models/minimax_m3/nvidia/indexer_msa.py
+13 -6
@@ -13,10 +13,9 @@ Prefill scores with ``fmha_sm100``'s score-only (``OnlyScore``) path (much faste
13 than Triton for the wide prefill score, benchmarked ~3-5x), writing its
14 ``max_score`` straight into the buffer's prefill region (stride-aware, no copy).
15
16 -Decode scores with the Triton split-K ``minimax_m3_index_decode_score`` (a
17 -purpose-built vector x matrix score, no wasted tensor-core tiles, 256-way
18 -split-K, cudagraph-safe by shape-constant grids), writing into the decode
19 -region. Its tuning heuristics are kept; only the top-k is shared with prefill.
16 +Decode scores with CuteDSL when the flattened query tile is supported and fall
17 +back to Triton otherwise, writing into the decode region. Only the top-k is
18 +shared with prefill.
19
20 ``fmha_sm100`` imports are function-local so this module is import-safe on
21 AMD / non-SM100.
@@ -39,6 +38,7 @@ from vllm.models.minimax_m3.common.indexer import (
38 from vllm.models.minimax_m3.common.ops.index_topk import (
39 minimax_m3_index_decode_score,
40 )
41 +from vllm.models.minimax_m3.nvidia.ops import minimax_m3_index_decode_score_cutedsl
42 from vllm.v1.attention.backend import (
43 AttentionBackend,
44 AttentionCGSupport,
@@ -257,7 +257,7 @@ class MiniMaxM3IndexerMSAMetadataBuilder(MiniMaxM3IndexerMetadataBuilder):
257
258
259 class MiniMaxM3IndexerMSAImpl(MiniMaxM3IndexerImpl):
260 - """Decode: Triton fused score+top-k. Prefill: fmha_sm100 OnlyScore + top-k."""
260 + """Decode: CuteDSL/Triton score. Prefill: fmha_sm100 OnlyScore + top-k."""
261
262 indexer_backend_cls: ClassVar[type[AttentionBackend]] = MiniMaxM3IndexerMSABackend
263
@@ -296,7 +296,14 @@ class MiniMaxM3IndexerMSAImpl(MiniMaxM3IndexerImpl):
296 # writes by strides). Top-k is deferred to the single unified call below.
297 if md.decode is not None:
298 d = md.decode
299 - minimax_m3_index_decode_score(
299 + # max_decode_query_len avoids recompiles across runtime decode sizes.
300 + # Fall back when the flattened Q tile gets too wide for this kernel.
301 + decode_score = (
302 + minimax_m3_index_decode_score_cutedsl
303 + if self.num_index_heads * d.max_decode_query_len <= 32
304 + else minimax_m3_index_decode_score
305 + )
306 + decode_score(
307 index_q[:nd],
308 kv,
309 d.block_table,
vllm/models/minimax_m3/nvidia/ops/__init__.py new
+5
@@ -0,0 +1,5 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from .index_decode_score import minimax_m3_index_decode_score_cutedsl
4 +
5 +__all__ = ["minimax_m3_index_decode_score_cutedsl"]
vllm/models/minimax_m3/nvidia/ops/index_decode_score.py new
+483
@@ -0,0 +1,483 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""CuteDSL MiniMax M3 index decode score kernel.
4 +
5 +The kernel computes decode-time index block scores with TMA + ``mma.sync``.
6 +We use ``mma.sync`` instead of tcgen05 because this score GEMM has a very small
7 +N dimension and benefits more from higher CTA occupancy than from a deeper
8 +single-CTA tcgen05 pipeline.
9 +
10 +The implementation should be portable to SM90/SM120 in principle, but it is
11 +currently validated only for SM100.
12 +"""
13 +
14 +from functools import cache
15 +
16 +import cutlass
17 +import torch
18 +from cuda.bindings.driver import CUstream
19 +from cutlass import Float8E4M3FN, Float16, Float32, Int32, Int64, Uint32, cute
20 +from cutlass.cute.nvgpu import cpasync, warp
21 +from quack.compile_utils import make_fake_tensor
22 +
23 +from vllm.cute_utils import (
24 + _TORCH_TO_CUTE_DTYPE,
25 + EVICT_FIRST,
26 + cvt,
27 + mma_sync,
28 + simple_tma_copy,
29 +)
30 +
31 +
32 +@cute.jit
33 +def _fp8_to_f16_mma_fragments(src: cute.Tensor):
34 + src_elems = cute.size(src)
35 + src_u32 = cute.recast_tensor(src, Uint32)
36 + src_f16 = cute.make_rmem_tensor(src_elems, Float16)
37 + src_f16_u32 = cute.recast_tensor(src_f16, Uint32)
38 + # This packed conversion is faster and emits fewer SASS instructions than
39 + # src.load().to(Float16).
40 + for i in cutlass.range_constexpr(src_elems // 4):
41 + converted = cvt.fp8x4_to_fp16x4(src_u32[i])
42 + src_f16_u32[i * 2] = converted[0]
43 + src_f16_u32[i * 2 + 1] = converted[1]
44 + lower = cute.make_rmem_tensor(src_elems // 2, Float16)
45 + upper = cute.make_rmem_tensor(src_elems // 2, Float16)
46 +
47 + # FP8 ldmatrix gives four consecutive values along K. Split each group
48 + # into the lower two and upper two values for two FP16 MMA k-fragments.
49 + for i in cutlass.range_constexpr(src_elems // 2):
50 + lower[i] = src_f16[(i // 2) * 4 + i % 2]
51 + upper[i] = src_f16[(i // 2) * 4 + 2 + i % 2]
52 + return lower, upper
53 +
54 +
55 +class IndexDecodeScoreKernel:
56 + BLOCK_K = 128
57 + BAR_MMA = 1
58 + num_stages = 2
59 +
60 + def __init__(
61 + self,
62 + dtype: type[cutlass.Numeric],
63 + num_heads: int,
64 + max_decode_query_len: int,
65 + split_k: int,
66 + head_dim: int = 128,
67 + ):
68 + self.dtype = dtype
69 + self.num_heads = num_heads
70 + self.max_decode_query_len = max_decode_query_len
71 + self.split_k = split_k
72 + self.head_dim = head_dim
73 +
74 + @cute.jit
75 + def __call__(
76 + self,
77 + gQ: cute.Tensor, # [bs * runtime_decode_query_len, num_heads, head_dim]
78 + gK_cache: cute.Tensor, # [num_pages, page_size, head_dim]
79 + block_table: cute.Tensor, # [bs, max_pages]
80 + score: cute.Tensor, # [num_heads, bs * runtime_decode_query_len, max_pages]
81 + seq_lens: cute.Tensor, # [bs]
82 + stream: CUstream,
83 + ):
84 + dtype = self.dtype
85 + num_heads = self.num_heads
86 + head_dim = self.head_dim
87 + BLOCK_K = self.BLOCK_K
88 + num_stages = self.num_stages
89 + MAX_DQL = self.max_decode_query_len
90 + BLOCK_Q = num_heads * MAX_DQL
91 + assert BLOCK_Q <= 32
92 +
93 + batch = seq_lens.shape[0]
94 + decode_query_len = gQ.shape[0] // batch
95 + grid = (batch, self.split_k, 1)
96 + block = (32 * 5, 1, 1)
97 +
98 + tma_g2s = cpasync.CopyBulkTensorTileG2SOp()
99 + swizzle_128B = cute.make_swizzle(3, 4, 3)
100 + elems = 128 * 8 // dtype.width
101 +
102 + sQ_layout = cute.make_layout(
103 + (MAX_DQL, num_heads, (elems, head_dim // elems)),
104 + stride=(elems, MAX_DQL * elems, (1, BLOCK_Q * elems)),
105 + )
106 + sQ_layout = cute.make_composed_layout(swizzle_128B, 0, sQ_layout)
107 + Q_tma = cpasync.make_tiled_tma_atom(
108 + tma_g2s,
109 + cute.logical_divide(gQ, (None, None, elems)),
110 + sQ_layout,
111 + cta_tiler=(MAX_DQL, num_heads, head_dim),
112 + )
113 +
114 + sK_layout = cute.make_layout(
115 + (1, BLOCK_K, (elems, head_dim // elems), num_stages),
116 + stride=(0, elems, (1, BLOCK_K * elems), BLOCK_K * head_dim),
117 + )
118 + sK_layout = cute.make_composed_layout(swizzle_128B, 0, sK_layout)
119 + K_tma = cpasync.make_tiled_tma_atom(
120 + tma_g2s,
121 + cute.logical_divide(gK_cache, (None, None, elems)),
122 + sK_layout,
123 + cta_tiler=(1, BLOCK_K, head_dim),
124 + )
125 +
126 + self.kernel(
127 + Q_tma,
128 + K_tma,
129 + block_table,
130 + score,
131 + seq_lens,
132 + decode_query_len,
133 + ).launch(grid=grid, block=block, stream=stream, use_pdl=True)
134 +
135 + @cute.kernel
136 + def kernel(
137 + self,
138 + Q_tma: cpasync.TmaInfo,
139 + K_tma: cpasync.TmaInfo,
140 + block_table: cute.Tensor,
141 + score: cute.Tensor,
142 + seq_lens: cute.Tensor,
143 + decode_query_len,
144 + ):
145 + tid, _, _ = cute.arch.thread_idx()
146 + batch_id, split_id, _ = cute.arch.block_idx()
147 + _, split_k, _ = cute.arch.grid_dim()
148 + warp_id = cute.arch.make_warp_uniform(cute.arch.warp_idx())
149 + lane_id = cute.arch.lane_idx()
150 +
151 + NUM_HEADS = self.num_heads
152 + MAX_DQL = self.max_decode_query_len
153 + BLOCK_Q = NUM_HEADS * MAX_DQL
154 + BLOCK_K = self.BLOCK_K
155 + head_dim = self.head_dim
156 + dtype = self.dtype
157 + MMA_N = 8
158 + num_stages = self.num_stages
159 + Q_TILES = cute.ceil_div(BLOCK_Q, MMA_N)
160 + EPI_Q = Q_TILES * MMA_N
161 +
162 + smem = cutlass.utils.SmemAllocator()
163 + sK = smem.allocate_tensor(
164 + dtype,
165 + K_tma.smem_layout.outer,
166 + byte_alignment=128,
167 + swizzle=K_tma.smem_layout.inner,
168 + )[0, None, None, None]
169 + # alias sQ with the 1st stage of sK
170 + sQ_tma = cute.make_tensor(
171 + sK[None, None, 0].iterator, layout=Q_tma.smem_layout.outer
172 + )
173 + # TMA sees Q as (query, head, dim), while ldmatrix consumes a
174 + # flattened Q column mode. The target profile keeps the rank-2 view even
175 + # for degenerate shapes like DQL1.
176 + q_tma_elems = 128 * 8 // dtype.width
177 + sQ = cute.coalesce(
178 + cute.group_modes(sQ_tma, 0, 2),
179 + target_profile=(BLOCK_Q, (q_tma_elems, head_dim // q_tma_elems)),
180 + )
181 + epi_buffer = smem.allocate_tensor(Float32, cute.make_layout((EPI_Q, 4)))
182 +
183 + tma_full_mbar = smem.allocate_array(Int64, num_stages)
184 + tma_empty_mbar = smem.allocate_array(Int64, num_stages)
185 +
186 + seqlen = seq_lens[batch_id]
187 + num_blocks = cute.ceil_div(seqlen, BLOCK_K)
188 +
189 + if split_id < num_blocks:
190 + if warp_id == 0:
191 + with cute.arch.elect_one():
192 + for i in cutlass.range_constexpr(num_stages):
193 + cute.arch.mbarrier_init(tma_full_mbar + i, 1)
194 + cute.arch.mbarrier_init(tma_empty_mbar + i, 128)
195 + cute.arch.mbarrier_init_fence()
196 + elif warp_id == 1:
197 + cpasync.prefetch_descriptor(Q_tma.atom)
198 + cpasync.prefetch_descriptor(K_tma.atom)
199 + cute.arch.sync_threads()
200 +
201 + cute.arch.griddepcontrol_wait()
202 + cute.arch.griddepcontrol_launch_dependents()
203 +
204 + if warp_id == 4:
205 + # TMA warp
206 + tma_stage = 0
207 + tma_parity = 1
208 +
209 + gQ_tile = cute.local_tile(
210 + cute.domain_offset(
211 + (batch_id * decode_query_len, 0, 0),
212 + Q_tma.tma_tensor,
213 + ),
214 + tiler=(MAX_DQL, NUM_HEADS, head_dim),
215 + coord=(0, 0, 0),
216 + )
217 + cute.arch.mbarrier_wait(tma_empty_mbar, tma_parity)
218 + with cute.arch.elect_one():
219 + Q_size = BLOCK_Q * head_dim * (dtype.width // 8)
220 + cute.arch.mbarrier_arrive_and_expect_tx(tma_full_mbar, Q_size)
221 + # TMA bounds-checks rows when runtime decode_query_len is smaller
222 + # than MAX_DQL; padded Q columns are masked before global stores.
223 + simple_tma_copy(Q_tma.atom, gQ_tile, sQ_tma, tma_full_mbar)
224 +
225 + tma_stage = (tma_stage + 1) % num_stages
226 + if tma_stage == 0:
227 + tma_parity ^= 1
228 +
229 + for block_id in range(split_id, num_blocks, split_k):
230 + page_id = block_table[batch_id, block_id]
231 + gK_tile = K_tma.tma_tensor[page_id, None, None]
232 + k_mbar = tma_full_mbar + tma_stage
233 +
234 + cute.arch.mbarrier_wait(tma_empty_mbar + tma_stage, tma_parity)
235 + with cute.arch.elect_one():
236 + K_size = BLOCK_K * head_dim * (dtype.width // 8)
237 + cute.arch.mbarrier_arrive_and_expect_tx(k_mbar, K_size)
238 + simple_tma_copy(
239 + K_tma.atom,
240 + gK_tile,
241 + sK[None, None, tma_stage],
242 + k_mbar,
243 + cache_policy=EVICT_FIRST,
244 + )
245 +
246 + tma_stage = (tma_stage + 1) % num_stages
247 + if tma_stage == 0:
248 + tma_parity ^= 1
249 +
250 + else:
251 + # MMA warps
252 + # each warp handles K[32, head_dim] @ Q[BLOCK_Q, head_dim].T
253 + sK_warp = cute.local_tile(
254 + sK, (32, head_dim, num_stages), (warp_id, 0, 0)
255 + )
256 + q_start = seqlen - decode_query_len
257 +
258 + elems = 128 // dtype.width # 16B
259 + MMA_K = 32 * 8 // dtype.width # 32B
260 +
261 + # Pre-compute ldmatrix address.
262 + # sK loads a [16 x 16B] tile:
263 + # ((16, (16B, 2), 1), (32 / 16, head_dim / 32B, num_stages))
264 + # sQ loads an [8 x 32B] tile:
265 + # ((8, (16B, 4)), (BLOCK_Q / MMA_N, head_dim / 64B))
266 + sK_ldsm = cute.zipped_divide(
267 + sK_warp, (16, cute.make_layout((elems, 2)), 1)
268 + )
269 + sQ_ldsm = cute.zipped_divide(sQ, (MMA_N, cute.make_layout((elems, 4))))
270 +
271 + # sK: (16B, (32 / 16, head_dim / 32B, num_stages))
272 + # sQ: (16B, (BLOCK_Q / MMA_N, head_dim / 64B))
273 + sK_ldsm = sK_ldsm[(lane_id % 16, (None, lane_id // 16), 0), None]
274 + sQ_ldsm = sQ_ldsm[(lane_id % MMA_N, (None, lane_id // 8)), None]
275 +
276 + ldsm_op = warp.LdMatrix8x8x16bOp(num_matrices=4)
277 + ldsm_atom = cute.make_copy_atom(ldsm_op, dtype)
278 +
279 + rQ = cute.make_rmem_tensor(
280 + ((elems // 2, 2), head_dim // (MMA_K * 2), Q_TILES), dtype
281 + )
282 + rK = cute.make_rmem_tensor((elems, 2, head_dim // MMA_K), dtype)
283 + rC = cute.make_rmem_tensor((4, 2, Q_TILES), Float32)
284 +
285 + if warp_id == 0:
286 + cute.arch.mbarrier_wait(tma_full_mbar, 0)
287 + cute.arch.barrier(barrier_id=self.BAR_MMA, number_of_threads=128)
288 + for q in cutlass.range_constexpr(Q_TILES):
289 + cute.copy(ldsm_atom, sQ_ldsm[None, (q, None)], rQ[None, None, q])
290 + cute.arch.mbarrier_arrive(tma_empty_mbar)
291 +
292 + tma_stage = 1 % self.num_stages
293 + tma_parity = 0
294 + if tma_stage == 0:
295 + tma_parity ^= 1
296 +
297 + # sm100 doesn't have native mma.sync.f8. ptxas lowers mma.sync.f8
298 + # to F2FP.F16.E4M3 + HMMA; doing the conversion explicitly gives
299 + # better codegen while keeping the two FP16 k-fragments visible.
300 + if cutlass.const_expr(dtype is Float8E4M3FN):
301 + rQ_f16 = cute.make_rmem_tensor(
302 + (4, head_dim // MMA_K, Q_TILES, 2), Float16
303 + )
304 + q_lower, q_upper = _fp8_to_f16_mma_fragments(rQ)
305 + rQ_f16[None, None, None, 0].store(q_lower.load())
306 + rQ_f16[None, None, None, 1].store(q_upper.load())
307 +
308 + for block_id in range(split_id, num_blocks, split_k):
309 + rC.fill(0.0)
310 +
311 + if warp_id == 0:
312 + cute.arch.mbarrier_wait(tma_full_mbar + tma_stage, tma_parity)
313 + cute.arch.barrier(barrier_id=self.BAR_MMA, number_of_threads=128)
314 +
315 + for k in cutlass.range_constexpr(head_dim // MMA_K):
316 + cute.copy(
317 + ldsm_atom,
318 + sK_ldsm[None, (None, k, tma_stage)],
319 + rK[None, None, k],
320 + )
321 + for m in cutlass.range_constexpr(2):
322 + if cutlass.const_expr(dtype is Float8E4M3FN):
323 + rK_lower, rK_upper = _fp8_to_f16_mma_fragments(
324 + rK[None, m, k]
325 + )
326 + for n in cutlass.range_constexpr(Q_TILES):
327 + rC[None, m, n] = mma_sync(
328 + rK_lower,
329 + rQ_f16[None, k, n, 0],
330 + rC[None, m, n],
331 + )
332 + rC[None, m, n] = mma_sync(
333 + rK_upper,
334 + rQ_f16[None, k, n, 1],
335 + rC[None, m, n],
336 + )
337 + else:
338 + for n in cutlass.range_constexpr(Q_TILES):
339 + rC[None, m, n] = mma_sync(
340 + rK[None, m, k],
341 + rQ[(None, k % 2), k // 2, n],
342 + rC[None, m, n],
343 + )
344 +
345 + cute.arch.mbarrier_arrive(tma_empty_mbar + tma_stage)
346 +
347 + k_start = block_id * BLOCK_K + warp_id * 32
348 +
349 + # causal mask
350 + for q in cutlass.range_constexpr(Q_TILES):
351 + for i in cutlass.range_constexpr(4):
352 + for j in cutlass.range_constexpr(2):
353 + col = q * 8 + (lane_id % 4) * 2 + j
354 + q_local_pos = col % MAX_DQL
355 + q_pos = q_start + q_local_pos
356 + k_pos = k_start + i * 8 + lane_id // 4
357 + rC[q * 8 + i * 2 + j] = (
358 + rC[q * 8 + i * 2 + j]
359 + if q_pos >= k_pos
360 + else float("-inf")
361 + )
362 +
363 + for q in cutlass.range_constexpr(Q_TILES):
364 + # thread-reduction along BLOCK_K dim
365 + rScore = cute.make_rmem_tensor(2, Float32)
366 + rScore.fill(float("-inf"))
367 + for i in cutlass.range_constexpr(4):
368 + rScore[0] = cute.arch.fmax(rScore[0], rC[i * 2 + 0 + q * 8])
369 + rScore[1] = cute.arch.fmax(rScore[1], rC[i * 2 + 1 + q * 8])
370 +
371 + # warp-reduction among lanes 0,4,8,12,...
372 + for i in cutlass.range_constexpr(3):
373 + offset = 4 << i
374 + other0 = cute.arch.shuffle_sync_bfly(
375 + rScore[0], offset=offset, mask=-1, mask_and_clamp=31
376 + )
377 + other1 = cute.arch.shuffle_sync_bfly(
378 + rScore[1], offset=offset, mask=-1, mask_and_clamp=31
379 + )
380 + rScore[0] = cute.arch.fmax(rScore[0], other0)
381 + rScore[1] = cute.arch.fmax(rScore[1], other1)
382 +
383 + # store to smem for 4-warp reduction
384 + if lane_id * 2 < MMA_N:
385 + epi_buffer[q * MMA_N + lane_id * 2 + 0, warp_id] = rScore[0]
386 + epi_buffer[q * MMA_N + lane_id * 2 + 1, warp_id] = rScore[1]
387 + cute.arch.barrier(barrier_id=self.BAR_MMA, number_of_threads=128)
388 +
389 + head_id = lane_id // MAX_DQL
390 + q_local_pos = lane_id - head_id * MAX_DQL
391 + valid_q = head_id < NUM_HEADS and q_local_pos < decode_query_len
392 + if lane_id < BLOCK_Q and valid_q:
393 + final_score = epi_buffer[lane_id, 0]
394 + for i in cutlass.range_constexpr(1, 4):
395 + final_score = cute.arch.fmax(
396 + final_score, epi_buffer[lane_id, i]
397 + )
398 +
399 + t = batch_id * decode_query_len + q_local_pos
400 + score[head_id, t, block_id] = final_score
401 +
402 + tma_stage = (tma_stage + 1) % self.num_stages
403 + if tma_stage == 0:
404 + tma_parity ^= 1
405 +
406 + @cache
407 + @staticmethod
408 + def compile(
409 + dtype: type[cutlass.Numeric],
410 + num_heads: int,
411 + max_decode_query_len: int,
412 + split_k: int,
413 + head_dim: int = 128,
414 + ):
415 + bs = cute.sym_int()
416 + total_tokens = cute.sym_int()
417 + BLOCK_K = IndexDecodeScoreKernel.BLOCK_K
418 +
419 + q = make_fake_tensor(
420 + dtype, (total_tokens, num_heads, head_dim), divisibility=16
421 + )
422 + k_cache = make_fake_tensor(
423 + dtype, (cute.sym_int(), BLOCK_K, head_dim), divisibility=16
424 + )
425 + block_table = make_fake_tensor(Int32, (bs, cute.sym_int()), divisibility=1)
426 + score = make_fake_tensor(
427 + Float32, (num_heads, total_tokens, cute.sym_int()), divisibility=4
428 + )
429 + seq_lens = make_fake_tensor(Int32, (bs,), divisibility=1)
430 + kernel = IndexDecodeScoreKernel(
431 + dtype,
432 + num_heads,
433 + max_decode_query_len,
434 + split_k,
435 + head_dim,
436 + )
437 + stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
438 + return cute.compile(
439 + kernel,
440 + q,
441 + k_cache,
442 + block_table,
443 + score,
444 + seq_lens,
445 + stream,
446 + options="--enable-tvm-ffi",
447 + )
448 +
449 +
450 +def minimax_m3_index_decode_score_cutedsl(
451 + idx_q: torch.Tensor,
452 + index_kv_cache: torch.Tensor,
453 + block_table: torch.Tensor,
454 + seq_lens: torch.Tensor,
455 + max_seq_len: int,
456 + init_blocks: int,
457 + local_blocks: int,
458 + num_kv_heads: int,
459 + decode_query_len: int,
460 + max_decode_query_len: int,
461 + score_out: torch.Tensor,
462 +) -> torch.Tensor:
463 + if idx_q.dtype not in (torch.bfloat16, torch.float8_e4m3fn):
464 + raise TypeError("CuteDSL index decode score supports BF16 and FP8 E4M3 only")
465 + total_tokens, num_heads, head_dim = idx_q.shape
466 + batch = block_table.shape[0]
467 + assert index_kv_cache.shape[1] == IndexDecodeScoreKernel.BLOCK_K
468 + assert total_tokens == batch * decode_query_len
469 + assert 1 <= decode_query_len <= max_decode_query_len
470 + assert num_heads * max_decode_query_len <= 32
471 + dtype = _TORCH_TO_CUTE_DTYPE[idx_q.dtype]
472 + del max_seq_len, init_blocks, local_blocks, num_kv_heads
473 + score = score_out
474 + split_k = 256
475 + kernel = IndexDecodeScoreKernel.compile(
476 + dtype,
477 + num_heads,
478 + max_decode_query_len,
479 + split_k,
480 + head_dim,
481 + )
482 + kernel(idx_q, index_kv_cache, block_table, score, seq_lens)
483 + return score