[Kernel][SM100] Add a CuTeDSL fused query kernel (#49792)

Signed-off-by: Peiyuan Zhou <peiyuanzhou1994@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>

xiaozhoupy committed Aug 5, 2026 at 06:23 UTC 3756bf1e2ba0909500fbe77e03fe02eafb7f7080
6 files changed +699 -1
benchmarks/kernels/benchmark_fused_q_cutedsl.py new
+95
@@ -0,0 +1,95 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Compare the CuTeDSL and Triton implementations of the DSA fused query op.
4 +
5 +``fused_q`` dispatches to the CuTeDSL kernel on SM100 when the dtypes and
6 +shapes are supported and falls back to Triton otherwise. This benchmark forces
7 +each path so the two can be compared directly.
8 +"""
9 +
10 +import argparse
11 +from unittest.mock import patch
12 +
13 +import torch
14 +
15 +import vllm.models.deepseek_v32.common.kernels as K
16 +from vllm.triton_utils import triton
17 +
18 +# GLM-5.2 / DeepSeek-V3.2 DSA dimensions.
19 +NOPE_DIM = 512
20 +ROPE_DIM = 64
21 +INDEX_HEADS = 32
22 +INDEX_HEAD_DIM = 128
23 +MAX_POS = 16384
24 +
25 +# num_heads is per-rank: 128 attention heads over TP 8/4/2.
26 +NUM_HEADS = [16, 32, 64]
27 +# MTP=5 makes a concurrency-N decode batch 6*N tokens.
28 +NUM_TOKENS = [1, 6, 24, 96, 384, 1536, 4096]
29 +
30 +
31 +def make_inputs(num_tokens: int, num_heads: int):
32 + dev = "cuda"
33 + bf16 = torch.bfloat16
34 + return dict(
35 + positions=torch.randint(0, MAX_POS, (num_tokens,), device=dev),
36 + q_pe=torch.randn(num_tokens, num_heads, ROPE_DIM, device=dev, dtype=bf16),
37 + q_pe_cos_sin_cache=torch.randn(MAX_POS, ROPE_DIM, device=dev, dtype=bf16),
38 + index_q=torch.randn(
39 + num_tokens, INDEX_HEADS, INDEX_HEAD_DIM, device=dev, dtype=bf16
40 + ),
41 + index_q_cos_sin_cache=torch.randn(MAX_POS, ROPE_DIM, device=dev, dtype=bf16),
42 + ql_nope=torch.randn(num_tokens, num_heads, NOPE_DIM, device=dev, dtype=bf16),
43 + q_scale=torch.tensor([0.5], device=dev, dtype=torch.float32),
44 + index_weights=torch.randn(
45 + num_tokens, INDEX_HEADS, device=dev, dtype=torch.float32
46 + ),
47 + index_weights_softmax_scale=INDEX_HEAD_DIM**-0.5,
48 + index_weights_head_scale=INDEX_HEADS**-0.5,
49 + )
50 +
51 +
52 +def run(kwargs, *, cutedsl: bool):
53 + # is_fused_q_cutedsl_supported gates the dispatch inside fused_q.
54 + from vllm.models.deepseek_v32.nvidia.ops import fused_q_cutedsl as C
55 +
56 + with patch.object(C, "is_fused_q_cutedsl_supported", lambda *_, **__: cutedsl):
57 + K.fused_q(**kwargs)
58 +
59 +
60 +@triton.testing.perf_report(
61 + [
62 + triton.testing.Benchmark(
63 + x_names=["num_tokens"],
64 + x_vals=NUM_TOKENS,
65 + line_arg="provider",
66 + line_vals=["triton", "cutedsl"],
67 + line_names=["Triton", "CuTeDSL"],
68 + styles=[("blue", "-"), ("green", "-")],
69 + ylabel="us",
70 + plot_name=f"fused_q-num_heads-{nh}",
71 + args={"num_heads": nh},
72 + )
73 + for nh in NUM_HEADS
74 + ]
75 +)
76 +def benchmark(num_tokens, num_heads, provider):
77 + kwargs = make_inputs(num_tokens, num_heads)
78 + fn = lambda: run(kwargs, cutedsl=provider == "cutedsl") # noqa: E731
79 + fn() # warm up JIT / kernel compilation before timing
80 + ms, min_ms, max_ms = triton.testing.do_bench(fn, quantiles=[0.5, 0.2, 0.8])
81 + return 1000 * ms, 1000 * max_ms, 1000 * min_ms
82 +
83 +
84 +if __name__ == "__main__":
85 + parser = argparse.ArgumentParser(description=__doc__)
86 + parser.add_argument("--save-path", type=str, default=None)
87 + args = parser.parse_args()
88 +
89 + from vllm.platforms import current_platform
90 + from vllm.utils.import_utils import has_cutedsl
91 +
92 + if not (current_platform.has_device_capability(100) and has_cutedsl()):
93 + raise SystemExit("fused_q CuTeDSL requires an SM100 device and cutlass-dsl")
94 +
95 + benchmark.run(print_data=True, show_plots=False, save_path=args.save_path)
vllm/cute_utils/__init__.py
+1
@@ -19,6 +19,7 @@ from cutlass.cutlass_dsl import T, dsl_user_op
19 _TORCH_TO_CUTE_DTYPE = {
20 torch.bfloat16: BFloat16,
21 torch.float8_e4m3fn: Float8E4M3FN,
22 + torch.float32: Float32,
23 }
24
25 _CUTE_TO_PTX_DTYPE = {
vllm/cute_utils/cvt.py
+22 -1
@@ -1,6 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -from cutlass import Constexpr, Float32, Uint32, cute
3 +from cutlass import BFloat16, Constexpr, Float32, Uint16, Uint32, cute
4 from cutlass._mlir import ir
5 from cutlass._mlir.dialects import llvm, vector
6 from cutlass.cutlass_dsl import T, dsl_user_op
@@ -38,6 +38,11 @@ def bf16x2_to_fp32x2(data, *, loc=None, ip=None) -> tuple[Float32, Float32]:
38 )
39
40 elif isinstance(data, (cute.Tensor, cute.TensorSSA)):
41 + # recast to 32-bit registers if needed
42 + if data.element_type == BFloat16:
43 + data = cute.recast_tensor(data, Uint32)
44 + assert data.element_type == Uint32
45 +
46 # NOTE: the output is always 1D
47 size = cute.size(data.shape)
48 out = cute.make_rmem_tensor(size * 2, Float32)
@@ -84,6 +89,22 @@ def fp8x4_to_bf16x4(x: Uint32, *, loc=None, ip=None) -> cute.TensorSSA:
89 return cute.TensorSSA(vec, 2, Uint32)
90
91
92 +@dsl_user_op
93 +def fp32x2_to_fp8x2(a0: Float32, a1: Float32, *, loc=None, ip=None) -> Uint16:
94 + out = llvm.inline_asm(
95 + T.i16(),
96 + [
97 + a0.ir_value(loc=loc, ip=ip),
98 + a1.ir_value(loc=loc, ip=ip),
99 + ],
100 + "cvt.rn.satfinite.e4m3x2.f32 $0, $2, $1;",
101 + "=h,f,f",
102 + has_side_effects=False,
103 + is_align_stack=False,
104 + )
105 + return Uint16(out)
106 +
107 +
108 @dsl_user_op
109 def fp8x4_to_fp16x4(x: Uint32, *, loc=None, ip=None) -> cute.TensorSSA:
110 out = llvm.inline_asm(
vllm/models/deepseek_v32/common/kernels.py
+42
@@ -1,7 +1,10 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from collections.abc import Callable
4 +
5 import torch
6
7 +from vllm.platforms import current_platform
8 from vllm.triton_utils import tl, triton
9
10 # Cache of tiny 1-element dummy tensors (per device, dtype) reused by the
@@ -773,10 +776,12 @@ def fused_q(
776 ``(ql_nope, q_pe)`` tuple the backend expects.
777 """
778 assert positions.ndim == 1
779 + assert positions.dtype == torch.int64
780 assert q_pe.ndim == 3
781 assert q_pe_cos_sin_cache.ndim == 2
782 assert ql_nope.ndim == 3
783 assert ql_nope.shape[:2] == q_pe.shape[:2]
784 + assert q_scale.dtype == torch.float32 and q_scale.numel() == 1
785
786 num_tokens = positions.shape[0]
787 num_q_heads = q_pe.shape[1]
@@ -794,6 +799,23 @@ def fused_q(
799 assert index_weights is not None
800 num_index_q_heads = index_q.shape[1]
801 index_q_head_dim = index_q.shape[2]
802 + # fused_q is shared with the ROCm path, and the CuTeDSL module imports
803 + # cutlass at module scope, so only reach for it on CUDA.
804 + cutedsl_kernel: Callable[..., None] | None = None
805 + if current_platform.is_cuda():
806 + from vllm.models.deepseek_v32.nvidia.ops.fused_q_cutedsl import (
807 + fused_q_cutedsl,
808 + is_fused_q_cutedsl_supported,
809 + )
810 +
811 + if is_fused_q_cutedsl_supported(
812 + q_pe,
813 + index_q,
814 + ql_nope,
815 + has_indexer=has_indexer,
816 + quantize_mqa=quantize_mqa,
817 + ):
818 + cutedsl_kernel = fused_q_cutedsl
819 grid_heads = max(mqa_grid_heads, num_index_q_heads)
820 if quantize_mqa:
821 # fp8 path: pack [ql_nope; q_pe] into a single fp8 tensor.
@@ -815,6 +837,26 @@ def fused_q(
837
838 index_q_fp8 = torch.empty_like(index_q, dtype=torch.float8_e4m3fn)
839 index_weights_out = torch.empty_like(index_weights, dtype=torch.float32)
840 + if cutedsl_kernel is not None:
841 + cutedsl_kernel(
842 + positions,
843 + q_pe,
844 + q_pe_cos_sin_cache,
845 + ql_nope,
846 + q_scale,
847 + mqa_q,
848 + index_q,
849 + index_q_cos_sin_cache,
850 + index_weights,
851 + index_weights_softmax_scale,
852 + index_weights_head_scale,
853 + index_q_fp8,
854 + index_weights_out,
855 + has_indexer=has_indexer,
856 + index_rope_interleave=index_rope_interleave,
857 + )
858 + return index_q_fp8, index_weights_out, mqa_q
859 +
860 _fused_q_kernel[(3, num_tokens, grid_heads)](
861 positions,
862 q_pe,
vllm/models/deepseek_v32/nvidia/ops/__init__.py new
+2
@@ -0,0 +1,2 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/models/deepseek_v32/nvidia/ops/fused_q_cutedsl.py new
+537
@@ -0,0 +1,537 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from functools import cache
4 +
5 +import cutlass
6 +import cutlass.cute as cute
7 +import torch
8 +from cuda.bindings.driver import CUstream
9 +from cutlass import BFloat16, Float8E4M3FN, Float32, Int64, Uint8, Uint16, Uint32
10 +
11 +from vllm.cute_utils import _TORCH_TO_CUTE_DTYPE, cvt
12 +from vllm.platforms import current_platform
13 +
14 +
15 +def _make_fake_tensor(dtype, shape, divisibility):
16 + stride = tuple(
17 + 1 if i == len(shape) - 1 else cute.sym_int64(divisibility=divisibility)
18 + for i in range(len(shape))
19 + )
20 + return cute.runtime.make_fake_tensor(
21 + dtype,
22 + shape,
23 + stride,
24 + assumed_align=divisibility * dtype.width // 8,
25 + )
26 +
27 +
28 +def is_fused_q_cutedsl_supported(
29 + q_pe: torch.Tensor,
30 + index_q: torch.Tensor | None,
31 + ql_nope: torch.Tensor,
32 + *,
33 + has_indexer: bool,
34 + quantize_mqa: bool,
35 +) -> bool:
36 + if not (
37 + current_platform.has_device_capability(100)
38 + and quantize_mqa
39 + and q_pe.dtype == ql_nope.dtype == torch.bfloat16
40 + and q_pe.shape[-1] == 64
41 + and ql_nope.shape[-1] == 512
42 + # One warp per head group; a non-multiple would trip the kernel's own
43 + # assert instead of falling back to Triton.
44 + and q_pe.shape[1] % 4 == 0
45 + ):
46 + return False
47 + return not has_indexer or (
48 + index_q is not None
49 + and index_q.dtype == torch.bfloat16
50 + and index_q.shape[1] % 16 == 0
51 + and index_q.shape[-1] == 128
52 + )
53 +
54 +
55 +def fused_q_cutedsl(
56 + positions: torch.Tensor,
57 + q_pe: torch.Tensor,
58 + rope_cache: torch.Tensor,
59 + ql_nope: torch.Tensor,
60 + q_scale: torch.Tensor,
61 + mqa_output: torch.Tensor,
62 + idx_q: torch.Tensor,
63 + idx_rope_cache: torch.Tensor,
64 + idx_weights: torch.Tensor,
65 + idx_weights_softmax_scale: float,
66 + idx_weights_head_scale: float,
67 + idx_q_fp8: torch.Tensor,
68 + idx_weights_out: torch.Tensor,
69 + has_indexer: bool = True,
70 + index_rope_interleave: bool = True,
71 +) -> None:
72 + _, num_heads, rope_dim = q_pe.shape
73 + _, _, nope_dim = ql_nope.shape
74 + _, num_idx_heads, idx_dim = idx_q.shape
75 +
76 + if has_indexer:
77 + idx_rope_type = _TORCH_TO_CUTE_DTYPE[idx_rope_cache.dtype]
78 + idx_weights_type = _TORCH_TO_CUTE_DTYPE[idx_weights.dtype]
79 + else:
80 + idx_dim = num_idx_heads = 0
81 + idx_q = idx_rope_cache = idx_q_fp8 = None
82 + idx_weights = idx_weights_out = None
83 + idx_rope_type = idx_weights_type = None
84 +
85 + rope_type = _TORCH_TO_CUTE_DTYPE[rope_cache.dtype]
86 + compiled = FusedQKernel.compile(
87 + rope_dim,
88 + nope_dim,
89 + num_heads,
90 + rope_type,
91 + idx_dim,
92 + num_idx_heads,
93 + idx_rope_type,
94 + idx_weights_type,
95 + index_rope_interleave,
96 + )
97 + compiled(
98 + positions,
99 + q_pe,
100 + rope_cache,
101 + ql_nope,
102 + q_scale.view(1),
103 + mqa_output,
104 + idx_q,
105 + idx_rope_cache,
106 + idx_weights,
107 + idx_q_fp8,
108 + idx_weights_out,
109 + float(idx_weights_softmax_scale * idx_weights_head_scale),
110 + )
111 +
112 +
113 +class FusedQKernel:
114 + def __init__(
115 + self,
116 + rope_dim: int,
117 + nope_dim: int,
118 + num_heads: int,
119 + idx_dim: int,
120 + num_idx_heads: int,
121 + index_rope_interleave: bool,
122 + ) -> None:
123 + assert rope_dim == 64
124 + assert nope_dim == 512
125 + assert idx_dim in (128, 0)
126 +
127 + self.rope_dim = rope_dim
128 + self.nope_dim = nope_dim
129 + self.num_heads = num_heads
130 + self.idx_dim = idx_dim
131 + self.num_idx_heads = num_idx_heads
132 + self.index_rope_interleave = index_rope_interleave
133 +
134 + # mqa: rope_dim=64, nope_dim=512, num_heads=64/TP
135 + # indexer: rope_dim=64, nope_dim=64, num_heads=32
136 +
137 + self.num_warps = 4
138 + assert num_heads % self.num_warps == 0
139 + assert num_idx_heads % (4 * self.num_warps) == 0
140 + self.num_ctas_per_tok = num_heads // self.num_warps
141 + self.num_ctas_per_idx_tok = num_idx_heads // (4 * self.num_warps)
142 +
143 + @cute.jit
144 + def __call__(
145 + self,
146 + positions: cute.Tensor,
147 + q_pe: cute.Tensor,
148 + rope_cache: cute.Tensor,
149 + ql_nope: cute.Tensor,
150 + q_scale: cute.Tensor,
151 + mqa_output: cute.Tensor,
152 + idx_q: cute.Tensor,
153 + idx_rope_cache: cute.Tensor,
154 + idx_weights: cute.Tensor,
155 + idx_q_fp8: cute.Tensor,
156 + idx_weights_out: cute.Tensor,
157 + weight_scale: Float32,
158 + stream: CUstream,
159 + ):
160 + num_tokens = positions.shape[0]
161 + if cutlass.const_expr(self.idx_dim == 0):
162 + grid = (num_tokens, self.num_ctas_per_tok, 1)
163 + else:
164 + num_mqa_ctas = num_tokens * self.num_ctas_per_tok
165 + num_idx_ctas = num_tokens * self.num_ctas_per_idx_tok
166 + grid = (num_mqa_ctas + num_idx_ctas, 1, 1)
167 +
168 + self.kernel(
169 + positions,
170 + q_pe,
171 + rope_cache,
172 + ql_nope,
173 + q_scale,
174 + mqa_output,
175 + idx_q,
176 + idx_rope_cache,
177 + idx_weights,
178 + idx_q_fp8,
179 + idx_weights_out,
180 + weight_scale,
181 + ).launch(
182 + grid=grid,
183 + block=(self.num_warps * 32, 1, 1),
184 + stream=stream,
185 + use_pdl=True,
186 + )
187 +
188 + @cute.kernel
189 + def kernel(
190 + self,
191 + positions: cute.Tensor,
192 + q_pe: cute.Tensor,
193 + q_pe_rope_cache: cute.Tensor,
194 + ql_nope: cute.Tensor,
195 + q_scale: cute.Tensor,
196 + mqa_output: cute.Tensor,
197 + idx_q: cute.Tensor,
198 + idx_q_rope_cache: cute.Tensor,
199 + idx_weights: cute.Tensor,
200 + idx_q_fp8: cute.Tensor,
201 + idx_weights_out: cute.Tensor,
202 + weight_scale: Float32,
203 + ):
204 + if cutlass.const_expr(self.idx_dim == 0):
205 + token_id, group_id, _ = cute.arch.block_idx()
206 + self.mqa(
207 + positions,
208 + q_pe,
209 + q_pe_rope_cache,
210 + ql_nope,
211 + q_scale,
212 + mqa_output,
213 + token_id,
214 + group_id,
215 + )
216 + else:
217 + # CTA-specialization
218 + bid, _, _ = cute.arch.block_idx()
219 + num_mqa_ctas = positions.shape[0] * self.num_ctas_per_tok
220 + if bid < num_mqa_ctas:
221 + self.mqa(
222 + positions,
223 + q_pe,
224 + q_pe_rope_cache,
225 + ql_nope,
226 + q_scale,
227 + mqa_output,
228 + bid // self.num_ctas_per_tok,
229 + bid % self.num_ctas_per_tok,
230 + )
231 + else:
232 + bid -= num_mqa_ctas
233 + self.indexer(
234 + positions,
235 + idx_q,
236 + idx_q_rope_cache,
237 + idx_weights,
238 + idx_q_fp8,
239 + idx_weights_out,
240 + weight_scale,
241 + bid // self.num_ctas_per_idx_tok,
242 + bid % self.num_ctas_per_idx_tok,
243 + )
244 +
245 + @cute.jit
246 + def mqa(
247 + self,
248 + positions: cute.Tensor,
249 + q_pe: cute.Tensor,
250 + q_pe_rope_cache: cute.Tensor,
251 + ql_nope: cute.Tensor,
252 + q_scale: cute.Tensor,
253 + mqa_output: cute.Tensor,
254 + token_id,
255 + group_id,
256 + ):
257 + tid, _, _ = cute.arch.thread_idx()
258 + warp_id = cute.arch.make_warp_uniform(tid // 32)
259 + lane_id = tid % 32
260 + head_id = group_id * self.num_warps + warp_id
261 +
262 + cute.arch.griddepcontrol_wait()
263 +
264 + pos = positions[token_id]
265 + inv_scale = 1.0 / q_scale[0]
266 +
267 + cp_op = cute.nvgpu.CopyUniversalOp()
268 + cp_32B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=256)
269 + cp_16B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=128)
270 + cp_4B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=32)
271 + cp_2B = cute.make_copy_atom(cp_op, Uint8, num_bits_per_copy=16)
272 +
273 + ##### issue all loads asap #####
274 + rQ_nope_bf16 = cute.make_rmem_tensor(16, BFloat16)
275 + rQ_rope_bf16 = cute.make_rmem_tensor(2, BFloat16)
276 +
277 + src_ql_nope = cute.local_tile(
278 + ql_nope[token_id, head_id, None], (16,), (lane_id,)
279 + )
280 + src_q_rope = cute.local_tile(q_pe[token_id, head_id, None], (2,), (lane_id,))
281 + cute.copy(cp_32B, src_ql_nope, rQ_nope_bf16)
282 + cute.copy(cp_4B, src_q_rope, rQ_rope_bf16)
283 +
284 + rCos_raw = q_pe_rope_cache[pos, 0 + lane_id]
285 + rSin_raw = q_pe_rope_cache[pos, 32 + lane_id]
286 +
287 + ##### process NoPE #####
288 + rQ_nope_f32 = cvt.bf16x2_to_fp32x2(rQ_nope_bf16).load() * inv_scale
289 + rQ_nope_f8 = cute.make_rmem_tensor(16, Float8E4M3FN)
290 + rQ_nope_f8.store(rQ_nope_f32.to(Float8E4M3FN))
291 + dst_Q_nope = cute.local_tile(
292 + mqa_output[token_id, head_id, None], (16,), (lane_id,)
293 + )
294 + cute.copy(cp_16B, rQ_nope_f8, dst_Q_nope)
295 +
296 + ##### process RoPE ######
297 + rQ_rope_f32 = cvt.bf16x2_to_fp32x2(rQ_rope_bf16)
298 + rCos = rCos_raw.to(Float32)
299 + rSin = rSin_raw.to(Float32)
300 + r0 = (rQ_rope_f32[0] * rCos - rQ_rope_f32[1] * rSin) * inv_scale
301 + r1 = (rQ_rope_f32[1] * rCos + rQ_rope_f32[0] * rSin) * inv_scale
302 +
303 + cute.arch.griddepcontrol_launch_dependents()
304 +
305 + # TensorSSA fp32->fp8 cvt has a bug. rely on direct PTX
306 + rQ_rope_f8 = cute.make_rmem_tensor(2, Float8E4M3FN)
307 + cute.recast_tensor(rQ_rope_f8, Uint16)[0] = cvt.fp32x2_to_fp8x2(r0, r1)
308 + dst_Q_rope = cute.local_tile(
309 + mqa_output[token_id, head_id, None], (2,), (256 + lane_id,)
310 + )
311 + cute.copy(cp_2B, rQ_rope_f8, dst_Q_rope)
312 +
313 + @cute.jit
314 + def indexer(
315 + self,
316 + positions: cute.Tensor,
317 + idx_q: cute.Tensor,
318 + idx_q_rope_cache: cute.Tensor,
319 + idx_weights: cute.Tensor,
320 + idx_q_fp8: cute.Tensor,
321 + idx_weights_out: cute.Tensor,
322 + weight_scale: Float32,
323 + token_id,
324 + group_id,
325 + ):
326 + tid, _, _ = cute.arch.thread_idx()
327 + subwarp_id = tid // 8
328 + sublane_id = tid % 8
329 + head_id = group_id * (4 * self.num_warps) + subwarp_id
330 + cute.arch.griddepcontrol_wait()
331 +
332 + pos = positions[token_id]
333 +
334 + cp_op = cute.nvgpu.CopyUniversalOp()
335 + cp_16B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=128)
336 + cp_8B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=64)
337 + cp_4B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=32)
338 +
339 + ##### issue all loads first #####
340 + rQ_rope_bf16 = cute.make_rmem_tensor(8, BFloat16)
341 + if cutlass.const_expr(self.index_rope_interleave):
342 + src_idx_q_rope = cute.local_tile(
343 + idx_q[token_id, head_id, None], (8,), (sublane_id,)
344 + )
345 + cute.copy(cp_16B, src_idx_q_rope, rQ_rope_bf16)
346 + else:
347 + src_idx_q_rope = cute.zipped_divide(
348 + idx_q[token_id, head_id, None], (4,)
349 + ) # (4,32)
350 + cute.copy(
351 + cp_8B,
352 + src_idx_q_rope[None, 0 + sublane_id],
353 + cute.local_tile(rQ_rope_bf16, (4,), (0,)),
354 + )
355 + cute.copy(
356 + cp_8B,
357 + src_idx_q_rope[None, 8 + sublane_id],
358 + cute.local_tile(rQ_rope_bf16, (4,), (1,)),
359 + )
360 +
361 + rQ_nope_bf16 = cute.make_rmem_tensor(8, BFloat16)
362 + src_idx_q_nope = cute.local_tile(
363 + idx_q[token_id, head_id, None], (8,), (8 + sublane_id,)
364 + )
365 + cute.copy(cp_16B, src_idx_q_nope, rQ_nope_bf16)
366 +
367 + rCos_raw = cute.make_rmem_tensor(4, idx_q_rope_cache.element_type)
368 + rSin_raw = cute.make_rmem_tensor(4, idx_q_rope_cache.element_type)
369 + rope_cache_view = cute.zipped_divide(idx_q_rope_cache[pos, None], (4,))
370 +
371 + if cutlass.const_expr(idx_q_rope_cache.element_type == Float32):
372 + cute.copy(cp_16B, rope_cache_view[None, sublane_id], rCos_raw)
373 + cute.copy(cp_16B, rope_cache_view[None, 8 + sublane_id], rSin_raw)
374 + elif cutlass.const_expr(idx_q_rope_cache.element_type == BFloat16):
375 + cute.copy(cp_8B, rope_cache_view[None, sublane_id], rCos_raw)
376 + cute.copy(cp_8B, rope_cache_view[None, 8 + sublane_id], rSin_raw)
377 +
378 + # unpack to FP32
379 + rQ_rope_f32 = cvt.bf16x2_to_fp32x2(rQ_rope_bf16)
380 + rQ_nope_f32 = cvt.bf16x2_to_fp32x2(rQ_nope_bf16)
381 + if cutlass.const_expr(idx_q_rope_cache.element_type == Float32):
382 + rCos = rCos_raw
383 + rSin = rSin_raw
384 + elif cutlass.const_expr(idx_q_rope_cache.element_type == BFloat16):
385 + rCos = cvt.bf16x2_to_fp32x2(rCos_raw)
386 + rSin = cvt.bf16x2_to_fp32x2(rSin_raw)
387 +
388 + # apply rope
389 + for i in cutlass.range_constexpr(4):
390 + if cutlass.const_expr(self.index_rope_interleave):
391 + r0 = rQ_rope_f32[i * 2 + 0] * rCos[i] - rQ_rope_f32[i * 2 + 1] * rSin[i]
392 + r1 = rQ_rope_f32[i * 2 + 1] * rCos[i] + rQ_rope_f32[i * 2 + 0] * rSin[i]
393 + rQ_rope_f32[i * 2 + 0] = r0
394 + rQ_rope_f32[i * 2 + 1] = r1
395 + else:
396 + r0 = rQ_rope_f32[0 + i] * rCos[i] - rQ_rope_f32[4 + i] * rSin[i]
397 + r1 = rQ_rope_f32[4 + i] * rCos[i] + rQ_rope_f32[0 + i] * rSin[i]
398 + rQ_rope_f32[0 + i] = r0
399 + rQ_rope_f32[4 + i] = r1
400 +
401 + # amax
402 + amax = Float32(1e-4)
403 + for i in cutlass.range_constexpr(8):
404 + amax = cute.arch.fmax(amax, cute.math.absf(rQ_rope_f32[i]))
405 + amax = cute.arch.fmax(amax, cute.math.absf(rQ_nope_f32[i]))
406 +
407 + # warp reduction among 8 lanes
408 + for i in cutlass.range_constexpr(3):
409 + other = cute.arch.shuffle_sync_bfly(amax, 1 << i)
410 + amax = cute.arch.fmax(amax, other)
411 +
412 + # compute scale from amax
413 + # exp2(ceil(log2(scale))) via bit manipulation
414 + scale = amax * (1.0 / 448.0)
415 + bits = scale.bitcast(Uint32)
416 + exp_bits = (bits + Uint32(0x007FFFFF)) & Uint32(0x7F800000)
417 + scale = exp_bits.bitcast(Float32)
418 + inv_scale = (Uint32(0x7F000000) - exp_bits).bitcast(Float32)
419 +
420 + for i in cutlass.range_constexpr(8):
421 + rQ_nope_f32[i] *= inv_scale
422 + rQ_rope_f32[i] *= inv_scale
423 +
424 + cute.arch.griddepcontrol_launch_dependents()
425 +
426 + # quantize and store
427 + rQ_nope_f8 = cute.make_rmem_tensor(8, Float8E4M3FN)
428 + rQ_nope_f8.store(rQ_nope_f32.load().to(Float8E4M3FN))
429 + dst_idx_q_nope = cute.local_tile(
430 + idx_q_fp8[token_id, head_id, None], (8,), (8 + sublane_id,)
431 + )
432 + cute.copy(cp_8B, rQ_nope_f8, dst_idx_q_nope)
433 +
434 + rQ_rope_f8 = cute.make_rmem_tensor(8, Float8E4M3FN)
435 + rQ_rope_f8.store(rQ_rope_f32.load().to(Float8E4M3FN))
436 + if cutlass.const_expr(self.index_rope_interleave):
437 + dst_idx_q_rope = cute.local_tile(
438 + idx_q_fp8[token_id, head_id, None], (8,), (sublane_id,)
439 + )
440 + cute.copy(cp_8B, rQ_rope_f8, dst_idx_q_rope)
441 + else:
442 + dst_idx_q_rope = cute.zipped_divide(
443 + idx_q_fp8[token_id, head_id, None], (4,)
444 + ) # (4,32)
445 + cute.copy(
446 + cp_4B,
447 + cute.local_tile(rQ_rope_f8, (4,), (0,)),
448 + dst_idx_q_rope[None, 0 + sublane_id],
449 + )
450 + cute.copy(
451 + cp_4B,
452 + cute.local_tile(rQ_rope_f8, (4,), (1,)),
453 + dst_idx_q_rope[None, 8 + sublane_id],
454 + )
455 +
456 + # scale indexer weights
457 + if sublane_id == 0:
458 + w = idx_weights[token_id, head_id].to(Float32)
459 + idx_weights_out[token_id, head_id] = w * scale * weight_scale
460 +
461 + @staticmethod
462 + @cache
463 + def compile(
464 + rope_dim: int,
465 + nope_dim: int,
466 + num_heads: int,
467 + rope_type: type[cutlass.Numeric],
468 + idx_dim: int,
469 + num_idx_heads: int,
470 + idx_rope_type: type[cutlass.Numeric] | None,
471 + idx_weights_type: type[cutlass.Numeric] | None,
472 + index_rope_interleave: bool,
473 + ):
474 + num_tokens = cute.sym_int()
475 + max_pos = cute.sym_int()
476 +
477 + positions = _make_fake_tensor(Int64, (num_tokens,), divisibility=1)
478 + q_pe = _make_fake_tensor(
479 + BFloat16, (num_tokens, num_heads, rope_dim), divisibility=16
480 + )
481 + rope_cache = _make_fake_tensor(rope_type, (max_pos, rope_dim), divisibility=8)
482 + ql_nope = _make_fake_tensor(
483 + BFloat16, (num_tokens, num_heads, nope_dim), divisibility=16
484 + )
485 + q_scale = _make_fake_tensor(Float32, (1,), divisibility=4)
486 + mqa_output = _make_fake_tensor(
487 + Float8E4M3FN,
488 + (num_tokens, num_heads, nope_dim + rope_dim),
489 + divisibility=16,
490 + )
491 +
492 + if idx_rope_type is not None:
493 + index_q = _make_fake_tensor(
494 + BFloat16, (num_tokens, num_idx_heads, idx_dim), divisibility=16
495 + )
496 + index_rope_cache = _make_fake_tensor(
497 + idx_rope_type, (max_pos, rope_dim), divisibility=8
498 + )
499 + index_weights = _make_fake_tensor(
500 + idx_weights_type, (num_tokens, num_idx_heads), divisibility=8
501 + )
502 + index_q_fp8 = _make_fake_tensor(
503 + Float8E4M3FN, (num_tokens, num_idx_heads, idx_dim), divisibility=16
504 + )
505 + index_weights_out = _make_fake_tensor(
506 + Float32, (num_tokens, num_idx_heads), divisibility=4
507 + )
508 + else:
509 + index_q = index_rope_cache = index_q_fp8 = None
510 + index_weights = index_weights_out = None
511 +
512 + kernel = FusedQKernel(
513 + rope_dim,
514 + nope_dim,
515 + num_heads,
516 + idx_dim,
517 + num_idx_heads,
518 + index_rope_interleave,
519 + )
520 + stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
521 + return cute.compile(
522 + kernel,
523 + positions,
524 + q_pe,
525 + rope_cache,
526 + ql_nope,
527 + q_scale,
528 + mqa_output,
529 + index_q,
530 + index_rope_cache,
531 + index_weights,
532 + index_q_fp8,
533 + index_weights_out,
534 + Float32(0.0),
535 + stream,
536 + options="--enable-tvm-ffi",
537 + )