mjolnir-kernel 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mjolnir/t5_decode.py ADDED
@@ -0,0 +1,2990 @@
1
+ # Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright (c) 2026 Mieszko Syty (Mjolnir modifications)
3
+ # SPDX-License-Identifier: BSD-3-Clause
4
+ #
5
+ # Derived from NVIDIA CUTLASS, examples/python/CuTeDSL/cute/blackwell/kernel/
6
+ # attention/mixed_input_fmha/mixed_input_fmha_decode.py; this file as a whole
7
+ # stays under the BSD-3-Clause license below (see NOTICE).
8
+
9
+ # Redistribution and use in source and binary forms, with or without
10
+ # modification, are permitted provided that the following conditions are met:
11
+
12
+ # 1. Redistributions of source code must retain the above copyright notice, this
13
+ # list of conditions and the following disclaimer.
14
+
15
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+
19
+ # 3. Neither the name of the copyright holder nor the names of its
20
+ # contributors may be used to endorse or promote products derived from
21
+ # this software without specific prior written permission.
22
+
23
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
+
34
+ """Paged-KV fp8 decode attention for Jetson Thor (sm_110a), vLLM-shaped.
35
+
36
+ Inputs follow vLLM's paged KV cache:
37
+ q (num_seqs * q_len, h_q, d) bf16, or e4m3 + q_descale
38
+ k/v cache (num_pages, page_size, h_k, d) e4m3
39
+ block_table(num_seqs, max_blocks) int32
40
+ seq_lens (num_seqs,) int32 (includes the new tokens)
41
+ q/k/v_descale (num_seqs, h_k) fp32 (per-tensor scales broadcast)
42
+ Outputs:
43
+ o (num_seqs * q_len, h_q, d) bf16
44
+ lse (num_seqs, h_k, q_len * h_r) fp32, natural-log LSE
45
+
46
+ Two compute paths, selected by the Q dtype:
47
+ bf16 Q : fp8 KV -> bf16 via the cvt warpgroups -> TMEM A-operand
48
+ e4m3 Q : fully-fp8, the mma reads K/V straight from the TMA'd SMEM ring
49
+
50
+ Query packing. The kernel works on N = q_len * h_r rows per KV head, packed
51
+ qi-major (n = qi * h_r + hr). For q_len == 1 that is exactly vLLM's layout
52
+ (no copy). For q_len > 1 (speculative decoding) the caller permutes q
53
+ (b, q_len, h_k, h_r, d) -> (b, h_k, q_len, h_r, d) and o back; see
54
+ pack_q / unpack_o below. Query token qi of a sequence of length L may
55
+ attend to keys 0 .. L - q_len + qi (causal tail). With ``causal=False`` every
56
+ query token sees all L keys — the block attention of parallel drafters
57
+ (DSpark / DFlash: one bonus query and K mask queries attend to the context
58
+ and to each other).
59
+
60
+ Split-KV scheduling is occupancy-pinned (FlashInfer/FlashDecoding style):
61
+ a host-side plan (plan_split_kv) picks the smallest contiguous KV chunk such
62
+ that all chunks of all sequences fit in one wave of CTA slots, and emits one
63
+ work item (seq, first tile, end tile, split slot) per chunk. Each CTA streams
64
+ its chunk front to back. Long sequences get more chunks, short ones fewer,
65
+ so variable-length batches balance. A sequence with a single chunk writes its
66
+ final output directly; the reduction only merges sequences that were split.
67
+
68
+ Workspace (o/m/l partials) needs NO initialization: the reduction only reads
69
+ the splits a sequence actually used, and padding work items (begin == end)
70
+ exit at once, so a fixed-size padded work list is CUDA-graph friendly.
71
+ max_splits == 1 compiles the reduction out entirely.
72
+ """
73
+
74
+ import argparse
75
+ import math
76
+ import os
77
+ from typing import Type, Tuple
78
+ from functools import partial
79
+
80
+ import torch
81
+
82
+ import cuda.bindings.driver as cuda
83
+
84
+ import cutlass
85
+ import cutlass.cute as cute
86
+ from cutlass.cute.nvgpu import tcgen05, OperandMajorMode
87
+ import cutlass.utils as utils
88
+ import cutlass.pipeline as pipeline
89
+ import cutlass.torch as cutlass_torch
90
+ import cutlass.utils.blackwell_helpers as sm100_utils
91
+ import cutlass.cute.testing as testing
92
+ from cutlass.cute.runtime import from_dlpack
93
+ from cutlass.cute.typing import *
94
+
95
+ # Compile-time notes (stages, tmem use): printed by the standalone harness
96
+ # (__main__), quiet inside vLLM unless MJOLNIR_ATTN_VERBOSE=1.
97
+ VERBOSE = os.environ.get("MJOLNIR_ATTN_VERBOSE", os.environ.get("THOR_T5_VERBOSE", "")).strip() in ("1", "true", "yes")
98
+
99
+
100
+ def _note(msg: str) -> None:
101
+ if VERBOSE:
102
+ print(msg)
103
+
104
+
105
+ # Kernel invariants
106
+ mma_modes = (0, 1, 2)
107
+ mma_dice = (None, None, None) # (MMA, #MMA_M, #MMA_K)
108
+ cpy_dice = (None,) + mma_dice # (CPY, #CPY_MMA, #CPY_M, #CPY_K)
109
+ warp_threads = 32
110
+ warpgroup_warps = 4
111
+ warpgroup_threads = 128
112
+ tile_s_rows = 128 # KV rows per tile (= GEMM1 M)
113
+
114
+ # SW128 smem atoms repeat every 1024 B and both TMA and the UMMA descriptor
115
+ # (base_offset = 0) assume the tile base is 1024-B aligned.
116
+ swizzle_align = 1024
117
+
118
+ # fp8 P is stored as e4m3(p * FP8_P_SCALE): p in [0, 1] then spans e4m3's
119
+ # normal range instead of flushing small probabilities to subnormals / 0.
120
+ # 1 / FP8_P_SCALE is folded into the output scale.
121
+ FP8_P_SCALE = 448.0
122
+
123
+ # KV cache formats (fp8-Q path). "fp8": e4m3 (default). Narrow formats read
124
+ # DIRECTLY by tcgen05 kind::f8f6f4 (TMA unpacks the packed gmem bytes into
125
+ # 1-byte SMEM containers): "e3m2"/"e2m3" (fp6), "e2m1" (fp4); scales are the
126
+ # usual per-tensor / per-head / per-(seq, head) descales. "nvfp4": packed
127
+ # e2m1 + one e4m3 scale per 16 head-dim elements, converted to e4m3 in SMEM
128
+ # by a convert warpgroup, then the normal fp8 MMA.
129
+ KV_FORMATS = ("fp8", "e3m2", "e2m3", "e2m1", "nvfp4")
130
+ KV_FORMAT_BITS = {"fp8": 8, "e3m2": 6, "e2m3": 6, "e2m1": 4, "nvfp4": 4}
131
+ # Harness (benchmark CLI) defaults: e2m3 KV on head-major pages, the fastest
132
+ # measured config at near-fp8 accuracy (rel L2 0.051 vs fp8 0.045). The kernel
133
+ # class and the vLLM runner keep fp8 (vLLM writes an fp8 cache).
134
+ HARNESS_KV_FORMAT_FP8Q = "e2m3"
135
+ HARNESS_KV_LAYOUT = "hnd"
136
+ # NVFP4 conversion computes e4m3(code * scale * 2^-3): with codes <= 6 and
137
+ # e4m3 scales <= 448 the product stays <= 336 (< 448, no saturation). The
138
+ # host folds the 2^3 back into the K/V descale.
139
+ NVFP4_SHIFT = 0.125
140
+ NVFP4_GROUP = 16
141
+
142
+ # Math helpers
143
+ log2_e = math.log2(math.e) # change exponential base
144
+ fadd2 = cute.arch.add_packed_f32x2
145
+ fmul2 = cute.arch.mul_packed_f32x2
146
+ ffma2 = cute.arch.fma_packed_f32x2
147
+ exp2 = partial(cute.math.exp2, fastmath=True)
148
+
149
+
150
+ def _warp_fmax_shfl(value, mask=0xFFFFFFFF):
151
+ """Warp-wide fmax via a butterfly shfl. ``warp_redux_sync(kind="fmax")``
152
+ emits ``redux.f32``, which ptxas rejects on sm_110a (redux is int-only)."""
153
+ for offset in (16, 8, 4, 2, 1):
154
+ value = cute.arch.fmax(value, cute.arch.shuffle_sync_bfly(value, offset=offset, mask=mask), nan=True)
155
+ return value
156
+
157
+
158
+ warp_fmax = _warp_fmax_shfl
159
+ smem_fmax = partial(cute.arch.atomic_fmax, sem="relaxed", scope="cta")
160
+
161
+
162
+ class MixedInputFusedMultiHeadAttentionDecode:
163
+ def __init__(
164
+ self,
165
+ headdim,
166
+ grouped_head_tile, # packed query rows (q_len * h_r) per CTA tile
167
+ convert_warpgroups=1, # bf16 path: warpgroups striding over cvt stages; fp8: 0
168
+ page_size=128, # vLLM block size: 16, 32, 64 or 128
169
+ q_len=1, # query tokens per sequence (> 1 for speculative decoding)
170
+ max_splits=16, # partial-slot capacity per sequence (compile-time)
171
+ ctas_per_sm=2, # co-resident CTAs per SM; 2 is the Thor default (1/SM: 0.80x FI
172
+ # at 128K-256K), each CTA gets half the smem/regs/TMEM
173
+ with_reduction=True, # False: no split in this plan -> no reduction launch
174
+ max_kv_stages=0, # KV ring depth; 0 = whole KV tiles (1 tile at 2/SM, 2 at 1/SM; fp6 at 2/SM d=256: 2)
175
+ pdl=False, # programmatic dependent launch for decode + reduction
176
+ l2_prefetch=0, # TMA-prefetch K/V tiles this many tiles ahead into L2 (0 = off)
177
+ kv_evict_first=False, # TMA K/V loads with an L2 EVICT_FIRST cache policy
178
+ kv_format="fp8", # KV cache format, see KV_FORMATS
179
+ causal=True, # False: every query token sees all seq_len keys (parallel-drafter blocks)
180
+ ):
181
+ self.causal = bool(causal)
182
+ self.max_kv_stages = max_kv_stages
183
+ self.pdl = bool(pdl)
184
+ # Extra .launch() kwargs, built here in plain Python: the DSL rewrites
185
+ # conditional expressions inside __call__ into regions that can't see
186
+ # `self`. Empty when PDL is off (works with DSLs without use_pdl).
187
+ self.launch_extra = {"use_pdl": True} if self.pdl else {}
188
+ self.l2_prefetch = int(l2_prefetch)
189
+ self.kv_evict_first = bool(kv_evict_first)
190
+ assert kv_format in KV_FORMATS, f"kv_format must be one of {KV_FORMATS}"
191
+ self.kv_format = kv_format
192
+ if kv_format == "nvfp4":
193
+ assert convert_warpgroups == 1, "nvfp4 KV needs exactly one convert warpgroup"
194
+ assert self.l2_prefetch == 0, "l2_prefetch is not implemented for nvfp4 KV"
195
+ assert 0 <= self.l2_prefetch <= 8, "l2_prefetch: 0..8 tiles"
196
+ if self.l2_prefetch > 0:
197
+ assert hasattr(cute, "prefetch"), (
198
+ "l2_prefetch needs cute.prefetch (TMA prefetch) — newer nvidia-cutlass-dsl"
199
+ )
200
+ if self.pdl:
201
+ assert hasattr(cute.arch, "griddepcontrol_wait"), (
202
+ "pdl needs cute.arch.griddepcontrol_* — newer nvidia-cutlass-dsl"
203
+ )
204
+ self.with_reduction = with_reduction
205
+ self.headdim = headdim
206
+ self.grouped_head_tile = grouped_head_tile
207
+ self.convert_warpgroups = convert_warpgroups
208
+ self.page_size = page_size
209
+ self.q_len = q_len
210
+ self.max_splits = max_splits
211
+ self.ctas_per_sm = ctas_per_sm
212
+
213
+ assert headdim % 128 == 0, "headdim must be a multiple of 128"
214
+ assert page_size in (16, 32, 64, 128), "page_size must be 16, 32, 64 or 128"
215
+ # Ld32x32b / St32x32b repetition (= tile width) must be a power of two.
216
+ assert grouped_head_tile in (8, 16, 32), "grouped_head_tile must be 8, 16 or 32"
217
+ assert convert_warpgroups >= 0
218
+ assert q_len >= 1 and max_splits >= 1
219
+ assert ctas_per_sm in (1, 2)
220
+
221
+ warpgroup_id = 0
222
+
223
+ self.softmax_warpgroup_id = warpgroup_id
224
+ warpgroup_id += 1
225
+
226
+ # Fully-fp8 launches with convert_warpgroups == 0: no cvt warpgroup
227
+ # exists at all (begin == end, so the cvt dispatch never matches).
228
+ self.cvt_wg_begin = warpgroup_id
229
+ self.cvt_wg_end = warpgroup_id + convert_warpgroups
230
+ warpgroup_id += convert_warpgroups
231
+
232
+ # 2 MMA warps (less register pressure, hides mma switching latency)
233
+ # + 2 TMA warps (KV ring / Q).
234
+ self.mma_kq_warp_id = warpgroup_id * warpgroup_warps + 0
235
+ self.mma_vp_warp_id = warpgroup_id * warpgroup_warps + 1
236
+ self.tma_kv_warp_id = warpgroup_id * warpgroup_warps + 2
237
+ self.tma_q_warp_id = warpgroup_id * warpgroup_warps + 3
238
+ warpgroup_id += 1
239
+
240
+ self.threads_per_cta = warpgroup_id * warpgroup_threads
241
+
242
+ # Register budget. The launch uses min_blocks_per_mp = ctas_per_sm, so
243
+ # ptxas caps every thread at R = 64K / (threads * ctas) rounded DOWN to
244
+ # a multiple of 8 (bf16 at 2 CTAs/SM: 384 threads -> R = 80, not 85).
245
+ # The CTA's register pool is R * threads; setmaxnreg must stay inside
246
+ # it, a .dec may not exceed R and an .inc may not go below R (either
247
+ # is an illegal instruction). Budget in per-warpgroup-thread units.
248
+ self.use_reg_reconfig = grouped_head_tile > 16
249
+ num_warpgroups = self.threads_per_cta // warpgroup_threads
250
+ launch_regs = min(255, 64 * 1024 // (self.threads_per_cta * ctas_per_sm)) // 8 * 8
251
+ pool_regs = launch_regs * num_warpgroups
252
+ # 2 CTAs/SM with a cvt warpgroup is the tight case (pool 240): give
253
+ # the MMA/TMA warps less so softmax keeps >= 96.
254
+ self.mma_tma_regs = min(launch_regs, 56 if (ctas_per_sm == 2 and convert_warpgroups > 0) else 72)
255
+ self.cvt_regs = min(launch_regs, 112 if ctas_per_sm == 1 else 88)
256
+ self.softmax_regs = pool_regs - self.mma_tma_regs - self.cvt_regs * convert_warpgroups
257
+ self.softmax_regs = min(256, self.softmax_regs) // 8 * 8 # setmaxnreg: 24..256, x8
258
+ if self.use_reg_reconfig:
259
+ assert self.softmax_regs >= max(96, launch_regs), (
260
+ f"softmax warpgroup gets {self.softmax_regs} regs (launch {launch_regs}, "
261
+ f"pool {pool_regs}); use ctas_per_sm=1 or fewer cvt warpgroups"
262
+ )
263
+ assert (self.mma_tma_regs + self.cvt_regs * convert_warpgroups + self.softmax_regs) <= pool_regs
264
+
265
+ self.sp_stages = 2
266
+ self.o_stages = 1
267
+
268
+ def _kv_types(self, kv_dtype, mma_dtype):
269
+ """KV-format type plumbing, kept OUT of the traced __call__ (the DSL
270
+ rewrites if/else and ternaries there). Returns (NVFP4, NARROW,
271
+ MMA A-operand dtype, SMEM ring dtype, packed stages)."""
272
+ nvfp4 = self.kv_format == "nvfp4"
273
+ narrow = self.kv_format in ("e3m2", "e2m3", "e2m1")
274
+ if nvfp4:
275
+ assert kv_dtype is cutlass.Float4E2M1FN, kv_dtype
276
+ assert mma_dtype.width == 8, "nvfp4 KV needs fp8 Q"
277
+ return True, False, mma_dtype, mma_dtype, 2
278
+ if narrow:
279
+ assert kv_dtype.width < 8 and mma_dtype.width == 8, (kv_dtype, mma_dtype)
280
+ # A = K/V in fp6/fp4 (MMA reads them directly), SMEM 1-B containers
281
+ return False, True, kv_dtype, cutlass.Int8, 0
282
+ assert kv_dtype is cutlass.Float8E4M3FN, kv_dtype
283
+ return False, False, mma_dtype, kv_dtype, 0
284
+
285
+ def _default_kv_stages(self, stages_per_tile):
286
+ """(default ring depth, round smem-limited depths to whole tiles).
287
+ Plain Python (not traced). fp6 KV at 2 CTAs/SM, d=256 (4 stages per
288
+ tile): 2 stages. Depth is monotonic for fp6 at long L — 2 < 3 < 4 <
289
+ 5 < 6 in ncu time; 2 beat 3 by 11-14% at 64K/128K/256K (verify ring
290
+ 20261011-091005), after 3 had beaten 4 by 4-5% in two earlier A/Bs.
291
+ fp8 keeps the whole tile (4) until its own ring sweep says otherwise."""
292
+ if self.max_kv_stages > 0:
293
+ return self.max_kv_stages, False
294
+ if self.kv_format in ("e3m2", "e2m3") and self.ctas_per_sm == 2 and stages_per_tile == 4:
295
+ return 2, False
296
+ return stages_per_tile * (1 if self.ctas_per_sm == 2 else 2), True
297
+
298
+ def _kv_tx_dtype(self, kv_smem_dtype):
299
+ """Element type whose size TMA credits to the KV stage barrier: the
300
+ packed gmem type for the narrow formats, else the SMEM type."""
301
+ return {
302
+ "e3m2": cutlass.Float6E3M2FN,
303
+ "e2m3": cutlass.Float6E2M3FN,
304
+ "e2m1": cutlass.Float4E2M1FN,
305
+ }.get(self.kv_format, kv_smem_dtype)
306
+
307
+ def can_implement(self, num_seqs, heads_q, heads_k, headdim, q_dtype, kv_dtype):
308
+ want = {
309
+ "fp8": cutlass.Float8E4M3FN,
310
+ "e3m2": cutlass.Float6E3M2FN,
311
+ "e2m3": cutlass.Float6E2M3FN,
312
+ "e2m1": cutlass.Float4E2M1FN,
313
+ "nvfp4": cutlass.Float4E2M1FN,
314
+ }[self.kv_format]
315
+ if kv_dtype is not want:
316
+ raise testing.CantImplementError(f"kv_format {self.kv_format} needs KV dtype {want}, got {kv_dtype}")
317
+ if self.kv_format != "fp8" and q_dtype is not cutlass.Float8E4M3FN:
318
+ raise testing.CantImplementError(f"kv_format {self.kv_format} runs on the fp8-Q path only")
319
+ if q_dtype not in (cutlass.BFloat16, cutlass.Float8E4M3FN):
320
+ raise testing.CantImplementError("Q must be BFloat16 or Float8E4M3FN")
321
+ if headdim != self.headdim or headdim % 128 != 0:
322
+ raise testing.CantImplementError(f"headdim({headdim}) must be a multiple of 128")
323
+ if heads_q % heads_k != 0:
324
+ raise testing.CantImplementError(f"heads_q({heads_q}) must be a multiple of heads_k({heads_k})")
325
+ fp8 = q_dtype.width == 8
326
+ if fp8:
327
+ need_cvt = 1 if self.kv_format == "nvfp4" else 0
328
+ if self.convert_warpgroups != need_cvt:
329
+ raise testing.CantImplementError(
330
+ f"fp8-Q path with kv_format {self.kv_format} needs convert_warpgroups={need_cvt}"
331
+ )
332
+ if self.grouped_head_tile % 16 != 0:
333
+ raise testing.CantImplementError("fully-fp8 mma needs grouped_head_tile % 16 == 0")
334
+ elif self.convert_warpgroups < 1:
335
+ raise testing.CantImplementError("bf16 Q path needs cvt warpgroups")
336
+
337
+ @cute.jit
338
+ def __call__(
339
+ self,
340
+ problem_shape: Tuple[
341
+ cutlass.Int32, cutlass.Int32, cutlass.Int32, cutlass.Int32
342
+ ], # num_seqs, h_k, n_q (= q_len * h_r packed rows), d
343
+ max_blocks: cutlass.Int32, # block_table row length
344
+ num_pages: cutlass.Int32, # pages in the KV pool
345
+ num_work: cutlass.Int32, # work items in the plan (grid.x)
346
+ strides: Tuple[
347
+ cutlass.Int32,
348
+ cutlass.Int32, # q_seq, o_seq
349
+ cutlass.Int32,
350
+ cutlass.Int32,
351
+ cutlass.Int32, # k_page, k_row, k_head
352
+ cutlass.Int32,
353
+ cutlass.Int32,
354
+ cutlass.Int32, # v_page, v_row, v_head
355
+ cutlass.Int32,
356
+ cutlass.Int32, # q_descale seq, head
357
+ cutlass.Int32,
358
+ cutlass.Int32, # k_descale seq, head
359
+ cutlass.Int32,
360
+ cutlass.Int32, # v_descale seq, head
361
+ cutlass.Int32,
362
+ cutlass.Int32,
363
+ cutlass.Int32, # ks_page, ks_row, ks_head
364
+ cutlass.Int32,
365
+ cutlass.Int32,
366
+ cutlass.Int32, # vs_page, vs_row, vs_head
367
+ ], # element strides (of the KV element type); see STRIDE_FIELDS
368
+ q_iter: cute.Pointer, # (b, h_k, n_q, d): rows d-contiguous, seq stride free
369
+ k_iter: cute.Pointer, # pages x rows x heads x d, d contiguous, any other strides
370
+ v_iter: cute.Pointer, # same for V (may alias K's buffer, e.g. interleaved)
371
+ ks_iter: cute.Pointer, # nvfp4 only: K scales (e4m3, one per 16 d), else unused
372
+ vs_iter: cute.Pointer, # nvfp4 only: V scales
373
+ block_table_iter: cute.Pointer, # (b, max_blocks) int32
374
+ seq_lens_iter: cute.Pointer, # (b,) int32
375
+ work_iter: cute.Pointer, # (num_work, 4) int32: seq, tile_begin, tile_end, split
376
+ n_splits_iter: cute.Pointer, # (b,) int32: chunks per sequence
377
+ q_descale_iter: cute.Pointer, # fp32, indexed (seq, kv head) via strides
378
+ k_descale_iter: cute.Pointer, # (stride 0, 0 = one per-tensor scale)
379
+ v_descale_iter: cute.Pointer,
380
+ o_iter: cute.Pointer, # (b, h_k, n_q, d): rows d-contiguous, seq stride free
381
+ lse_iter: cute.Pointer, # (b, h_k, n_q) fp32
382
+ o_partial_iter: cute.Pointer, # (max_splits, b, h_k, n_q, d) fp32
383
+ m_partial_iter: cute.Pointer, # (max_splits, b, h_k, n_q) fp32
384
+ l_partial_iter: cute.Pointer, # (max_splits, b, h_k, n_q) fp32
385
+ scale_qs: cutlass.Float32, # softmax scale (1/sqrt(d) by default)
386
+ scale_o: cutlass.Float32,
387
+ stream: cuda.CUstream,
388
+ ):
389
+ ##############################
390
+ # TiledMma creation
391
+ ##############################
392
+ mma_dtype = q_iter.dtype
393
+ kv_dtype = k_iter.dtype
394
+ acc_dtype = cutlass.Float32
395
+ NVFP4, NARROW, kv_mma_dtype, kv_smem_dtype, self.packed_stages = self._kv_types(kv_dtype, mma_dtype)
396
+ assert v_iter.dtype is kv_dtype
397
+ assert o_partial_iter.dtype is acc_dtype
398
+ assert m_partial_iter.dtype is acc_dtype
399
+ assert l_partial_iter.dtype is acc_dtype
400
+ assert lse_iter.dtype is acc_dtype
401
+
402
+ FP8 = mma_dtype.width == 8
403
+ if cutlass.const_expr(FP8):
404
+ assert self.convert_warpgroups == int(NVFP4)
405
+ else:
406
+ assert self.convert_warpgroups >= 1
407
+
408
+ PS = self.page_size
409
+ max_splits = self.max_splits
410
+
411
+ # Block tile sets the granularity at which threadblocks consume work
412
+ blk_tile_s = tile_s_rows
413
+ blk_tile_h = self.grouped_head_tile
414
+ blk_tile_d = self.headdim
415
+ blk_tile_shd = (blk_tile_s, blk_tile_h, blk_tile_d)
416
+
417
+ # MMA tile sets the granularity at which TMAs + MMAs are issued
418
+ mma_tile_m = 128
419
+ mma_tile_n = self.grouped_head_tile
420
+ mma_tile_k = 128
421
+ mma_tile_mnk = (mma_tile_m, mma_tile_n, mma_tile_k)
422
+
423
+ # A-operand source: fp8 reads K/V straight from the TMA'd SMEM ring;
424
+ # bf16 reads the cvt output from TMEM.
425
+ a_operand_source = tcgen05.OperandSource.SMEM if cutlass.const_expr(FP8) else tcgen05.OperandSource.TMEM
426
+
427
+ # GEMM1: S(s, n) = K(s, d) . Q(n, d) — A-operand K.
428
+ # Narrow formats: A = K/V in fp6/fp4, B = Q/P in e4m3 (kind::f8f6f4
429
+ # takes mixed A/B types). nvfp4 is converted to e4m3 before the MMA.
430
+ # (kv_mma_dtype / kv_smem_dtype come from self._kv_types)
431
+ tiled_mma_kq = sm100_utils.make_trivial_tiled_mma(
432
+ kv_mma_dtype,
433
+ mma_dtype,
434
+ OperandMajorMode.K, # K
435
+ OperandMajorMode.K, # Q
436
+ acc_dtype,
437
+ tcgen05.CtaGroup.ONE,
438
+ mma_tile_mnk[:2],
439
+ a_operand_source,
440
+ )
441
+
442
+ # GEMM2: O(d, n) = V(d, s) . P(s, n). V is headdim-major (MN-major)
443
+ # in SMEM on the fp8 path; on the bf16 path V comes from TMEM, which
444
+ # must be K-major. The a-major must match smem_layout_v's orientation.
445
+ v_major_mode = OperandMajorMode.MN if cutlass.const_expr(FP8) else OperandMajorMode.K
446
+ tiled_mma_vp = sm100_utils.make_trivial_tiled_mma(
447
+ kv_mma_dtype,
448
+ mma_dtype,
449
+ v_major_mode,
450
+ OperandMajorMode.MN, # P
451
+ acc_dtype,
452
+ tcgen05.CtaGroup.ONE,
453
+ mma_tile_mnk[:2],
454
+ a_operand_source,
455
+ )
456
+
457
+ # Q stages
458
+ self.q_stages = blk_tile_d // mma_tile_k
459
+
460
+ # Perf heuristics
461
+ max_cvt_stages = 4 if self.grouped_head_tile == 32 else 8
462
+ # KV ring depth: a stage is freed only when the GEMM that read it
463
+ # completes, so loads for tile s+2 wait on GEMM2(s). Deeper rings let
464
+ # the loader run further ahead of the softmax/GEMM chain at long L.
465
+ # Default depth = a WHOLE number of KV tiles (one tile = tiles_dk K
466
+ # stages + tiles_dm V stages; 4 at d=256): 1 tile at 2 CTAs/SM, 2 at
467
+ # 1 CTA/SM. Measured on Thor (ncu, cold L2, d=256): 2/SM depth 4 beat
468
+ # 2, 3, 5, 6 at 8K/64K/256K, and 1/SM depth 8 beat 13. Ring depths
469
+ # that are not tile multiples rotate a tile's K/V across slots and
470
+ # stream worse on LPDDR5X; deeper is not better.
471
+ # Exception, fp6 KV (e3m2/e2m3) at 2/SM, d=256: shallower is faster —
472
+ # depth 2 beat 3 by 11-14% at 64K-256K, 3 beat 4 by 4-5%, 6 was worst
473
+ # — see _default_kv_stages.
474
+ stages_per_tile = (blk_tile_d // mma_tile_k) + (blk_tile_d // mma_tile_m)
475
+ # Plain ternaries, not if/else: the DSL stages if/else into regions
476
+ # whose bindings don't escape (NameError after the block).
477
+ max_kv_stages, round_to_tile = self._default_kv_stages(stages_per_tile)
478
+
479
+ # TMEM: S stages + O + (bf16 only) the cvt ring
480
+ tmem_alloc_cols = mma_tile_n * self.sp_stages
481
+ tmem_alloc_cols += mma_tile_n * self.o_stages * (blk_tile_d // mma_tile_m)
482
+ tmem_capacity = 512 // self.ctas_per_sm # 512 cols per SM, split by CTAs
483
+ cvt_stage_cols = mma_tile_k * mma_dtype.width // 32
484
+ if cutlass.const_expr(FP8):
485
+ self.cvt_stages = 0
486
+ else:
487
+ self.cvt_stages = min((tmem_capacity - tmem_alloc_cols) // cvt_stage_cols, max_cvt_stages)
488
+ tmem_alloc_cols += self.cvt_stages * cvt_stage_cols
489
+ self.tmem_alloc_cols = 2 ** math.ceil(math.log2(tmem_alloc_cols)) # PO2
490
+ # A second co-resident CTA blocks in tcgen05.alloc if the first holds
491
+ # more than its share.
492
+ assert self.tmem_alloc_cols <= tmem_capacity
493
+
494
+ _note(f"\tcvt stages: {self.cvt_stages}")
495
+
496
+ # SMEM budget -> KV stages
497
+ mbarrier_reserved_bytes = 768
498
+ smem_alloc_bits = mbarrier_reserved_bytes * 8
499
+ smem_alloc_bits += mma_tile_n * acc_dtype.width # colmax
500
+ smem_alloc_bits += mma_tile_n * warpgroup_warps * acc_dtype.width # colsum
501
+ smem_alloc_bits += mma_tile_n * mma_tile_k * self.q_stages * mma_dtype.width # Q
502
+ smem_alloc_bits += mma_tile_m * mma_tile_n * self.sp_stages * mma_dtype.width # P
503
+ smem_alloc_bits += 3 * swizzle_align * 8 # worst-case padding: Q, KV, P
504
+ # nvfp4: packed e2m1 tiles (TMA -> convert warpgroup), one K + one V
505
+ # tile in flight; the e4m3 ring below is what the MMA reads
506
+ smem_alloc_bits += self.packed_stages * (blk_tile_s * blk_tile_d * 4 + 128 * 8)
507
+
508
+ # Thor (sm_110a): 228 KB smem per SM, 1 KB reserved per resident CTA,
509
+ # at most 227 KB (232448 B) per CTA. At 2 CTAs/SM each gets ~113 KB
510
+ # (6 KV stages instead of 8 — still 96 KB of KV in flight per CTA).
511
+ smem_capacity = min(232448, 233472 // self.ctas_per_sm - 1024)
512
+ # SMEM ring dtype (from self._kv_types): 1-byte containers for the
513
+ # narrow formats (TMA unpacks), e4m3 for fp8 and converted nvfp4
514
+ self.kv_stages = (smem_capacity * 8 - smem_alloc_bits) // (mma_tile_m * mma_tile_k * kv_smem_dtype.width)
515
+ self.kv_stages = min(self.kv_stages, max_kv_stages)
516
+ # smem-limited below the target: keep a whole number of tiles
517
+ self.kv_stages = (
518
+ self.kv_stages - self.kv_stages % stages_per_tile
519
+ if round_to_tile and self.kv_stages >= stages_per_tile
520
+ else self.kv_stages
521
+ )
522
+
523
+ _note(f"\tkv stages: {self.kv_stages}, kv format: {self.kv_format}, packed stages: {self.packed_stages}")
524
+ _note(f"\tpdl: {self.pdl}, l2 prefetch: {self.l2_prefetch} tiles ahead, kv evict_first: {self.kv_evict_first}")
525
+
526
+ ##############################
527
+ # Global tensors
528
+ ##############################
529
+ b, h_k, n_q, d = problem_shape
530
+ (
531
+ q_seq,
532
+ o_seq,
533
+ k_page,
534
+ k_row,
535
+ k_head,
536
+ v_page,
537
+ v_row,
538
+ v_head,
539
+ qd_seq,
540
+ qd_head,
541
+ kd_seq,
542
+ kd_head,
543
+ vd_seq,
544
+ vd_head,
545
+ ks_page,
546
+ ks_row,
547
+ ks_head,
548
+ vs_page,
549
+ vs_row,
550
+ vs_head,
551
+ ) = strides
552
+
553
+ # Q: (n_q, d, (h_k, b)). Rows of one sequence are packed (n_q, h_k)
554
+ # with d contiguous; the per-sequence stride is free so vLLM's
555
+ # fused-QKV `query` view is read in place (TMA strides only need
556
+ # 16-B multiples).
557
+ q = cute.make_tensor(
558
+ q_iter,
559
+ cute.make_layout((n_q, d, (h_k, b)), stride=(d, 1, (n_q * d, q_seq))),
560
+ )
561
+ # KV cache, any layout with d contiguous (vLLM NHD, the interleaved
562
+ # (blocks, heads, tokens, 2*d) layout, FlashInfer (blocks, 2, ...)):
563
+ # K view: (PS, d, (h_k, num_pages)) — GEMM1 A-operand (M=s, K=d)
564
+ # V view: (d, PS, (h_k, num_pages)) — GEMM2 A-operand (M=d, K=s)
565
+ # A page of one KV head is a (PS, d) block: page index and head
566
+ # together are the L-mode, so a page-table lookup is just a TMA
567
+ # coordinate.
568
+ k = cute.make_tensor(
569
+ k_iter,
570
+ cute.make_layout((PS, d, (h_k, num_pages)), stride=(k_row, 1, (k_head, k_page))),
571
+ )
572
+ v = cute.make_tensor(
573
+ v_iter,
574
+ cute.make_layout((d, PS, (h_k, num_pages)), stride=(1, v_row, (v_head, v_page))),
575
+ )
576
+ # nvfp4: packed K and V are both token-major (PS, d) blocks; scales are
577
+ # (PS, d/16) e4m3 per (head, page). Unused (dummy) on other formats.
578
+ v_tok = cute.make_tensor(
579
+ v_iter,
580
+ cute.make_layout((PS, d, (h_k, num_pages)), stride=(v_row, 1, (v_head, v_page))),
581
+ )
582
+ n_groups = d // NVFP4_GROUP
583
+ k_scales = cute.make_tensor(
584
+ ks_iter,
585
+ cute.make_layout((PS, n_groups, (h_k, num_pages)), stride=(ks_row, 1, (ks_head, ks_page))),
586
+ )
587
+ v_scales = cute.make_tensor(
588
+ vs_iter,
589
+ cute.make_layout((PS, n_groups, (h_k, num_pages)), stride=(vs_row, 1, (vs_head, vs_page))),
590
+ )
591
+ block_table = cute.make_tensor(
592
+ block_table_iter,
593
+ cute.make_layout((b, max_blocks), stride=(max_blocks, 1)), # row-major
594
+ )
595
+ seq_lens = cute.make_tensor(seq_lens_iter, cute.make_layout((b,)))
596
+ work = cute.make_tensor(work_iter, cute.make_layout((num_work, 4), stride=(4, 1)))
597
+ n_splits = cute.make_tensor(n_splits_iter, cute.make_layout((b,)))
598
+ # Descales indexed by coord_hb = (kv head, seq); stride 0 broadcasts
599
+ # a per-tensor scale (vLLM layer._k_scale) with no copy.
600
+ q_descale = cute.make_tensor(q_descale_iter, cute.make_layout(((h_k, b),), stride=((qd_head, qd_seq),)))
601
+ k_descale = cute.make_tensor(k_descale_iter, cute.make_layout(((h_k, b),), stride=((kd_head, kd_seq),)))
602
+ v_descale = cute.make_tensor(v_descale_iter, cute.make_layout(((h_k, b),), stride=((vd_head, vd_seq),)))
603
+
604
+ o = cute.make_tensor(
605
+ o_iter,
606
+ cute.make_layout((d, n_q, (h_k, b)), stride=(1, d, (n_q * d, o_seq))),
607
+ )
608
+ lse = cute.make_tensor(
609
+ lse_iter,
610
+ cute.make_ordered_layout(shape=(n_q, (h_k, b)), order=(0, (1, 2))),
611
+ )
612
+ o_partial = cute.make_tensor(
613
+ o_partial_iter,
614
+ cute.make_ordered_layout(shape=(d, n_q, (h_k, b), max_splits), order=(0, 1, (2, 3), 4)),
615
+ )
616
+ m_partial = cute.make_tensor(
617
+ m_partial_iter,
618
+ cute.make_ordered_layout(shape=(n_q, (h_k, b), max_splits), order=(0, (1, 2), 3)),
619
+ )
620
+ l_partial = cute.make_tensor(
621
+ l_partial_iter,
622
+ cute.make_ordered_layout(shape=(n_q, (h_k, b), max_splits), order=(0, (1, 2), 3)),
623
+ )
624
+
625
+ ##############################
626
+ # SMEM layouts
627
+ ##############################
628
+ # (MMA, MMA_M/N, MMA_K, Stages)
629
+ smem_layout_q = sm100_utils.make_smem_layout_b(tiled_mma_kq, mma_tile_mnk, mma_dtype, self.q_stages)
630
+ smem_layout_k = sm100_utils.make_smem_layout_a(tiled_mma_kq, mma_tile_mnk, kv_smem_dtype, self.kv_stages)
631
+ smem_layout_v = sm100_utils.make_smem_layout_a(
632
+ tiled_mma_vp, mma_tile_mnk, kv_smem_dtype, self.kv_stages, is_k_major=False
633
+ ) # V is headdim-major (GEMM2 M-major) in gmem + smem
634
+
635
+ # O tile shape (used only to derive the TMEM O fragment / softmax
636
+ # partitions; the epilogue stores O from registers).
637
+ smem_layout_atom_o = tcgen05.make_smem_layout_atom(tcgen05.mma.SmemLayoutAtomKind.MN_SW128, acc_dtype)
638
+ smem_layout_o = cute.tile_to_shape(smem_layout_atom_o, (blk_tile_d, blk_tile_h), order=(1, 0))
639
+ smem_layout_o = cute.flat_divide(smem_layout_o, (mma_tile_m, mma_tile_n))
640
+
641
+ ##############################
642
+ # TMA atoms
643
+ ##############################
644
+ tma_load_op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SOp()
645
+
646
+ tma_atom_q, tma_tensor_q = cute.nvgpu.make_tiled_tma_atom_B(
647
+ tma_load_op,
648
+ q,
649
+ cute.select(smem_layout_q, mma_modes),
650
+ mma_tile_mnk,
651
+ tiled_mma_kq,
652
+ )
653
+
654
+ if cutlass.const_expr(NVFP4):
655
+ # Packed e2m1 boxes of one page x the whole head dim, into a plain
656
+ # row-major (token, d) packed tile; the convert warpgroup reads it.
657
+ packed_box = cute.make_layout((PS, blk_tile_d), stride=(blk_tile_d, 1))
658
+ tma_atom_k, tma_tensor_k = cute.nvgpu.cpasync.make_tiled_tma_atom(
659
+ tma_load_op, k, packed_box, (PS, blk_tile_d)
660
+ )
661
+ tma_atom_v, tma_tensor_v = cute.nvgpu.cpasync.make_tiled_tma_atom(
662
+ tma_load_op, v_tok, packed_box, (PS, blk_tile_d)
663
+ )
664
+ elif cutlass.const_expr(PS == blk_tile_s):
665
+ # One page == one KV tile: the same full-stage TMA box as a dense
666
+ # KV tensor, indexed by (head, page) in the L-mode.
667
+ tma_atom_k, tma_tensor_k = cute.nvgpu.make_tiled_tma_atom_A(
668
+ tma_load_op,
669
+ k,
670
+ cute.select(smem_layout_k, mma_modes),
671
+ mma_tile_mnk,
672
+ tiled_mma_kq,
673
+ internal_type=kv_smem_dtype,
674
+ )
675
+ tma_atom_v, tma_tensor_v = cute.nvgpu.make_tiled_tma_atom_A(
676
+ tma_load_op,
677
+ v,
678
+ cute.select(smem_layout_v, mma_modes),
679
+ mma_tile_mnk,
680
+ tiled_mma_vp,
681
+ internal_type=kv_smem_dtype,
682
+ )
683
+ else:
684
+ # PS < 128: a KV tile spans 128 / PS pages, one TMA box per page.
685
+ # The MMA-partitioned stage (MMA, #MMA_M, #MMA_K) flattens colex
686
+ # to m + 128 * k, so (rows, cols) sub-boxes are relabeled with a
687
+ # (1, 128)-strided layout; the swizzle stays in the ComposedLayout.
688
+ k_box = cute.composition(
689
+ cute.select(smem_layout_k, mma_modes),
690
+ cute.make_layout((PS, mma_tile_k), stride=(1, mma_tile_m)),
691
+ ) # (s rows of one page, d)
692
+ v_box = cute.composition(
693
+ cute.select(smem_layout_v, mma_modes),
694
+ cute.make_layout((mma_tile_m, PS), stride=(1, mma_tile_m)),
695
+ ) # (d, s rows of one page)
696
+ tma_atom_k, tma_tensor_k = cute.nvgpu.cpasync.make_tiled_tma_atom(
697
+ tma_load_op, k, k_box, (PS, mma_tile_k), internal_type=kv_smem_dtype
698
+ )
699
+ tma_atom_v, tma_tensor_v = cute.nvgpu.cpasync.make_tiled_tma_atom(
700
+ tma_load_op, v, v_box, (mma_tile_m, PS), internal_type=kv_smem_dtype
701
+ )
702
+
703
+ ##############################
704
+ # Decode kernel launch
705
+ ##############################
706
+ scale_qs_log2_e = scale_qs * log2_e
707
+
708
+ n_tiles = cute.ceil_div(n_q, blk_tile_h)
709
+ grid = (num_work, n_tiles, h_k) # one CTA per (work item, q tile, kv head)
710
+
711
+ self.decode(
712
+ blk_tile_shd,
713
+ mma_tile_mnk,
714
+ tiled_mma_kq,
715
+ tiled_mma_vp,
716
+ mma_dtype,
717
+ kv_smem_dtype,
718
+ smem_layout_q,
719
+ tma_atom_q,
720
+ tma_tensor_q,
721
+ smem_layout_k,
722
+ tma_atom_k,
723
+ tma_tensor_k,
724
+ smem_layout_v,
725
+ tma_atom_v,
726
+ tma_tensor_v,
727
+ smem_layout_o,
728
+ block_table,
729
+ seq_lens,
730
+ work,
731
+ n_splits,
732
+ q_descale,
733
+ k_descale,
734
+ v_descale,
735
+ k_scales,
736
+ v_scales,
737
+ o,
738
+ lse,
739
+ o_partial,
740
+ m_partial,
741
+ l_partial,
742
+ h_k,
743
+ n_q,
744
+ num_pages,
745
+ scale_qs,
746
+ scale_qs_log2_e,
747
+ scale_o,
748
+ ).launch(
749
+ grid=grid,
750
+ block=[self.threads_per_cta, 1, 1],
751
+ cluster=[1, 1, 1],
752
+ stream=stream,
753
+ min_blocks_per_mp=self.ctas_per_sm, # caps regs/thread for 2 CTAs/SM
754
+ **self.launch_extra,
755
+ )
756
+
757
+ ##############################
758
+ # Split-KV reduction (merges only sequences the plan split)
759
+ ##############################
760
+ # Compiled out when the plan splits no sequence (every sequence wrote
761
+ # its final output in decode): saves a ~7 us no-op launch.
762
+ if cutlass.const_expr(max_splits > 1 and self.with_reduction):
763
+ h_q = h_k * n_q
764
+ o_r = cute.make_tensor(o_iter, cute.make_layout((d, h_q, b), stride=(1, d, o_seq)))
765
+ lse_r = cute.make_tensor(lse_iter, cute.make_layout((h_q, b)))
766
+ o_partial_r = cute.make_tensor(o_partial_iter, cute.make_layout((d, h_q, b, max_splits)))
767
+ m_partial_r = cute.make_tensor(m_partial_iter, cute.make_layout((h_q, b, max_splits)))
768
+ l_partial_r = cute.make_tensor(l_partial_iter, cute.make_layout((h_q, b, max_splits)))
769
+ d_per_blk = 128
770
+ d_blks = cute.ceil_div(d, d_per_blk)
771
+ self.reduction(
772
+ o_r,
773
+ lse_r,
774
+ o_partial_r,
775
+ m_partial_r,
776
+ l_partial_r,
777
+ n_splits,
778
+ scale_o,
779
+ self.pdl,
780
+ ).launch(
781
+ grid=[d_blks, h_q, b],
782
+ block=[d_per_blk, 1, 1],
783
+ cluster=[1, 1, 1],
784
+ stream=stream,
785
+ **self.launch_extra,
786
+ )
787
+
788
+ @cute.kernel
789
+ def decode(
790
+ self,
791
+ # MMA
792
+ blk_tile_shd: cute.Tile,
793
+ mma_tile_mnk: cute.Tile,
794
+ tiled_mma_kq: cute.TiledMma,
795
+ tiled_mma_vp: cute.TiledMma,
796
+ q_dtype: Type[cutlass.Numeric],
797
+ kv_smem_dtype: Type[cutlass.Numeric],
798
+ # Q
799
+ smem_layout_q: cute.ComposedLayout,
800
+ tma_atom_q: cute.CopyAtom,
801
+ mQ: cute.Tensor,
802
+ # K
803
+ smem_layout_k: cute.ComposedLayout,
804
+ tma_atom_k: cute.CopyAtom,
805
+ mK: cute.Tensor,
806
+ # V
807
+ smem_layout_v: cute.ComposedLayout,
808
+ tma_atom_v: cute.CopyAtom,
809
+ mV: cute.Tensor,
810
+ # O tile shape
811
+ smem_layout_o: cute.ComposedLayout,
812
+ # Paging / varlen / scales
813
+ mBlockTable: cute.Tensor,
814
+ mSeqLens: cute.Tensor,
815
+ mWork: cute.Tensor,
816
+ mNSplits: cute.Tensor,
817
+ mQD: cute.Tensor,
818
+ mKD: cute.Tensor,
819
+ mVD: cute.Tensor,
820
+ mKS: cute.Tensor, # nvfp4 K scales (PS, d/16, (h_k, pages)), else unused
821
+ mVS: cute.Tensor,
822
+ # Outputs
823
+ mO: cute.Tensor,
824
+ mLSE: cute.Tensor,
825
+ mO_partial: cute.Tensor,
826
+ mM_partial: cute.Tensor,
827
+ mL_partial: cute.Tensor,
828
+ # Scalars
829
+ h_k: cutlass.Int32,
830
+ n_q: cutlass.Int32,
831
+ num_pages: cutlass.Int32,
832
+ scale_qs: cutlass.Float32,
833
+ scale_qs_log2_e: cutlass.Float32,
834
+ scale_o: cutlass.Float32,
835
+ ):
836
+ # Read special registers
837
+ work_idx, coord_hr, h_k_idx = cute.arch.block_idx()
838
+ tidx, _, _ = cute.arch.thread_idx()
839
+ lane_idx = cute.arch.lane_idx()
840
+ warp_idx = cute.arch.make_warp_uniform(tidx // warp_threads)
841
+ warpgroup_idx = cute.arch.make_warp_uniform(tidx // warpgroup_threads)
842
+ warpgroup_tidx = tidx % warpgroup_threads
843
+ warpgroup_widx = warp_idx % warpgroup_warps
844
+ init_warp = 0
845
+
846
+ # No multicast
847
+ mcast_coord = 0
848
+ mcast_layout = cute.make_layout((1, 1, 1, 1)) # vmnk
849
+
850
+ # Alias types
851
+ mma_dtype = q_dtype
852
+ acc_dtype = cutlass.Float32
853
+
854
+ # Compile-time selectors (plain Python values)
855
+ FP8 = cutlass.const_expr(mma_dtype.width == 8)
856
+ NVFP4 = cutlass.const_expr(self.kv_format == "nvfp4")
857
+ SINGLE_SPLIT = cutlass.const_expr(self.max_splits == 1)
858
+ PS = self.page_size
859
+ p_scale = FP8_P_SCALE if FP8 else 1.0
860
+
861
+ # Shapes for MMA tile indexing
862
+ blk_tile_s, blk_tile_h, blk_tile_d = blk_tile_shd
863
+ mma_tile_m, mma_tile_n, mma_tile_k = mma_tile_mnk
864
+ tiles_dm, tiles_sk = cute.ceil_div((blk_tile_d, blk_tile_s), (mma_tile_m, mma_tile_k))
865
+ tiles_dk, tiles_sm = cute.ceil_div((blk_tile_d, blk_tile_s), (mma_tile_k, mma_tile_m))
866
+ assert tiles_sm == 1
867
+ assert tiles_sk == 1
868
+ pages_per_tile = blk_tile_s // PS
869
+ L2P = self.l2_prefetch # compile-time
870
+
871
+ # PDL: this grid may start while the previous kernel in the stream
872
+ # (QKV/rope, KV-cache write, Q quantization, an earlier layer's
873
+ # reduction reading the partial buffers) is still finishing. Wait for
874
+ # it before the first global read/write; then let our dependent (the
875
+ # reduction) launch early — it waits for this grid in turn, so its
876
+ # launch latency and prologue hide behind our tail.
877
+ if cutlass.const_expr(self.pdl):
878
+ cute.arch.griddepcontrol_wait()
879
+ cute.arch.griddepcontrol_launch_dependents()
880
+
881
+ # Work item from the plan: this CTA streams the CONTIGUOUS tile range
882
+ # [tile_begin, tile_end) of one sequence and writes partial slot
883
+ # split_slot (or the final output if the sequence has one chunk).
884
+ b_idx = mWork[work_idx, 0]
885
+ tile_begin = mWork[work_idx, 1]
886
+ tile_end = mWork[work_idx, 2]
887
+ split_slot = mWork[work_idx, 3]
888
+ coord_hb = h_k_idx + h_k * b_idx # (h_k, b) L-mode index
889
+ seq_len = mSeqLens[b_idx]
890
+ n_splits_seq = mNSplits[b_idx]
891
+ iters_s = tile_end - tile_begin
892
+ exit_early = iters_s <= 0 # padding work item
893
+ prefetch_iters = self.sp_stages - 1
894
+ if iters_s < prefetch_iters:
895
+ prefetch_iters = iters_s
896
+
897
+ n_base = coord_hr * blk_tile_h
898
+ lane_store_max = mma_tile_n == warp_threads or lane_idx < mma_tile_n
899
+
900
+ # Smem alloc helper
901
+ svector_align = 16
902
+ smem = utils.SmemAllocator()
903
+
904
+ ##############################
905
+ # Prefetch TMA descriptor
906
+ ##############################
907
+ if warp_idx == init_warp and not exit_early:
908
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_q)
909
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_k)
910
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_v)
911
+ init_warp += 1
912
+
913
+ ##############################
914
+ # Tmem Allocation
915
+ ##############################
916
+ tmem_ptr_smem_ptr = smem.allocate_array(cutlass.Int32)
917
+ if warp_idx == init_warp and not exit_early:
918
+ cute.arch.alloc_tmem(self.tmem_alloc_cols, tmem_ptr_smem_ptr)
919
+ init_warp += 1
920
+
921
+ ##############################
922
+ # Pipeline Allocation + Init
923
+ ##############################
924
+ q_pipeline_ptr = smem.allocate_array(cutlass.Int64, self.q_stages * 2)
925
+ pk_pipeline_ptr = smem.allocate_array(cutlass.Int64, max(1, self.packed_stages) * 2)
926
+ kv_pipeline_ptr = smem.allocate_array(cutlass.Int64, self.kv_stages * 2)
927
+ cvt_pipeline_ptr = smem.allocate_array(cutlass.Int64, max(1, self.cvt_stages * 2))
928
+ s_pipeline_ptr = smem.allocate_array(cutlass.Int64, self.sp_stages * 2)
929
+ p_pipeline_ptr = smem.allocate_array(cutlass.Int64, self.sp_stages * 2)
930
+ o_pipeline_ptr = smem.allocate_array(cutlass.Int64, self.o_stages * 2)
931
+
932
+ # Named barriers
933
+ softmax_nbar = pipeline.NamedBarrier(barrier_id=1, num_threads=warpgroup_threads)
934
+ mma_kq_nbar = pipeline.NamedBarrier(barrier_id=2, num_threads=64)
935
+ mma_vp_nbar = pipeline.NamedBarrier(barrier_id=3, num_threads=64)
936
+
937
+ # Thread cooperatives
938
+ elect_one_cooperative = pipeline.CooperativeGroup(pipeline.Agent.Thread)
939
+ warpgroup_cooperative = pipeline.CooperativeGroup(pipeline.Agent.Thread, warpgroup_threads)
940
+ mma_group = elect_one_cooperative
941
+ tma_group = elect_one_cooperative
942
+ cvt_group = warpgroup_cooperative
943
+ softmax_group = warpgroup_cooperative
944
+
945
+ q_producer, q_consumer = pipeline.PipelineTmaAsync.create(
946
+ num_stages=self.q_stages,
947
+ producer_group=tma_group,
948
+ consumer_group=softmax_group,
949
+ tx_count=cute.size_in_bytes(q_dtype, cute.select(smem_layout_q, mma_modes)),
950
+ barrier_storage=q_pipeline_ptr,
951
+ tidx=mcast_coord,
952
+ cta_layout_vmnk=mcast_layout,
953
+ defer_sync=True,
954
+ ).make_participants()
955
+
956
+ # KV ring (always TMA; a stage is filled by 1 box or 128/PS page boxes,
957
+ # tx_count is the full stage either way — OOB boxes are zero-filled
958
+ # and still credit their bytes).
959
+ # fp8 : UMMA consumer (PipelineTmaUmma): released by tcgen05.commit
960
+ # once the mma that read the stage completes; each stage is read
961
+ # by exactly one mma warp -> consumer count 1.
962
+ # bf16: cvt consumer (PipelineTmaAsync). The cvt warpgroups STRIDE
963
+ # over stages, so each stage is released by ONE warpgroup.
964
+ # TMA credits the barrier with the bytes it READS from gmem: for the
965
+ # narrow formats that is the packed size (fp6: 12 KB, fp4: 8 KB per
966
+ # 128x128 stage), not the unpacked 1-B/elem SMEM size — counting
967
+ # the SMEM bytes would leave the stage barrier waiting forever.
968
+ kv_tx_dtype = self._kv_tx_dtype(kv_smem_dtype)
969
+ kv_tx_count = cute.size_in_bytes(kv_tx_dtype, cute.select(smem_layout_k, mma_modes))
970
+ pk_producer, pk_consumer = None, None
971
+ if cutlass.const_expr(NVFP4):
972
+ # TMA -> packed e2m1 ring -> convert warpgroup (one K or V tile
973
+ # per stage) ...
974
+ pk_producer, pk_consumer = pipeline.PipelineTmaAsync.create(
975
+ num_stages=self.packed_stages,
976
+ producer_group=tma_group,
977
+ consumer_group=cvt_group,
978
+ tx_count=blk_tile_s * blk_tile_d // 2, # packed bytes of a tile
979
+ barrier_storage=pk_pipeline_ptr,
980
+ tidx=mcast_coord,
981
+ cta_layout_vmnk=mcast_layout,
982
+ defer_sync=True,
983
+ ).make_participants()
984
+ # ... -> e4m3 ring (the normal fp8 A-operand ring) -> MMA
985
+ kv_producer, kv_consumer = pipeline.PipelineAsyncUmma.create(
986
+ num_stages=self.kv_stages,
987
+ producer_group=cvt_group,
988
+ consumer_group=mma_group,
989
+ barrier_storage=kv_pipeline_ptr,
990
+ defer_sync=True,
991
+ ).make_participants()
992
+ cvt_producer, cvt_consumer = None, None
993
+ a_consumer = kv_consumer
994
+ elif cutlass.const_expr(FP8):
995
+ kv_producer, kv_consumer = pipeline.PipelineTmaUmma.create(
996
+ num_stages=self.kv_stages,
997
+ producer_group=tma_group,
998
+ consumer_group=mma_group,
999
+ tx_count=kv_tx_count,
1000
+ barrier_storage=kv_pipeline_ptr,
1001
+ cta_layout_vmnk=mcast_layout,
1002
+ defer_sync=True,
1003
+ ).make_participants()
1004
+ cvt_producer, cvt_consumer = None, None
1005
+ a_consumer = kv_consumer
1006
+ else:
1007
+ kv_producer, kv_consumer = pipeline.PipelineTmaAsync.create(
1008
+ num_stages=self.kv_stages,
1009
+ producer_group=tma_group,
1010
+ consumer_group=cvt_group,
1011
+ tx_count=kv_tx_count,
1012
+ barrier_storage=kv_pipeline_ptr,
1013
+ tidx=mcast_coord,
1014
+ cta_layout_vmnk=mcast_layout,
1015
+ defer_sync=True,
1016
+ ).make_participants()
1017
+ cvt_producer, cvt_consumer = pipeline.PipelineAsyncUmma.create(
1018
+ num_stages=max(1, self.cvt_stages),
1019
+ producer_group=cvt_group,
1020
+ consumer_group=mma_group,
1021
+ barrier_storage=cvt_pipeline_ptr,
1022
+ defer_sync=True,
1023
+ ).make_participants()
1024
+ a_consumer = cvt_consumer
1025
+
1026
+ s_producer, s_consumer = pipeline.PipelineUmmaAsync.create(
1027
+ num_stages=self.sp_stages,
1028
+ producer_group=mma_group,
1029
+ consumer_group=softmax_group,
1030
+ barrier_storage=s_pipeline_ptr,
1031
+ defer_sync=True,
1032
+ ).make_participants()
1033
+ p_producer, p_consumer = pipeline.PipelineAsyncUmma.create(
1034
+ num_stages=self.sp_stages,
1035
+ producer_group=softmax_group,
1036
+ consumer_group=mma_group,
1037
+ barrier_storage=p_pipeline_ptr,
1038
+ defer_sync=True,
1039
+ ).make_participants()
1040
+ o_producer, o_consumer = pipeline.PipelineUmmaAsync.create(
1041
+ num_stages=self.o_stages,
1042
+ producer_group=mma_group,
1043
+ consumer_group=softmax_group,
1044
+ barrier_storage=o_pipeline_ptr,
1045
+ defer_sync=True,
1046
+ ).make_participants()
1047
+
1048
+ # Ensure visibility of local mbarrier inits and tmem alloc
1049
+ cute.arch.sync_threads()
1050
+
1051
+ ##############################
1052
+ # MMA Partition + Allocate
1053
+ ##############################
1054
+ thrblk_mma_kq = tiled_mma_kq.get_slice(0)
1055
+ thrblk_mma_vp = tiled_mma_vp.get_slice(0)
1056
+
1057
+ # M - colmax
1058
+ sM_layout = cute.make_layout(shape=(mma_tile_m, mma_tile_n), stride=(0, 1))
1059
+ sM = smem.allocate_tensor(acc_dtype, sM_layout, svector_align)
1060
+ tCsM = thrblk_mma_kq.partition_C(sM)
1061
+
1062
+ # L - colsum
1063
+ sL_layout = cute.make_layout(shape=(mma_tile_m, mma_tile_n, warpgroup_warps), stride=(0, 1, mma_tile_n))
1064
+ sL = smem.allocate_tensor(acc_dtype, sL_layout, svector_align)
1065
+ tCsL = thrblk_mma_kq.partition_C(sL)
1066
+
1067
+ # Q (swizzled: 1024-B aligned)
1068
+ tBsQ = smem.allocate_tensor(
1069
+ q_dtype, smem_layout_q.outer, swizzle_align, smem_layout_q.inner
1070
+ ) # (MMA, #MMA_N, #MMA_K, q_stages)
1071
+
1072
+ # K (swizzled KV ring: 1024-B aligned)
1073
+ tAsK = smem.allocate_tensor(
1074
+ kv_smem_dtype, smem_layout_k.outer, swizzle_align, smem_layout_k.inner
1075
+ ) # (MMA, #MMA_M, #MMA_K, kv_stages)
1076
+ if cutlass.const_expr(FP8):
1077
+ tAtK_cvt = thrblk_mma_kq.make_fragment_A(tAsK)
1078
+ else:
1079
+ tAtK_cvt_shape = tiled_mma_kq.partition_shape_A((mma_tile_m, mma_tile_k, self.cvt_stages))
1080
+ tAtK_cvt = thrblk_mma_kq.make_fragment_A(tAtK_cvt_shape)
1081
+
1082
+ # V (shares the KV ring)
1083
+ tAsV_iterator = cute.recast_ptr(tAsK.iterator, smem_layout_v.inner, dtype=kv_smem_dtype)
1084
+ tAsV = cute.make_tensor(tAsV_iterator, smem_layout_v.outer) # (MMA, #MMA_M, #MMA_K, kv_stages)
1085
+ if cutlass.const_expr(NVFP4):
1086
+ # packed e2m1 tiles, row-major (token, d) per stage
1087
+ sKVp = smem.allocate_tensor(
1088
+ cutlass.Float4E2M1FN,
1089
+ cute.make_layout(
1090
+ (blk_tile_s, blk_tile_d, self.packed_stages),
1091
+ stride=(blk_tile_d, 1, blk_tile_s * blk_tile_d),
1092
+ ),
1093
+ 128,
1094
+ )
1095
+ if cutlass.const_expr(FP8):
1096
+ tAtV_cvt = thrblk_mma_vp.make_fragment_A(tAsV)
1097
+ else:
1098
+ tAtV_cvt_shape = tiled_mma_vp.partition_shape_A((mma_tile_m, mma_tile_k, self.cvt_stages))
1099
+ tAtV_cvt = thrblk_mma_vp.make_fragment_A(tAtV_cvt_shape)
1100
+
1101
+ # S
1102
+ tCtS_shape = tiled_mma_kq.partition_shape_C((mma_tile_m, mma_tile_n, self.sp_stages))
1103
+ tCtS = thrblk_mma_kq.make_fragment_C(tCtS_shape)
1104
+
1105
+ # P - Treat MN C tile of BMM0 as NM B tile of BMM1
1106
+ mma_tile_nm = (None, mma_tile_n, mma_tile_m)
1107
+ tBsP_nm_layout = sm100_utils.make_smem_layout_b(tiled_mma_vp, mma_tile_nm, mma_dtype, self.sp_stages)
1108
+ tBsP_nm = smem.allocate_tensor(mma_dtype, tBsP_nm_layout.outer, swizzle_align, tBsP_nm_layout.inner)
1109
+ tBsP_nk_tile = thrblk_mma_vp.partition_shape_B((mma_tile_n, mma_tile_k))
1110
+ tBsP_nk = cute.local_tile(tBsP_nm, tBsP_nk_tile, (0, 0, None, None))
1111
+ tCsP_tile = cute.make_ordered_layout(tCtS_shape, order=((2, 0), 3, 1, 4))
1112
+ tCsP = cute.composition(tBsP_nm, tCsP_tile)
1113
+
1114
+ # O tile view (shape source only — aliases Q smem, never written)
1115
+ sO_iterator = cute.recast_ptr(tBsQ.iterator, smem_layout_o.inner, dtype=acc_dtype)
1116
+ sO_mma = cute.make_tensor(sO_iterator, smem_layout_o.outer)
1117
+ tCsO = thrblk_mma_vp.partition_C(sO_mma)
1118
+ tCtO = thrblk_mma_vp.make_fragment_C(tCsO.shape)
1119
+
1120
+ # TMEM tensors
1121
+ tmem_ptr = cute.arch.retrieve_tmem_ptr(cutlass.Int32, 16, tmem_ptr_smem_ptr)
1122
+ tmem_offset = 0
1123
+
1124
+ if cutlass.const_expr(not FP8):
1125
+ tAtK_cvt = cute.make_tensor(
1126
+ cute.recast_ptr(tmem_ptr + tmem_offset, dtype=mma_dtype),
1127
+ tAtK_cvt.layout,
1128
+ )
1129
+ tAtV_cvt = cute.make_tensor(
1130
+ cute.recast_ptr(tmem_ptr + tmem_offset, dtype=mma_dtype),
1131
+ tAtV_cvt.layout,
1132
+ )
1133
+ tmem_offset += tcgen05.find_tmem_tensor_col_offset(tAtK_cvt)
1134
+
1135
+ tCtS = cute.make_tensor(cute.recast_ptr(tmem_ptr + tmem_offset, dtype=acc_dtype), tCtS.layout)
1136
+ tmem_offset += tcgen05.find_tmem_tensor_col_offset(tCtS)
1137
+
1138
+ tCtO = cute.make_tensor(cute.recast_ptr(tmem_ptr + tmem_offset, dtype=acc_dtype), tCtO.layout)
1139
+ tmem_offset += tcgen05.find_tmem_tensor_col_offset(tCtO)
1140
+
1141
+ _note(f"\t{tmem_offset} tmem cols used, {self.tmem_alloc_cols} tmem cols allocated")
1142
+ assert tmem_offset <= self.tmem_alloc_cols
1143
+
1144
+ ##############################
1145
+ # Exit early (this split has no tiles for this sequence)
1146
+ ##############################
1147
+ if exit_early:
1148
+ noop = None # early return not supported # noqa: F841
1149
+
1150
+ ##############################
1151
+ # TMA KV Dispatch
1152
+ ##############################
1153
+ elif warp_idx == self.tma_kv_warp_id:
1154
+ if cutlass.const_expr(self.use_reg_reconfig):
1155
+ cute.arch.setmaxregister_decrease(self.mma_tma_regs)
1156
+
1157
+ # KV is streamed exactly once: EVICT_FIRST keeps it from pushing
1158
+ # Q / block table / partials out of L2 (CUTLASS CacheHintSm90
1159
+ # EVICT_FIRST policy word).
1160
+ kv_hint = {}
1161
+ if cutlass.const_expr(self.kv_evict_first):
1162
+ kv_hint = {"cache_policy": cutlass.Int64(0x12F0000000000000)}
1163
+
1164
+ # Walk this CTA's contiguous chunk: step i loads K of tile
1165
+ # tile_begin + i and V of tile tile_begin + i - prefetch_iters
1166
+ # (K runs prefetch_iters tiles ahead of V, same order as the
1167
+ # consumers drain the ring).
1168
+ if cutlass.const_expr(NVFP4):
1169
+ # Packed e2m1 K / V tiles (token-major, whole head dim) into
1170
+ # the packed ring, one box per page; the convert warpgroup
1171
+ # turns them into e4m3 stages. Same K-ahead-of-V order.
1172
+ sKVp_box = cute.flat_divide(sKVp, (PS, blk_tile_d)) # (PS, D, pages_per_tile, 1, packed_stages)
1173
+ gKp_box = cute.flat_divide(mK, (PS, blk_tile_d)) # (PS, D, 1, 1, L)
1174
+ gVp_box = cute.flat_divide(mV, (PS, blk_tile_d))
1175
+ tGSsKp, tGSgKp = cute.nvgpu.cpasync.tma_partition(
1176
+ tma_atom_k,
1177
+ mcast_coord,
1178
+ mcast_layout,
1179
+ smem_tensor=cute.group_modes(sKVp_box, 0, 2),
1180
+ gmem_tensor=cute.group_modes(gKp_box, 0, 2),
1181
+ ) # (TMA, pages_per_tile, 1, stages), (TMA, 1, 1, L)
1182
+ tGSsVp, tGSgVp = cute.nvgpu.cpasync.tma_partition(
1183
+ tma_atom_v,
1184
+ mcast_coord,
1185
+ mcast_layout,
1186
+ smem_tensor=cute.group_modes(sKVp_box, 0, 2),
1187
+ gmem_tensor=cute.group_modes(gVp_box, 0, 2),
1188
+ )
1189
+ for s in cutlass.range(iters_s + prefetch_iters):
1190
+ if s < iters_s:
1191
+ t = tile_begin + s
1192
+ pk_handle = pk_producer.acquire_and_advance()
1193
+ for j in cutlass.range_constexpr(pages_per_tile):
1194
+ blk = t * pages_per_tile + j
1195
+ page = num_pages # past the end: OOB, zero-filled
1196
+ if blk * PS < seq_len:
1197
+ page = mBlockTable[b_idx, blk]
1198
+ l_idx = h_k_idx + h_k * page
1199
+ cute.copy(
1200
+ tma_atom_k,
1201
+ tGSgKp[None, 0, 0, l_idx],
1202
+ tGSsKp[None, j, 0, pk_handle.index],
1203
+ tma_bar_ptr=pk_handle.barrier,
1204
+ **kv_hint,
1205
+ )
1206
+ if s >= prefetch_iters:
1207
+ t = tile_begin + s - prefetch_iters
1208
+ pk_handle = pk_producer.acquire_and_advance()
1209
+ for j in cutlass.range_constexpr(pages_per_tile):
1210
+ blk = t * pages_per_tile + j
1211
+ page = num_pages
1212
+ if blk * PS < seq_len:
1213
+ page = mBlockTable[b_idx, blk]
1214
+ l_idx = h_k_idx + h_k * page
1215
+ cute.copy(
1216
+ tma_atom_v,
1217
+ tGSgVp[None, 0, 0, l_idx],
1218
+ tGSsVp[None, j, 0, pk_handle.index],
1219
+ tma_bar_ptr=pk_handle.barrier,
1220
+ **kv_hint,
1221
+ )
1222
+ elif cutlass.const_expr(PS == blk_tile_s):
1223
+ # One page per tile. L-mode = (h_k, num_pages).
1224
+ gK = cute.local_tile(mK, tiler=(blk_tile_s, blk_tile_d), coord=(0, 0, None)) # (TILE_S, TILE_D, L)
1225
+ gV = cute.local_tile(mV, tiler=(blk_tile_d, blk_tile_s), coord=(0, 0, None)) # (TILE_D, TILE_S, L)
1226
+ gK_mma = cute.flat_divide(
1227
+ gK, (mma_tile_m, mma_tile_k)
1228
+ ) # (MMA_TILE_M, MMA_TILE_K, #TILE_SM, #TILE_DK, L)
1229
+ gV_mma = cute.flat_divide(
1230
+ gV, (mma_tile_m, mma_tile_k)
1231
+ ) # (MMA_TILE_M, MMA_TILE_K, #TILE_DM, #TILE_SK, L)
1232
+ tAgK = thrblk_mma_kq.partition_A(gK_mma)
1233
+ tAgV = thrblk_mma_vp.partition_A(gV_mma)
1234
+ tGSsK, tGSgK = cute.nvgpu.cpasync.tma_partition(
1235
+ tma_atom_k,
1236
+ mcast_coord,
1237
+ mcast_layout,
1238
+ smem_tensor=cute.group_modes(tAsK, 0, 3),
1239
+ gmem_tensor=cute.group_modes(tAgK, 0, 3),
1240
+ ) # (TMA, stages), (TMA, #TILE_SM, #TILE_DK, L)
1241
+ tGSsV, tGSgV = cute.nvgpu.cpasync.tma_partition(
1242
+ tma_atom_v,
1243
+ mcast_coord,
1244
+ mcast_layout,
1245
+ smem_tensor=cute.group_modes(tAsV, 0, 3),
1246
+ gmem_tensor=cute.group_modes(tAgV, 0, 3),
1247
+ ) # (TMA, stages), (TMA, #TILE_DM, #TILE_SK, L)
1248
+
1249
+ for s in cutlass.range(iters_s + prefetch_iters):
1250
+ # L2 prefetch: K+V of tile s + L2P (and, on the first step,
1251
+ # s + 1 .. s + L2P - 1) so DRAM streams ahead of the ring.
1252
+ if cutlass.const_expr(L2P > 0):
1253
+ for pp in cutlass.range_constexpr(1, L2P + 1):
1254
+ if s + pp < iters_s:
1255
+ if cutlass.const_expr(pp == L2P):
1256
+ page_p = mBlockTable[b_idx, tile_begin + s + pp]
1257
+ l_p = h_k_idx + h_k * page_p
1258
+ for dk in cutlass.range_constexpr(tiles_dk):
1259
+ cute.prefetch(tma_atom_k, tGSgK[None, 0, dk, l_p])
1260
+ for dm in cutlass.range_constexpr(tiles_dm):
1261
+ cute.prefetch(tma_atom_v, tGSgV[None, dm, 0, l_p])
1262
+ else:
1263
+ if s == 0:
1264
+ page_p = mBlockTable[b_idx, tile_begin + pp]
1265
+ l_p = h_k_idx + h_k * page_p
1266
+ for dk in cutlass.range_constexpr(tiles_dk):
1267
+ cute.prefetch(tma_atom_k, tGSgK[None, 0, dk, l_p])
1268
+ for dm in cutlass.range_constexpr(tiles_dm):
1269
+ cute.prefetch(tma_atom_v, tGSgV[None, dm, 0, l_p])
1270
+ # Load K (tile tile_begin + s)
1271
+ if s < iters_s:
1272
+ page = mBlockTable[b_idx, tile_begin + s]
1273
+ l_idx = h_k_idx + h_k * page
1274
+ for dk in cutlass.range_constexpr(tiles_dk):
1275
+ k_handle = kv_producer.acquire_and_advance()
1276
+ cute.copy(
1277
+ tma_atom_k,
1278
+ tGSgK[None, 0, dk, l_idx],
1279
+ tGSsK[None, k_handle.index],
1280
+ tma_bar_ptr=k_handle.barrier,
1281
+ **kv_hint,
1282
+ )
1283
+ # Load V (tile tile_begin + s - prefetch_iters)
1284
+ if s >= prefetch_iters:
1285
+ page = mBlockTable[b_idx, tile_begin + s - prefetch_iters]
1286
+ l_idx = h_k_idx + h_k * page
1287
+ for dm in cutlass.range_constexpr(tiles_dm):
1288
+ v_handle = kv_producer.acquire_and_advance()
1289
+ cute.copy(
1290
+ tma_atom_v,
1291
+ tGSgV[None, dm, 0, l_idx],
1292
+ tGSsV[None, v_handle.index],
1293
+ tma_bar_ptr=v_handle.barrier,
1294
+ **kv_hint,
1295
+ )
1296
+ else:
1297
+ # PS < 128: pages_per_tile boxes per stage. Logical views of
1298
+ # the ring: K (s, d, stage), V (d, s, stage).
1299
+ ring_2d = cute.make_layout(
1300
+ (mma_tile_m, mma_tile_k, self.kv_stages),
1301
+ stride=(1, mma_tile_m, mma_tile_m * mma_tile_k),
1302
+ )
1303
+ sK_sd = cute.composition(tAsK, ring_2d)
1304
+ sV_ds = cute.composition(tAsV, ring_2d)
1305
+ sK_box = cute.flat_divide(sK_sd, (PS, mma_tile_k)) # (PS, K, #J, 1, stages)
1306
+ sV_box = cute.flat_divide(sV_ds, (mma_tile_m, PS)) # (M, PS, 1, #J, stages)
1307
+ gK_box = cute.flat_divide(mK, (PS, mma_tile_k)) # (PS, K, 1, #DK, L)
1308
+ gV_box = cute.flat_divide(mV, (mma_tile_m, PS)) # (M, PS, #DM, 1, L)
1309
+ tGSsK, tGSgK = cute.nvgpu.cpasync.tma_partition(
1310
+ tma_atom_k,
1311
+ mcast_coord,
1312
+ mcast_layout,
1313
+ smem_tensor=cute.group_modes(sK_box, 0, 2),
1314
+ gmem_tensor=cute.group_modes(gK_box, 0, 2),
1315
+ ) # (TMA, #J, 1, stages), (TMA, 1, #DK, L)
1316
+ tGSsV, tGSgV = cute.nvgpu.cpasync.tma_partition(
1317
+ tma_atom_v,
1318
+ mcast_coord,
1319
+ mcast_layout,
1320
+ smem_tensor=cute.group_modes(sV_box, 0, 2),
1321
+ gmem_tensor=cute.group_modes(gV_box, 0, 2),
1322
+ ) # (TMA, 1, #J, stages), (TMA, #DM, 1, L)
1323
+
1324
+ for s in cutlass.range(iters_s + prefetch_iters):
1325
+ # L2 prefetch (see the PS == 128 path); pages past the
1326
+ # sequence end are skipped, not zero-filled.
1327
+ if cutlass.const_expr(L2P > 0):
1328
+ for pp in cutlass.range_constexpr(1, L2P + 1):
1329
+ if s + pp < iters_s:
1330
+ do_p = s == 0
1331
+ if cutlass.const_expr(pp == L2P):
1332
+ do_p = s >= 0
1333
+ if do_p:
1334
+ t_p = tile_begin + s + pp
1335
+ for j in cutlass.range_constexpr(pages_per_tile):
1336
+ blk_p = t_p * pages_per_tile + j
1337
+ if blk_p * PS < seq_len:
1338
+ page_p = mBlockTable[b_idx, blk_p]
1339
+ l_p = h_k_idx + h_k * page_p
1340
+ for dk in cutlass.range_constexpr(tiles_dk):
1341
+ cute.prefetch(tma_atom_k, tGSgK[None, 0, dk, l_p])
1342
+ for dm in cutlass.range_constexpr(tiles_dm):
1343
+ cute.prefetch(tma_atom_v, tGSgV[None, dm, 0, l_p])
1344
+ # Load K (tile tile_begin + s)
1345
+ if s < iters_s:
1346
+ t = tile_begin + s
1347
+ for dk in cutlass.range_constexpr(tiles_dk):
1348
+ k_handle = kv_producer.acquire_and_advance()
1349
+ for j in cutlass.range_constexpr(pages_per_tile):
1350
+ # Pages past the sequence end get an
1351
+ # out-of-bounds page index: TMA zero-fills
1352
+ # them and never touches the block table
1353
+ # padding (which may hold garbage).
1354
+ blk = t * pages_per_tile + j
1355
+ page = num_pages
1356
+ if blk * PS < seq_len:
1357
+ page = mBlockTable[b_idx, blk]
1358
+ l_idx = h_k_idx + h_k * page
1359
+ cute.copy(
1360
+ tma_atom_k,
1361
+ tGSgK[None, 0, dk, l_idx],
1362
+ tGSsK[None, j, 0, k_handle.index],
1363
+ tma_bar_ptr=k_handle.barrier,
1364
+ **kv_hint,
1365
+ )
1366
+ # Load V (tile tile_begin + s - prefetch_iters)
1367
+ if s >= prefetch_iters:
1368
+ t = tile_begin + s - prefetch_iters
1369
+ for dm in cutlass.range_constexpr(tiles_dm):
1370
+ v_handle = kv_producer.acquire_and_advance()
1371
+ for j in cutlass.range_constexpr(pages_per_tile):
1372
+ blk = t * pages_per_tile + j
1373
+ page = num_pages
1374
+ if blk * PS < seq_len:
1375
+ page = mBlockTable[b_idx, blk]
1376
+ l_idx = h_k_idx + h_k * page
1377
+ cute.copy(
1378
+ tma_atom_v,
1379
+ tGSgV[None, dm, 0, l_idx],
1380
+ tGSsV[None, 0, j, v_handle.index],
1381
+ tma_bar_ptr=v_handle.barrier,
1382
+ **kv_hint,
1383
+ )
1384
+
1385
+ ##############################
1386
+ # TMA Q Dispatch
1387
+ ##############################
1388
+ elif warp_idx == self.tma_q_warp_id:
1389
+ if cutlass.const_expr(self.use_reg_reconfig):
1390
+ cute.arch.setmaxregister_decrease(self.mma_tma_regs)
1391
+
1392
+ gQ = cute.local_tile(mQ, tiler=(blk_tile_h, blk_tile_d), coord=(coord_hr, 0, coord_hb)) # (TILE_H, TILE_D)
1393
+ gQ_mma = cute.flat_divide(gQ, (mma_tile_n, mma_tile_k)) # (MMA_TILE_N, MMA_TILE_K, #TILE_HN, #TILE_DK)
1394
+ tBgQ = thrblk_mma_kq.partition_B(gQ_mma)
1395
+ tGSsQ, tGSgQ = cute.nvgpu.cpasync.tma_partition(
1396
+ tma_atom_q,
1397
+ mcast_coord,
1398
+ mcast_layout,
1399
+ smem_tensor=cute.group_modes(tBsQ, 0, 3),
1400
+ gmem_tensor=cute.group_modes(tBgQ, 0, 3),
1401
+ )
1402
+ for dk in cutlass.range_constexpr(tiles_dk):
1403
+ q_handle = q_producer.acquire_and_advance()
1404
+ cute.copy(
1405
+ tma_atom_q,
1406
+ tGSgQ[None, 0, dk], # q_stages == tiles_dk
1407
+ tGSsQ[None, dk],
1408
+ tma_bar_ptr=q_handle.barrier,
1409
+ )
1410
+
1411
+ ##############################
1412
+ # Convert Dispatch (bf16 only; empty range on the fp8 launch)
1413
+ ##############################
1414
+ elif warpgroup_idx >= self.cvt_wg_begin and warpgroup_idx < self.cvt_wg_end:
1415
+ if cutlass.const_expr(not FP8):
1416
+ if cutlass.const_expr(self.use_reg_reconfig):
1417
+ cute.arch.setmaxregister_decrease(self.cvt_regs)
1418
+
1419
+ cvt_type = cutlass.Float32 # e4m3 -> f32 -> bf16
1420
+
1421
+ # Multiple warpgroups stride over the KV / cvt stages
1422
+ convert_phase = 0
1423
+ if cutlass.const_expr(self.convert_warpgroups > 1):
1424
+ assert tiles_dk % self.convert_warpgroups == 0
1425
+ assert (tiles_dm * tiles_sk) % self.convert_warpgroups == 0
1426
+ convert_phase = (warpgroup_idx - self.cvt_wg_begin) % self.convert_warpgroups
1427
+ for _ in cutlass.range(convert_phase):
1428
+ kv_consumer.advance()
1429
+ cvt_producer.advance()
1430
+
1431
+ # K: smem (ldmatrix) -> rf -> cvt -> TMEM
1432
+ mma_k_bits = mma_tile_k * mma_dtype.width
1433
+ tmem_store_atom_k = cute.make_copy_atom(
1434
+ tcgen05.St16x256bOp(tcgen05.Repetition(mma_k_bits // 256)),
1435
+ mma_dtype,
1436
+ )
1437
+ smem_load_atom_k = cute.make_copy_atom(
1438
+ cute.nvgpu.warp.LdMatrix8x16x8bOp(num_matrices=4),
1439
+ kv_smem_dtype,
1440
+ )
1441
+ tmem_store_k = tcgen05.make_tmem_copy(tmem_store_atom_k, tAtK_cvt[mma_dice + (0,)])
1442
+ thr_store_k = tmem_store_k.get_slice(warpgroup_tidx)
1443
+ tKrK_cvt_shape = thr_store_k.partition_S(tAtK_cvt).shape[:-1]
1444
+ tKtK_cvt = thr_store_k.partition_D(tAtK_cvt)
1445
+ smem_load_k = cute.make_tiled_copy_S(smem_load_atom_k, tmem_store_k)
1446
+ thr_load_k = smem_load_k.get_slice(warpgroup_tidx)
1447
+ tKsK = thr_load_k.partition_S(tAsK)
1448
+ tKrK_shape = thr_load_k.partition_D(tAsK).shape[:-1]
1449
+
1450
+ # V
1451
+ tmem_store_atom_v = cute.make_copy_atom(
1452
+ tcgen05.St16x256bOp(tcgen05.Repetition(mma_k_bits // 256)),
1453
+ mma_dtype,
1454
+ )
1455
+ smem_load_atom_v = cute.make_copy_atom(
1456
+ cute.nvgpu.warp.LdMatrix16x16x8bOp(transpose=True, num_matrices=2),
1457
+ kv_smem_dtype,
1458
+ )
1459
+ tmem_store_v = tcgen05.make_tmem_copy(tmem_store_atom_v, tAtV_cvt[mma_dice + (0,)])
1460
+ thr_store_v = tmem_store_v.get_slice(warpgroup_tidx)
1461
+ tVrV_cvt_shape = thr_store_v.partition_S(tAtV_cvt).shape[:-1]
1462
+ tVtV_cvt = thr_store_v.partition_D(tAtV_cvt)
1463
+ smem_load_v = cute.make_tiled_copy_S(smem_load_atom_v, tmem_store_v)
1464
+ thr_load_v = smem_load_v.get_slice(warpgroup_tidx)
1465
+ tVsV = thr_load_v.partition_S(tAsV)
1466
+ tVrV_shape = thr_load_v.partition_D(tAsV).shape[:-1]
1467
+
1468
+ for s in cutlass.range(prefetch_iters + iters_s):
1469
+ if s < iters_s:
1470
+ for dk in cutlass.range(tiles_dk // self.convert_warpgroups, unroll=2):
1471
+ tKrK = cute.make_rmem_tensor(tKrK_shape, kv_smem_dtype)
1472
+ tKrK_cvt = cute.make_rmem_tensor(tKrK_cvt_shape, mma_dtype)
1473
+
1474
+ kv_handle = kv_consumer.wait_and_advance()
1475
+ cute.copy(thr_load_k, tKsK[cpy_dice + (kv_handle.index,)], tKrK)
1476
+ cute.arch.fence_view_async_shared()
1477
+ kv_handle.release()
1478
+
1479
+ tKrK_ssa = tKrK.load().to(cvt_type).to(mma_dtype)
1480
+ tKrK_cvt.store(tKrK_ssa.reshape(tKrK_cvt_shape))
1481
+
1482
+ cvt_handle = cvt_producer.acquire_and_advance()
1483
+ cute.copy(
1484
+ thr_store_k,
1485
+ tKrK_cvt,
1486
+ tKtK_cvt[cpy_dice + (cvt_handle.index,)],
1487
+ )
1488
+ cute.arch.fence_view_async_tmem_store()
1489
+ cvt_handle.commit()
1490
+
1491
+ for _ in cutlass.range_constexpr(self.convert_warpgroups - 1):
1492
+ kv_consumer.advance()
1493
+ cvt_producer.advance()
1494
+
1495
+ if s >= prefetch_iters:
1496
+ for dmsk in cutlass.range(tiles_dm * tiles_sk // self.convert_warpgroups, unroll=2):
1497
+ tVrV = cute.make_rmem_tensor(tVrV_shape, kv_smem_dtype)
1498
+ tVrV_cvt = cute.make_rmem_tensor(tVrV_cvt_shape, mma_dtype)
1499
+
1500
+ kv_handle = kv_consumer.wait_and_advance()
1501
+ cute.copy(thr_load_v, tVsV[cpy_dice + (kv_handle.index,)], tVrV)
1502
+ cute.arch.fence_view_async_shared()
1503
+ kv_handle.release()
1504
+
1505
+ tVrV_ssa = tVrV.load().to(cvt_type).to(mma_dtype)
1506
+ tVrV_cvt.store(tVrV_ssa.reshape(tVrV_cvt_shape))
1507
+
1508
+ cvt_handle = cvt_producer.acquire_and_advance()
1509
+ cute.copy(
1510
+ thr_store_v,
1511
+ tVrV_cvt,
1512
+ tVtV_cvt[cpy_dice + (cvt_handle.index,)],
1513
+ )
1514
+ cute.arch.fence_view_async_tmem_store()
1515
+ cvt_handle.commit()
1516
+
1517
+ for _ in cutlass.range_constexpr(self.convert_warpgroups - 1):
1518
+ kv_consumer.advance()
1519
+ cvt_producer.advance()
1520
+
1521
+ if cutlass.const_expr(NVFP4):
1522
+ # nvfp4 -> e4m3: packed (token, d) tile in SMEM, e4m3 scale
1523
+ # per 16 d elements from gmem; writes the e4m3 A-operand
1524
+ # stages the fp8 MMA reads (K: (s, d) K-major, V: (d, s)
1525
+ # MN-major — both addressed through their logical views).
1526
+ # Thread t owns d-chunk c = t % 8 (32 elements = 2 scale
1527
+ # groups) of rows t // 8 + 16 * i: a warp reads 8 consecutive
1528
+ # 16-B chunks of 4 rows (conflict-free) and the chunk lands
1529
+ # in one half-head-dim e4m3 stage (c // 4).
1530
+ if cutlass.const_expr(self.use_reg_reconfig):
1531
+ cute.arch.setmaxregister_decrease(self.cvt_regs)
1532
+ CH = 32 # elements per chunk
1533
+ n_chunks = blk_tile_d // CH # 8 at d = 256
1534
+ rows_per_pass = warpgroup_threads // n_chunks # 16
1535
+ cvt_c = warpgroup_tidx % n_chunks
1536
+ cvt_r0 = warpgroup_tidx // n_chunks
1537
+ half = cvt_c // (mma_tile_k // CH) # which 128-d e4m3 stage
1538
+ cvt_cc = cvt_c % (mma_tile_k // CH) # chunk inside that stage
1539
+ ring_2d = cute.make_layout(
1540
+ (mma_tile_m, mma_tile_k, self.kv_stages),
1541
+ stride=(1, mma_tile_m, mma_tile_m * mma_tile_k),
1542
+ )
1543
+ sK_sd = cute.composition(tAsK, ring_2d) # (s, d, stage)
1544
+ sV_ds = cute.composition(tAsV, ring_2d) # (d, s, stage)
1545
+ shift = cutlass.Float32(NVFP4_SHIFT)
1546
+
1547
+ for s in cutlass.range(prefetch_iters + iters_s):
1548
+ for kv in cutlass.range_constexpr(2): # 0: K tile, 1: V tile
1549
+ do_tile = s < iters_s
1550
+ t = tile_begin + s
1551
+ if cutlass.const_expr(kv == 1):
1552
+ do_tile = s >= prefetch_iters
1553
+ t = tile_begin + s - prefetch_iters
1554
+ if do_tile:
1555
+ pk_handle = pk_consumer.wait_and_advance()
1556
+ h0 = kv_producer.acquire_and_advance()
1557
+ h1 = kv_producer.acquire_and_advance()
1558
+ dst_stage = h0.index
1559
+ if half == 1:
1560
+ dst_stage = h1.index
1561
+ for i in cutlass.range_constexpr(blk_tile_s // rows_per_pass):
1562
+ r = cvt_r0 + rows_per_pass * i
1563
+ # scale page: rows past the sequence end were
1564
+ # zero-filled by TMA (code 0 -> 0); read page 0
1565
+ blk = t * pages_per_tile + r // PS
1566
+ page_s = 0
1567
+ if blk * PS < seq_len:
1568
+ page_s = mBlockTable[b_idx, blk]
1569
+ l_s = h_k_idx + h_k * page_s
1570
+ rsc = cute.make_rmem_tensor((2,), cutlass.Float8E4M3FN)
1571
+ if cutlass.const_expr(kv == 0):
1572
+ gsc = cute.local_tile(mKS[r % PS, None, l_s], (2,), (cvt_c,))
1573
+ else:
1574
+ gsc = cute.local_tile(mVS[r % PS, None, l_s], (2,), (cvt_c,))
1575
+ cute.autovec_copy(gsc, rsc)
1576
+ rscf = cute.make_rmem_tensor((2,), cutlass.Float32)
1577
+ rscf.store(rsc.load().to(cutlass.Float32))
1578
+ sc0 = rscf[0] * shift
1579
+ sc1 = rscf[1] * shift
1580
+ # 32 packed codes -> f32 -> * scale -> e4m3
1581
+ src = cute.local_tile(sKVp[r, None, pk_handle.index], (CH,), (cvt_c,))
1582
+ rc = cute.make_rmem_tensor((CH,), cutlass.Float4E2M1FN)
1583
+ cute.autovec_copy(src, rc)
1584
+ rf = cute.make_rmem_tensor((CH,), cutlass.Float32)
1585
+ rf.store(rc.load().to(cutlass.Float32))
1586
+ for j in cutlass.range_constexpr(CH):
1587
+ if cutlass.const_expr(j < NVFP4_GROUP):
1588
+ rf[j] = rf[j] * sc0
1589
+ else:
1590
+ rf[j] = rf[j] * sc1
1591
+ r8 = cute.make_rmem_tensor((CH,), mma_dtype)
1592
+ r8.store(rf.load().to(mma_dtype))
1593
+ if cutlass.const_expr(kv == 0):
1594
+ dst = cute.local_tile(sK_sd[r, None, dst_stage], (CH,), (cvt_cc,))
1595
+ else:
1596
+ dst = cute.local_tile(sV_ds[None, r, dst_stage], (CH,), (cvt_cc,))
1597
+ cute.autovec_copy(r8, dst)
1598
+ # generic-proxy SMEM writes -> visible to tcgen05
1599
+ cute.arch.fence_view_async_shared()
1600
+ pk_handle.release()
1601
+ h0.commit()
1602
+ h1.commit()
1603
+
1604
+ ##############################
1605
+ # MMA KQ Dispatch
1606
+ ##############################
1607
+ elif warp_idx == self.mma_kq_warp_id:
1608
+ if cutlass.const_expr(self.use_reg_reconfig):
1609
+ cute.arch.setmaxregister_decrease(self.mma_tma_regs)
1610
+
1611
+ tBsQ_desc = thrblk_mma_kq.make_fragment_B(tBsQ)
1612
+
1613
+ # Wait for Q
1614
+ for dk in cutlass.range_constexpr(tiles_dk):
1615
+ q_consumer.wait_and_advance()
1616
+
1617
+ s_token = True # Producer always acquires first
1618
+ for s in cutlass.range(iters_s):
1619
+ # BMM1: A-operand K (SMEM ring stage for fp8, TMEM for bf16);
1620
+ # a_consumer.release() is a tcgen05.commit in both cases.
1621
+ k_token = a_consumer.try_wait()
1622
+ tiled_mma_kq.set(tcgen05.Field.ACCUMULATE, False)
1623
+ s_handle = s_producer.acquire_and_advance(s_token)
1624
+ for dk in cutlass.range_constexpr(tiles_dk):
1625
+ is_last_iter = dk == tiles_dk - 1
1626
+ k_handle = a_consumer.wait_and_advance(k_token)
1627
+ if is_last_iter:
1628
+ mma_kq_nbar.arrive() # signal BMM2 to start
1629
+ # shape[2] = UMMA instructions per stage along K
1630
+ for mma_k in cutlass.range_constexpr(tAtK_cvt.shape[2]):
1631
+ cute.gemm(
1632
+ tiled_mma_kq,
1633
+ tCtS[mma_dice + (s_handle.index,)],
1634
+ tAtK_cvt[None, None, mma_k, k_handle.index],
1635
+ tBsQ_desc[None, None, mma_k, dk],
1636
+ tCtS[mma_dice + (s_handle.index,)],
1637
+ )
1638
+ if dk == 0 and mma_k == 0:
1639
+ tiled_mma_kq.set(tcgen05.Field.ACCUMULATE, True)
1640
+ k_handle.release()
1641
+ if not is_last_iter:
1642
+ k_token = a_consumer.try_wait()
1643
+ s_handle.commit()
1644
+
1645
+ # Skip the V stages (mma_vp's) and wait for BMM2
1646
+ if s > 0:
1647
+ for _ in cutlass.range_constexpr(tiles_dm * tiles_sk):
1648
+ a_consumer.advance()
1649
+ mma_vp_nbar.arrive_and_wait()
1650
+ s_token = s_producer.try_acquire()
1651
+
1652
+ ##############################
1653
+ # MMA VP Dispatch
1654
+ ##############################
1655
+ elif warp_idx == self.mma_vp_warp_id:
1656
+ if cutlass.const_expr(self.use_reg_reconfig):
1657
+ cute.arch.setmaxregister_decrease(self.mma_tma_regs)
1658
+
1659
+ tiled_mma_vp.set(tcgen05.Field.ACCUMULATE, True)
1660
+ tBsP_desc = thrblk_mma_vp.make_fragment_B(tBsP_nk)
1661
+
1662
+ # Skip the first K stages (mma_kq's) and wait for BMM1
1663
+ for _ in cutlass.range_constexpr(tiles_dk):
1664
+ a_consumer.advance()
1665
+ mma_kq_nbar.arrive_and_wait()
1666
+
1667
+ p_token = False
1668
+ o_token = True # Producer always acquires first
1669
+ for s in cutlass.range(iters_s):
1670
+ if s < iters_s - 1:
1671
+ for _ in cutlass.range_constexpr(tiles_dk):
1672
+ a_consumer.advance()
1673
+ mma_kq_nbar.arrive_and_wait()
1674
+ p_token = p_consumer.try_wait()
1675
+
1676
+ # BMM2: A-operand V
1677
+ v_token = a_consumer.try_wait()
1678
+ p_handle = p_consumer.wait_and_advance(p_token)
1679
+ o_handle = o_producer.acquire_and_advance(o_token)
1680
+ for sk in cutlass.range_constexpr(tiles_sk):
1681
+ for dm in cutlass.range_constexpr(tiles_dm):
1682
+ is_last_iter = sk == tiles_sk - 1 and dm == tiles_dm - 1
1683
+ v_handle = a_consumer.wait_and_advance(v_token)
1684
+ if is_last_iter:
1685
+ mma_vp_nbar.arrive() # signal BMM1 to start
1686
+ for mma_k in cutlass.range_constexpr(tAtV_cvt.shape[2]):
1687
+ cute.gemm(
1688
+ tiled_mma_vp,
1689
+ tCtO[mma_dice + (dm, 0)],
1690
+ tAtV_cvt[None, None, mma_k, v_handle.index],
1691
+ tBsP_desc[None, None, mma_k, sk, p_handle.index],
1692
+ tCtO[mma_dice + (dm, 0)],
1693
+ )
1694
+ v_handle.release()
1695
+ if not is_last_iter:
1696
+ v_token = a_consumer.try_wait()
1697
+ p_handle.release()
1698
+ o_handle.commit()
1699
+ o_token = o_producer.try_acquire()
1700
+
1701
+ # Wait for the softmax's final O read, then dealloc TMEM
1702
+ o_producer.tail()
1703
+ cute.arch.relinquish_tmem_alloc_permit()
1704
+ cute.arch.dealloc_tmem(tmem_ptr, self.tmem_alloc_cols)
1705
+
1706
+ ##############################
1707
+ # Softmax + Correction + Epilogue Dispatch
1708
+ ##############################
1709
+ elif warpgroup_idx == self.softmax_warpgroup_id:
1710
+ if cutlass.const_expr(self.use_reg_reconfig):
1711
+ cute.arch.setmaxregister_increase(self.softmax_regs)
1712
+
1713
+ # Per-(seq, kv head) descales. q/k descale fold into the softmax
1714
+ # exponent (S is only ever max'ed and exponentiated), v descale
1715
+ # into the output scale.
1716
+ qk_descale = mQD[coord_hb] * mKD[coord_hb]
1717
+ cta_c = scale_qs_log2_e * qk_descale # exponent base-2 scale
1718
+ cta_scale = scale_qs * qk_descale # natural-log LSE scale
1719
+ v_descale = mVD[coord_hb]
1720
+
1721
+ # Tiled copies
1722
+ tmem_op_width = 32
1723
+ tmem_op_repeat = tcgen05.Repetition(mma_tile_n * acc_dtype.width // tmem_op_width)
1724
+ tmem_load_atom_s = cute.make_copy_atom(tcgen05.Ld32x32bOp(tmem_op_repeat), acc_dtype)
1725
+ tmem_load_s = tcgen05.make_tmem_copy(tmem_load_atom_s, tCtS[mma_dice + (0,)])
1726
+ thr_load_s = tmem_load_s.get_slice(warpgroup_tidx)
1727
+
1728
+ tmem_store_atom_o = cute.make_copy_atom(tcgen05.St32x32bOp(tmem_op_repeat), acc_dtype)
1729
+ tmem_store_o = tcgen05.make_tmem_copy(tmem_store_atom_o, tCtO[mma_dice + (0, 0)])
1730
+ thr_store_o = tmem_store_o.get_slice(warpgroup_tidx)
1731
+
1732
+ # Partition S and P. Thread t holds TMEM lane t == S row t of the
1733
+ # tile; element i of its fragment is column (query row) i.
1734
+ tStS = thr_load_s.partition_S(tCtS)
1735
+ tSsP = thr_load_s.partition_D(tCsP)
1736
+
1737
+ # Partition O
1738
+ tStO = thr_load_s.partition_S(tCtO)
1739
+ tSsO = thr_load_s.partition_D(tCsO)
1740
+ tSrO = cute.make_rmem_tensor_like(tSsO)
1741
+
1742
+ # colmax (running, raw S units)
1743
+ tSsM = thr_load_s.partition_D(tCsM)
1744
+ tSrM_prev = cute.make_rmem_tensor_like(tSsM)
1745
+ tSrM_prev.fill(-cutlass.Float32.inf)
1746
+
1747
+ # colsum (per thread, reduced after the loop)
1748
+ tSsL = thr_load_s.partition_D(tCsL)
1749
+ tSrL = cute.make_rmem_tensor_like(tSsL[cpy_dice + (0,)])
1750
+ tSrL.fill(cutlass.Float32(0))
1751
+
1752
+ assert warp_threads >= cute.size(tSsM)
1753
+
1754
+ # Causal tail for q_len > 1: column n (= qi * h_r + hr) may see
1755
+ # keys 0 .. seq_len - q_len + qi. Non-causal: every column sees
1756
+ # keys 0 .. seq_len - 1 (the q_len == 1 mask below).
1757
+ if cutlass.const_expr(self.q_len > 1 and self.causal):
1758
+ h_r_true = n_q // self.q_len
1759
+ col_lim = cute.make_fragment_like(tSrM_prev, cutlass.Int32)
1760
+ for i in cutlass.range_constexpr(cute.size(col_lim)):
1761
+ col_lim[i] = seq_len - self.q_len + (n_base + i) // h_r_true
1762
+
1763
+ # Initialize O
1764
+ tSrO.fill(cutlass.Float32(0))
1765
+ cute.copy(thr_store_o, tSrO, tStO)
1766
+
1767
+ # Initialize colsum and colmax in smem and wait
1768
+ if warpgroup_widx == 0 and lane_store_max:
1769
+ tSsM[lane_idx] = -cutlass.Float32.inf
1770
+ if warpgroup_widx == 1 and lane_store_max:
1771
+ tSsL[lane_idx] = cutlass.Float32(0)
1772
+ softmax_nbar.arrive_and_wait()
1773
+
1774
+ #
1775
+ # Sequence loop
1776
+ #
1777
+ for s in cutlass.range(iters_s):
1778
+ tile_idx = tile_begin + s
1779
+ row = tile_idx * blk_tile_s + warpgroup_tidx
1780
+
1781
+ # Load S from tmem
1782
+ s_handle = s_consumer.wait_and_advance()
1783
+ tSrS = cute.make_rmem_tensor(tSsP.shape[:-1], acc_dtype)
1784
+ cute.copy(tmem_load_s, tStS[cpy_dice + (s_handle.index,)], tSrS)
1785
+ cute.arch.fence_view_async_tmem_load()
1786
+ s_handle.release()
1787
+
1788
+ # Mask keys past the sequence end (select, so garbage/zero
1789
+ # K rows from padding or OOB pages can't leak in)
1790
+ if cutlass.const_expr(self.q_len == 1 or not self.causal):
1791
+ if row >= seq_len:
1792
+ tSrS.fill(-cutlass.Float32.inf)
1793
+ else:
1794
+ for i in cutlass.range_constexpr(cute.size(tSrS)):
1795
+ if row > col_lim[i]:
1796
+ tSrS[i] = -cutlass.Float32.inf
1797
+
1798
+ # colmax in warp RF
1799
+ tSrM = cute.make_rmem_tensor_like(tSsM)
1800
+ tSrM_lane = cutlass.Float32(0) # Avoid dynamic register indexing
1801
+ for i in cutlass.range_constexpr(cute.size(tSrS)):
1802
+ tSrM[i] = warp_fmax(tSrS[i])
1803
+ if i == lane_idx:
1804
+ tSrM_lane = tSrM[i]
1805
+
1806
+ # colmax in smem (running across this CTA's tiles)
1807
+ if lane_store_max:
1808
+ smem_fmax(tSsM.iterator + tSsM.layout(lane_idx), tSrM_lane)
1809
+
1810
+ softmax_nbar.arrive_and_wait()
1811
+ cute.autovec_copy(tSsM, tSrM)
1812
+
1813
+ # A column with no visible key so far has max -inf: use 0 as
1814
+ # the exponent reference so p = exp2(-inf) = 0 and the
1815
+ # correction exp2(-inf) = 0 instead of NaN from (-inf) - (-inf).
1816
+ m_use = cute.make_rmem_tensor_like(tSrM)
1817
+ for i in cutlass.range_constexpr(cute.size(tSrM)):
1818
+ m_use[i] = tSrM[i]
1819
+ if tSrM[i] == -cutlass.Float32.inf:
1820
+ m_use[i] = cutlass.Float32(0.0)
1821
+
1822
+ # Online softmax
1823
+ tSrP = cute.make_rmem_tensor(tSsP.shape[:-1], mma_dtype)
1824
+ tSrP_f32 = cute.make_rmem_tensor(tSrS.shape, acc_dtype)
1825
+ for i in cutlass.range_constexpr(0, cute.size(tSrS), 2):
1826
+ p_f32x2 = fadd2((tSrS[i], tSrS[i + 1]), (-m_use[i], -m_use[i + 1]))
1827
+ p_f32x2 = fmul2(p_f32x2, (cta_c, cta_c))
1828
+ tSrP_f32[i] = exp2(p_f32x2[0])
1829
+ tSrP_f32[i + 1] = exp2(p_f32x2[1])
1830
+ if cutlass.const_expr(FP8):
1831
+ p_scale_f32 = cutlass.Float32(p_scale)
1832
+ tSrPq_f32 = cute.make_rmem_tensor(tSrS.shape, acc_dtype)
1833
+ for i in cutlass.range_constexpr(0, cute.size(tSrS), 2):
1834
+ pq_f32x2 = fmul2((tSrP_f32[i], tSrP_f32[i + 1]), (p_scale_f32, p_scale_f32))
1835
+ tSrPq_f32[i] = pq_f32x2[0]
1836
+ tSrPq_f32[i + 1] = pq_f32x2[1]
1837
+ tSrP.store(tSrPq_f32.load().to(mma_dtype))
1838
+ else:
1839
+ tSrP.store(tSrP_f32.load().to(mma_dtype))
1840
+
1841
+ # Store P to smem (generic writes -> async-proxy UMMA read)
1842
+ p_handle = p_producer.acquire_and_advance()
1843
+ cute.autovec_copy(tSrP, tSsP[cpy_dice + (p_handle.index,)])
1844
+ cute.arch.fence_view_async_shared()
1845
+ p_handle.commit()
1846
+
1847
+ # Correction factor and colsum (unscaled p)
1848
+ correction = cute.make_rmem_tensor_like(tSrM)
1849
+ for i in cutlass.range_constexpr(0, cute.size(tSrM), 2):
1850
+ c_f32x2 = fadd2((tSrM_prev[i], tSrM_prev[i + 1]), (-m_use[i], -m_use[i + 1]))
1851
+ c_f32x2 = fmul2(c_f32x2, (cta_c, cta_c))
1852
+ c_f32x2 = (exp2(c_f32x2[0]), exp2(c_f32x2[1]))
1853
+ correction[i] = c_f32x2[0]
1854
+ correction[i + 1] = c_f32x2[1]
1855
+ l_f32x2 = ffma2(
1856
+ c_f32x2,
1857
+ (tSrL[i], tSrL[i + 1]),
1858
+ (tSrP_f32[i], tSrP_f32[i + 1]),
1859
+ )
1860
+ tSrL[i] = l_f32x2[0]
1861
+ tSrL[i + 1] = l_f32x2[1]
1862
+
1863
+ # Correct O — only when some column max actually moved. In
1864
+ # decode the running max settles after a few tiles, so most
1865
+ # iterations skip the full O TMEM load/scale/store round trip
1866
+ # that otherwise sits on the critical path before GEMM2(s).
1867
+ # tSrM / tSrM_prev come from the shared colmax, so the test is
1868
+ # warpgroup-uniform; when nothing moved every correction is
1869
+ # exactly exp2(0) = 1, so skipping is bit-identical.
1870
+ if s > 0:
1871
+ rescale = cutlass.Boolean(False)
1872
+ for i in cutlass.range_constexpr(cute.size(tSrM)):
1873
+ if tSrM[i] != tSrM_prev[i]:
1874
+ rescale = cutlass.Boolean(True)
1875
+ o_handle = o_consumer.wait_and_advance()
1876
+ if rescale:
1877
+ for dm in cutlass.range_constexpr(tiles_dm):
1878
+ tSrO_dm = cute.make_rmem_tensor_like(tSsO[cpy_dice + (0, 0)])
1879
+ cute.copy(thr_load_s, tStO[cpy_dice + (dm, 0)], tSrO_dm)
1880
+ for i in cutlass.range_constexpr(0, cute.size(tSrO_dm), 2):
1881
+ o_f32x2 = fmul2(
1882
+ (tSrO_dm[i], tSrO_dm[i + 1]),
1883
+ (correction[i], correction[i + 1]),
1884
+ )
1885
+ tSrO_dm[i] = o_f32x2[0]
1886
+ tSrO_dm[i + 1] = o_f32x2[1]
1887
+ cute.copy(thr_store_o, tSrO_dm, tStO[cpy_dice + (dm, 0)])
1888
+ cute.arch.fence_view_async_tmem_store()
1889
+ o_handle.release()
1890
+
1891
+ tSrM_prev.store(tSrM.load())
1892
+
1893
+ #
1894
+ # Epilogue
1895
+ #
1896
+ # colsum: warp reduce, then per-warp partials in smem
1897
+ tSrL_lane = cutlass.Float32(0.0)
1898
+ for i in cutlass.range_constexpr(cute.size(tSrL)):
1899
+ tSrL[i] = cute.arch.warp_reduction_sum(tSrL[i])
1900
+ if i == lane_idx:
1901
+ tSrL_lane = tSrL[i]
1902
+ if lane_store_max:
1903
+ tSsL[cpy_dice + (warpgroup_widx,)][lane_idx] = tSrL_lane
1904
+ softmax_nbar.arrive_and_wait()
1905
+
1906
+ # Row statistics (one lane per query column)
1907
+ if warpgroup_widx == 0 and lane_store_max and n_base + lane_idx < n_q:
1908
+ sL_lane_wg = sL[0, lane_idx, None]
1909
+ sL_lane = sL_lane_wg[0] + sL_lane_wg[1] + sL_lane_wg[2] + sL_lane_wg[3]
1910
+ sM_lane = sM[0, lane_idx] * cta_scale # -inf stays -inf
1911
+ n_lane = n_base + lane_idx
1912
+ if cutlass.const_expr(SINGLE_SPLIT):
1913
+ mLSE[n_lane, coord_hb] = sM_lane + cute.math.log(sL_lane, fastmath=False)
1914
+ else:
1915
+ if n_splits_seq == 1: # unsplit sequence: final LSE now
1916
+ mLSE[n_lane, coord_hb] = sM_lane + cute.math.log(sL_lane, fastmath=False)
1917
+ else:
1918
+ mM_partial[n_lane, coord_hb, split_slot] = sM_lane
1919
+ mL_partial[n_lane, coord_hb, split_slot] = sL_lane
1920
+
1921
+ # Final O from TMEM
1922
+ o_handle = o_consumer.wait_and_advance()
1923
+ cute.copy(thr_load_s, tStO, tSrO)
1924
+ cute.arch.fence_view_async_tmem_load()
1925
+ o_handle.release() # Final release signals tmem dealloc
1926
+
1927
+ # Store O straight from registers: thread t owns headdim row
1928
+ # dm * 128 + t, element i is query column n_base + i, so each
1929
+ # store instruction writes 128 consecutive d values (coalesced).
1930
+ # Unsplit sequences (one chunk) are normalized and written as the
1931
+ # final bf16 output; split ones write fp32 partials.
1932
+ out_scale = scale_o * v_descale * (1.0 / p_scale)
1933
+ part_scale = v_descale * (1.0 / p_scale)
1934
+ inv_l = cute.make_rmem_tensor_like(tSrM_prev)
1935
+ for i in cutlass.range_constexpr(cute.size(inv_l)):
1936
+ # + tiny: an empty row (seq_len 0, e.g. CUDA-graph padding)
1937
+ # has l = 0 and O = 0 -> writes 0 instead of NaN. l >= 1
1938
+ # otherwise, so the epsilon is exact in fp32.
1939
+ l_i = sL[0, i, 0] + sL[0, i, 1] + sL[0, i, 2] + sL[0, i, 3] + cutlass.Float32(1e-30)
1940
+ inv_l[i] = out_scale / l_i
1941
+ write_final = cutlass.Boolean(True)
1942
+ if cutlass.const_expr(not SINGLE_SPLIT):
1943
+ write_final = n_splits_seq == 1
1944
+ if write_final:
1945
+ for dm in cutlass.range_constexpr(tiles_dm):
1946
+ tSrO_dm = tSrO[cpy_dice + (dm, 0)]
1947
+ d_idx = dm * mma_tile_m + warpgroup_tidx
1948
+ for i in cutlass.range_constexpr(cute.size(tSrO_dm)):
1949
+ if n_base + i < n_q:
1950
+ mO[d_idx, n_base + i, coord_hb] = mO.element_type(tSrO_dm[i] * inv_l[i])
1951
+ else:
1952
+ for dm in cutlass.range_constexpr(tiles_dm):
1953
+ tSrO_dm = tSrO[cpy_dice + (dm, 0)]
1954
+ d_idx = dm * mma_tile_m + warpgroup_tidx
1955
+ for i in cutlass.range_constexpr(cute.size(tSrO_dm)):
1956
+ if n_base + i < n_q:
1957
+ mO_partial[d_idx, n_base + i, coord_hb, split_slot] = tSrO_dm[i] * part_scale
1958
+
1959
+ return
1960
+
1961
+ @staticmethod
1962
+ @cute.kernel
1963
+ def reduction(
1964
+ o: cute.Tensor, # (d, h_q, b)
1965
+ lse: cute.Tensor, # (h_q, b)
1966
+ o_partial: cute.Tensor, # (d, h_q, b, max_splits)
1967
+ m_partial: cute.Tensor, # (h_q, b, max_splits)
1968
+ l_partial: cute.Tensor, # (h_q, b, max_splits)
1969
+ n_splits: cute.Tensor, # (b,) chunks per sequence from the plan
1970
+ scale_o: cutlass.Float32,
1971
+ pdl: cutlass.Constexpr = False, # static kernel: no self
1972
+ ):
1973
+ if cutlass.const_expr(pdl):
1974
+ # launched early (PDL): the partials are only complete once the
1975
+ # decode grid has fully finished
1976
+ cute.arch.griddepcontrol_wait()
1977
+ d_blk_idx, coord_h, coord_b = cute.arch.block_idx()
1978
+ d_per_blk, _, _ = cute.arch.block_dim()
1979
+ d_idx, _, _ = cute.arch.thread_idx()
1980
+ coord_d = d_blk_idx * d_per_blk + d_idx
1981
+
1982
+ # Only the splits this sequence actually ran: the partial buffers of
1983
+ # the others were never written (no workspace init needed). A
1984
+ # sequence with one chunk already wrote its final output in decode.
1985
+ kv_splits = o_partial.shape[3] # max_splits (compile-time)
1986
+ n_valid = n_splits[coord_b]
1987
+ if n_valid > kv_splits:
1988
+ n_valid = kv_splits
1989
+
1990
+ # Issue every split's loads up front (kv_splits is compile-time, so
1991
+ # this unrolls into independent predicated loads): one DRAM latency
1992
+ # instead of a chain of ~3 * kv_splits dependent ones.
1993
+ m_vals = cute.make_rmem_tensor((kv_splits,), cutlass.Float32)
1994
+ l_vals = cute.make_rmem_tensor((kv_splits,), cutlass.Float32)
1995
+ o_vals = cute.make_rmem_tensor((kv_splits,), cutlass.Float32)
1996
+ for j in cutlass.range_constexpr(kv_splits):
1997
+ m_vals[j] = -cutlass.Float32.inf
1998
+ l_vals[j] = cutlass.Float32(0)
1999
+ o_vals[j] = cutlass.Float32(0)
2000
+ if j < n_valid:
2001
+ m_vals[j] = m_partial[coord_h, coord_b, j]
2002
+ l_vals[j] = l_partial[coord_h, coord_b, j]
2003
+ o_vals[j] = o_partial[coord_d, coord_h, coord_b, j]
2004
+
2005
+ m_max = -cutlass.Float32.inf
2006
+ for j in cutlass.range_constexpr(kv_splits):
2007
+ m_max = cute.arch.fmax(m_max, m_vals[j])
2008
+
2009
+ o_acc = cutlass.Float32(0)
2010
+ l_acc = cutlass.Float32(0)
2011
+ for j in cutlass.range_constexpr(kv_splits):
2012
+ w = cutlass.Float32(0)
2013
+ if m_vals[j] != -cutlass.Float32.inf:
2014
+ w = exp2(log2_e * (m_vals[j] - m_max))
2015
+ l_acc += w * l_vals[j]
2016
+ o_acc += w * o_vals[j]
2017
+
2018
+ inv_l = cutlass.Float32(0)
2019
+ if l_acc > cutlass.Float32(0):
2020
+ inv_l = 1.0 / l_acc
2021
+
2022
+ # n_valid <= 1: decode already wrote the final O / LSE — leave it.
2023
+ if n_valid > 1:
2024
+ if coord_d < o.shape[0]:
2025
+ o[coord_d, coord_h, coord_b] = o.element_type(scale_o * o_acc * inv_l)
2026
+ if d_blk_idx == 0 and d_idx == 0:
2027
+ lse[coord_h, coord_b] = m_max + cute.math.log(l_acc, fastmath=False)
2028
+
2029
+ return
2030
+
2031
+
2032
+ ##############################################################################
2033
+ # Host helpers (also what a vLLM backend would call)
2034
+ ##############################################################################
2035
+
2036
+
2037
+ class SplitPlan:
2038
+ """Host-side occupancy-pinned split-KV plan (see plan_split_kv)."""
2039
+
2040
+ def __init__(self, work, n_splits, chunk_tiles, slots):
2041
+ self.work = work # list of (seq, tile_begin, tile_end, split_slot)
2042
+ self.n_splits = n_splits # chunks per sequence
2043
+ self.chunk_tiles = chunk_tiles # tiles per chunk (last chunk may be short)
2044
+ self.slots = slots # work items that fit one wave
2045
+
2046
+ @property
2047
+ def num_work(self):
2048
+ return len(self.work)
2049
+
2050
+ def work_tensor(self, device="cpu"):
2051
+ t = torch.tensor(self.work if self.work else [(0, 0, 0, 0)], dtype=torch.int32)
2052
+ return t.reshape(-1, 4).contiguous().to(device)
2053
+
2054
+ def n_splits_tensor(self, device="cpu"):
2055
+ return torch.tensor(self.n_splits, dtype=torch.int32).to(device)
2056
+
2057
+ def __repr__(self):
2058
+ real = sum(1 for w in self.work if w[2] > w[1])
2059
+ return (
2060
+ f"SplitPlan(chunk={self.chunk_tiles} tiles, work={real}"
2061
+ f"{f'+{self.num_work - real} pad' if self.num_work > real else ''}, "
2062
+ f"slots/wave={self.slots}, max chunks/seq={max(self.n_splits)})"
2063
+ )
2064
+
2065
+
2066
+ def plan_split_kv(
2067
+ seq_lens,
2068
+ heads_k,
2069
+ n_q_tiles,
2070
+ sm_count,
2071
+ ctas_per_sm=1,
2072
+ max_splits=16,
2073
+ force_splits=0,
2074
+ min_chunk_tiles=1,
2075
+ pad_to=0,
2076
+ ):
2077
+ """Occupancy-pinned split-KV plan (the FlashInfer PrefillPlan / FlashDecoding
2078
+ scheme), in units of 128-row KV tiles.
2079
+
2080
+ The grid is (work items, q tiles, kv heads), so one wave holds
2081
+ slots = sm_count * ctas_per_sm // (heads_k * n_q_tiles)
2082
+ work items. Pick the SMALLEST contiguous chunk size such that the total
2083
+ number of chunks over all sequences fits in one wave; every sequence is
2084
+ then cut into ceil(tiles / chunk) contiguous chunks. Long sequences get
2085
+ more chunks, short ones fewer, so varlen batches balance, and each CTA
2086
+ streams one contiguous KV range front to back.
2087
+
2088
+ If the batch alone already fills the wave, nothing is split (each
2089
+ sequence is one work item, ordered longest first for the waves).
2090
+ force_splits > 0 cuts every sequence into that many chunks instead.
2091
+ pad_to > 0 pads the work list to a fixed length with empty items
2092
+ (begin == end, they exit at once) — keeps the grid fixed for CUDA graphs.
2093
+ """
2094
+ tiles = [max(1, math.ceil(int(L) / tile_s_rows)) for L in seq_lens]
2095
+ T = max(tiles)
2096
+ slots = max(1, (sm_count * ctas_per_sm) // (heads_k * n_q_tiles))
2097
+ lo = max(min_chunk_tiles, math.ceil(T / max_splits)) # cap chunks/seq
2098
+ if force_splits > 0:
2099
+ chunk = max(lo, math.ceil(T / force_splits))
2100
+ elif len(tiles) >= slots or max_splits == 1:
2101
+ chunk = T
2102
+ else:
2103
+ hi = T
2104
+ while lo < hi: # smallest chunk whose total work fits one wave
2105
+ mid = (lo + hi) // 2
2106
+ if sum(math.ceil(t / mid) for t in tiles) <= slots:
2107
+ hi = mid
2108
+ else:
2109
+ lo = mid + 1
2110
+ chunk = lo
2111
+ work, n_splits = [], []
2112
+ for b, t in enumerate(tiles):
2113
+ k = math.ceil(t / chunk)
2114
+ n_splits.append(k)
2115
+ for j in range(k):
2116
+ work.append((b, j * chunk, min((j + 1) * chunk, t), j))
2117
+ work.sort(key=lambda w: -(w[2] - w[1])) # longest first (stable)
2118
+ if pad_to:
2119
+ assert pad_to >= len(work), f"pad_to {pad_to} < {len(work)} work items"
2120
+ work += [(0, 0, 0, 0)] * (pad_to - len(work))
2121
+ assert max(n_splits) <= max_splits
2122
+ return SplitPlan(work, n_splits, chunk, slots)
2123
+
2124
+
2125
+ STRIDE_FIELDS = (
2126
+ "q_seq",
2127
+ "o_seq",
2128
+ "k_page",
2129
+ "k_row",
2130
+ "k_head",
2131
+ "v_page",
2132
+ "v_row",
2133
+ "v_head",
2134
+ "qd_seq",
2135
+ "qd_head",
2136
+ "kd_seq",
2137
+ "kd_head",
2138
+ "vd_seq",
2139
+ "vd_head",
2140
+ "ks_page",
2141
+ "ks_row",
2142
+ "ks_head",
2143
+ "vs_page",
2144
+ "vs_row",
2145
+ "vs_head",
2146
+ )
2147
+
2148
+
2149
+ def _descale_strides(t, num_seqs, heads_k):
2150
+ """(seq, head) element strides of a descale tensor: a 1-element tensor
2151
+ is a per-tensor scale (0, 0); (h_k,) per head; (b, h_k) per seq+head."""
2152
+ if t.numel() == 1:
2153
+ return 0, 0
2154
+ if t.dim() == 1 and t.shape[0] == heads_k:
2155
+ return 0, t.stride(0)
2156
+ assert t.dim() == 2 and t.shape[1] == heads_k, f"descale shape {tuple(t.shape)}"
2157
+ return t.stride(0), t.stride(1)
2158
+
2159
+
2160
+ def _kv_elem_strides(t, kv_bits, name):
2161
+ """(page, row, head) strides of a KV view in units of the KV ELEMENT.
2162
+ 8-bit formats: the view's own strides. Sub-byte formats (fp6 / fp4 /
2163
+ nvfp4) are passed as uint8 BYTE views (pages, PS, h_k, d * bits / 8);
2164
+ their byte strides are converted to element strides."""
2165
+ assert t.stride(3) == 1, f"{name}: head dim must be contiguous"
2166
+ byte_strides = [x * t.element_size() for x in t.stride()[:3]]
2167
+ assert all(x % 16 == 0 for x in byte_strides), f"{name}: TMA needs 16-byte-multiple strides, got {byte_strides} B"
2168
+ if kv_bits == 8:
2169
+ return tuple(t.stride()[:3])
2170
+ assert t.element_size() == 1, f"{name}: sub-byte KV must be a uint8 byte view"
2171
+ assert all((x * 8) % kv_bits == 0 for x in byte_strides), (
2172
+ f"{name}: byte strides {byte_strides} are not whole {kv_bits}-bit elements"
2173
+ )
2174
+ return tuple(x * 8 // kv_bits for x in byte_strides)
2175
+
2176
+
2177
+ def kernel_strides(q, o, k_cache, v_cache, q_descale, k_descale, v_descale, kv_bits=8, k_scales=None, v_scales=None):
2178
+ """Element strides for the kernel's `strides` argument.
2179
+
2180
+ q, o : (b, h_k, n_q, d) views (rows d-contiguous; seq stride free)
2181
+ k/v_cache : (num_pages, page_size, h_k, d) LOGICAL views of any physical
2182
+ layout with d contiguous (vLLM NHD, interleaved K|V, FlashInfer,
2183
+ head-major HND); for sub-byte formats uint8 byte views with
2184
+ last dim d * kv_bits / 8
2185
+ *_descale : 1-element, (h_k,) or (b, h_k) fp32 tensors
2186
+ k/v_scales: nvfp4 only, (num_pages, page_size, h_k, d / 16) e4m3 (uint8)
2187
+ """
2188
+ b, h_k, n_q, d = q.shape
2189
+ for name, t in (("q", q), ("o", o)):
2190
+ assert t.stride(3) == 1 and t.stride(2) == d and t.stride(1) == n_q * d, (
2191
+ f"{name}: rows must be packed (n_q, h_k) with d contiguous, got {t.stride()}"
2192
+ )
2193
+ ks = (0, 0, 0)
2194
+ vs = (0, 0, 0)
2195
+ if k_scales is not None:
2196
+ assert k_scales.stride(3) == 1 and v_scales.stride(3) == 1
2197
+ ks = tuple(k_scales.stride()[:3])
2198
+ vs = tuple(v_scales.stride()[:3])
2199
+ vals = (
2200
+ q.stride(0),
2201
+ o.stride(0),
2202
+ *_kv_elem_strides(k_cache, kv_bits, "k_cache"),
2203
+ *_kv_elem_strides(v_cache, kv_bits, "v_cache"),
2204
+ *_descale_strides(q_descale, b, h_k),
2205
+ *_descale_strides(k_descale, b, h_k),
2206
+ *_descale_strides(v_descale, b, h_k),
2207
+ *ks,
2208
+ *vs,
2209
+ )
2210
+ assert all(0 <= x < 2**31 for x in vals), f"stride out of int32 range: {vals}"
2211
+ return tuple(int(x) for x in vals)
2212
+
2213
+
2214
+ # ----------------------------------------------------------------------------
2215
+ # Narrow KV formats: value tables, quantization and bit packing
2216
+ # ----------------------------------------------------------------------------
2217
+ _MINIFLOAT = { # format: (exponent bits, mantissa bits, bias); no inf / nan
2218
+ "e3m2": (3, 2, 3),
2219
+ "e2m3": (2, 3, 1),
2220
+ "e2m1": (2, 1, 1),
2221
+ }
2222
+
2223
+
2224
+ def minifloat_table(fmt):
2225
+ """Values of every code of an fp6 / fp4 format (code = sign|exp|mant)."""
2226
+ eb, mb, bias = _MINIFLOAT[fmt]
2227
+ vals = []
2228
+ for code in range(1 << (1 + eb + mb)):
2229
+ sign = -1.0 if code >> (eb + mb) else 1.0
2230
+ e = (code >> mb) & ((1 << eb) - 1)
2231
+ m = code & ((1 << mb) - 1)
2232
+ if e == 0:
2233
+ v = (m / (1 << mb)) * 2.0 ** (1 - bias)
2234
+ else:
2235
+ v = (1 + m / (1 << mb)) * 2.0 ** (e - bias)
2236
+ vals.append(sign * v)
2237
+ return torch.tensor(vals, dtype=torch.float32)
2238
+
2239
+
2240
+ def minifloat_quantize(x, fmt):
2241
+ """Round-to-nearest (saturating) codes of x in an fp6 / fp4 format.
2242
+ Returns (codes uint8, decoded float values)."""
2243
+ table = minifloat_table(fmt)
2244
+ pos = table[: len(table) // 2] # non-negative half, ascending
2245
+ ax = x.abs().clamp(max=float(pos[-1]))
2246
+ idx = torch.bucketize(ax, pos) # first pos >= ax
2247
+ lo = (idx - 1).clamp(min=0)
2248
+ hi = idx.clamp(max=len(pos) - 1)
2249
+ pick_lo = (ax - pos[lo]) <= (pos[hi] - ax)
2250
+ mag = torch.where(pick_lo, lo, hi)
2251
+ neg = (x < 0) & (pos[mag] > 0)
2252
+ codes = (mag + neg.to(mag.dtype) * (len(table) // 2)).to(torch.uint8)
2253
+ return codes, table[codes.long()]
2254
+
2255
+
2256
+ def pack_subbyte(codes, bits):
2257
+ """Pack codes (..., d) into bytes (..., d * bits / 8): little-endian
2258
+ bitstream, element i at bits [bits*i, bits*i + bits) (fp4: element 2i in
2259
+ the low nibble)."""
2260
+ c = codes.to(torch.int64)
2261
+ d = c.shape[-1]
2262
+ if bits == 8:
2263
+ return codes.to(torch.uint8)
2264
+ if bits == 4:
2265
+ c = c.reshape(*c.shape[:-1], d // 2, 2)
2266
+ return (c[..., 0] | (c[..., 1] << 4)).to(torch.uint8)
2267
+ assert bits == 6 and d % 4 == 0
2268
+ c = c.reshape(*c.shape[:-1], d // 4, 4)
2269
+ w = c[..., 0] | (c[..., 1] << 6) | (c[..., 2] << 12) | (c[..., 3] << 18)
2270
+ out = torch.stack([w & 0xFF, (w >> 8) & 0xFF, (w >> 16) & 0xFF], dim=-1)
2271
+ return out.reshape(*c.shape[:-2], d // 4 * 3).to(torch.uint8)
2272
+
2273
+
2274
+ def nvfp4_quantize(x, group=NVFP4_GROUP):
2275
+ """NVFP4: e2m1 codes, one e4m3 scale per `group` elements of the last dim
2276
+ and one fp32 global scale. Returns (codes uint8, scales e4m3, global
2277
+ scale, values the T5 kernel feeds its fp8 MMA, descale to pass).
2278
+ Kernel value = e4m3(e2m1 * scale * 2^-3); descale = global * 2^3."""
2279
+ *lead, d = x.shape
2280
+ xg = x.reshape(*lead, d // group, group)
2281
+ amax_g = xg.abs().amax(dim=-1)
2282
+ gs = float(x.abs().amax().clamp(min=1e-12)) / (6.0 * 448.0)
2283
+ sc = (amax_g / 6.0 / gs).clamp(max=448.0).to(torch.float8_e4m3fn)
2284
+ scf = sc.float()
2285
+ denom = (scf * gs).unsqueeze(-1)
2286
+ q = torch.where(denom > 0, xg / denom.clamp(min=1e-30), torch.zeros_like(xg))
2287
+ codes, _ = minifloat_quantize(q, "e2m1")
2288
+ lut = minifloat_table("e2m1")
2289
+ kern = (lut[codes.long()] * scf.unsqueeze(-1) * NVFP4_SHIFT).to(torch.float8_e4m3fn)
2290
+ return (codes.reshape(*lead, d), sc, gs, kern.float().reshape(*lead, d), gs / NVFP4_SHIFT)
2291
+
2292
+
2293
+ def pack_q(q, num_seqs, q_len, heads_k):
2294
+ """vLLM q (num_seqs * q_len, h_q, d) -> kernel q (b, h_k, q_len * h_r, d).
2295
+ A free view for q_len == 1."""
2296
+ _, h_q, d = q.shape
2297
+ h_r = h_q // heads_k
2298
+ if q_len == 1:
2299
+ return q.view(num_seqs, heads_k, h_r, d)
2300
+ return (
2301
+ q.view(num_seqs, q_len, heads_k, h_r, d)
2302
+ .permute(0, 2, 1, 3, 4)
2303
+ .reshape(num_seqs, heads_k, q_len * h_r, d)
2304
+ .contiguous()
2305
+ )
2306
+
2307
+
2308
+ def unpack_o(o, num_seqs, q_len, heads_k):
2309
+ """kernel o (b, h_k, q_len * h_r, d) -> vLLM (num_seqs * q_len, h_q, d)."""
2310
+ b, h_k, n_q, d = o.shape
2311
+ h_r = n_q // q_len
2312
+ if q_len == 1:
2313
+ return o.view(num_seqs, h_k * h_r, d)
2314
+ return o.view(b, h_k, q_len, h_r, d).permute(0, 2, 1, 3, 4).reshape(b * q_len, h_k * h_r, d)
2315
+
2316
+
2317
+ def quantize_q_fp8(q, num_seqs, q_len, heads_k):
2318
+ """Per-(seq, kv head) e4m3 quantization of q (num_seqs * q_len, h_q, d).
2319
+ Returns (q_fp8 same shape, q_descale (num_seqs, h_k) fp32)."""
2320
+ _, h_q, d = q.shape
2321
+ h_r = h_q // heads_k
2322
+ qg = q.float().view(num_seqs, q_len, heads_k, h_r, d)
2323
+ amax = qg.abs().amax(dim=(1, 3, 4)).clamp(min=1e-6) # (b, h_k)
2324
+ q_descale = (amax / 448.0).contiguous()
2325
+ q8 = (qg / q_descale[:, None, :, None, None]).to(torch.float8_e4m3fn)
2326
+ return q8.view(num_seqs * q_len, h_q, d), q_descale
2327
+
2328
+
2329
+ def _to_cute(t, dtype=None):
2330
+ """torch -> cute tensor; fp8 goes through a uint8 view (dlpack-safe)."""
2331
+ if t.dtype == torch.float8_e4m3fn:
2332
+ ct = from_dlpack(t.view(torch.uint8), assumed_align=16)
2333
+ ct.element_type = cutlass.Float8E4M3FN
2334
+ return ct
2335
+ ct = from_dlpack(t, assumed_align=16)
2336
+ if dtype is not None:
2337
+ ct.element_type = dtype
2338
+ return ct
2339
+
2340
+
2341
+ def emulate_decode(
2342
+ q_packed, # (b, h_k, n_q, d) float — the exact values the kernel reads
2343
+ k_pool, # (num_pages, PS, h_k, d) float
2344
+ v_pool,
2345
+ block_table, # (b, max_blocks) int
2346
+ seq_lens, # (b,) int
2347
+ q_descale,
2348
+ k_descale,
2349
+ v_descale, # (b, h_k) float
2350
+ scale_qs,
2351
+ scale_o,
2352
+ q_len,
2353
+ chunk_tiles,
2354
+ fp8,
2355
+ causal=True,
2356
+ ):
2357
+ """Bit-faithful model of the kernel's arithmetic (contiguous chunk split, running
2358
+ max, -inf guard, P rounding to e4m3(p * 448) or bf16, descale folding).
2359
+ Also returns a plain fp32 attention for the fp8-vs-exact gap."""
2360
+ b, h_k, n_q, d = q_packed.shape
2361
+ PS = k_pool.shape[1]
2362
+ h_r = n_q // q_len
2363
+ p_scale = FP8_P_SCALE if fp8 else 1.0
2364
+ o_emu = torch.zeros(b, h_k, n_q, d)
2365
+ lse_emu = torch.zeros(b, h_k, n_q)
2366
+ o_exact = torch.zeros(b, h_k, n_q, d)
2367
+ for bi in range(b):
2368
+ L = int(seq_lens[bi])
2369
+ n_blk = math.ceil(L / PS)
2370
+ pages = block_table[bi, :n_blk].long()
2371
+ Kb = k_pool[pages].reshape(-1, h_k, d)[:L].permute(1, 0, 2) # (h_k, L, d)
2372
+ Vb = v_pool[pages].reshape(-1, h_k, d)[:L].permute(1, 0, 2)
2373
+ S = torch.einsum("gnd,gld->gnl", q_packed[bi], Kb) # raw scores
2374
+ qi = torch.arange(n_q) // h_r
2375
+ lim = L - q_len + qi if causal else torch.full((n_q,), L - 1) # (n_q,) last key each row sees
2376
+ masked = torch.arange(L)[None, :] > lim[:, None] # (n_q, L)
2377
+ S = S.masked_fill(masked[None], -math.inf)
2378
+ qk = q_descale[bi] * k_descale[bi] # (h_k,)
2379
+ c = (scale_qs * log2_e * qk)[:, None] # (h_k, 1)
2380
+ cta_scale = (scale_qs * qk)[:, None]
2381
+ vd = v_descale[bi][:, None, None]
2382
+
2383
+ tiles = math.ceil(L / tile_s_rows)
2384
+ o_parts, m_parts, l_parts = [], [], []
2385
+ n_chunks = math.ceil(tiles / chunk_tiles)
2386
+ for j in range(n_chunks): # contiguous chunks, as in the plan
2387
+ m = torch.full((h_k, n_q), -math.inf)
2388
+ l = torch.zeros(h_k, n_q)
2389
+ o = torch.zeros(h_k, n_q, d)
2390
+ for t in range(j * chunk_tiles, min((j + 1) * chunk_tiles, tiles)):
2391
+ sl = slice(t * tile_s_rows, min((t + 1) * tile_s_rows, L))
2392
+ St = S[:, :, sl]
2393
+ m_new = torch.maximum(m, St.amax(dim=-1))
2394
+ m_use = torch.where(m_new == -math.inf, torch.zeros_like(m_new), m_new)
2395
+ p = torch.exp2((St - m_use[..., None]) * c[..., None])
2396
+ corr = torch.exp2((m - m_use) * c)
2397
+ l = l * corr + p.sum(dim=-1)
2398
+ if fp8:
2399
+ pq = (p * p_scale).to(torch.float8_e4m3fn).float()
2400
+ else:
2401
+ pq = p.to(torch.bfloat16).float()
2402
+ o = o * corr[..., None] + torch.einsum("gnl,gld->gnd", pq, Vb[:, sl])
2403
+ m = m_new
2404
+ o_parts.append(o * vd / p_scale)
2405
+ m_parts.append(m * cta_scale)
2406
+ l_parts.append(l)
2407
+ m_all = torch.stack(m_parts).amax(dim=0)
2408
+ w = [torch.exp(mp - m_all) for mp in m_parts]
2409
+ o_sum = sum(wi[..., None] * op for wi, op in zip(w, o_parts))
2410
+ l_sum = sum(wi * lp for wi, lp in zip(w, l_parts))
2411
+ o_emu[bi] = scale_o * o_sum / l_sum[..., None]
2412
+ lse_emu[bi] = m_all + torch.log(l_sum)
2413
+
2414
+ # exact fp32 attention (no P rounding) for reference
2415
+ P = torch.softmax(S * cta_scale[..., None], dim=-1)
2416
+ o_exact[bi] = scale_o * torch.einsum("gnl,gld->gnd", P, Vb) * vd
2417
+ return o_emu, lse_emu, o_exact
2418
+
2419
+
2420
+ def run(
2421
+ batches: int = 1,
2422
+ seqlen: int = 1024,
2423
+ varlen: bool = False,
2424
+ q_len: int = 1,
2425
+ heads_q: int = 24,
2426
+ heads_k: int = 4,
2427
+ headdim: int = 256,
2428
+ page_size: int = 128,
2429
+ kv_splits: int = 0,
2430
+ q_dtype: Type[cutlass.Numeric] = cutlass.BFloat16,
2431
+ kv_dtype: Type[cutlass.Numeric] = cutlass.Float8E4M3FN,
2432
+ tolerance: float = 0.05,
2433
+ scale_s: float = 0.0,
2434
+ scale_o: float = 1.0,
2435
+ warmup_iterations: int = 0,
2436
+ iterations: int = 0,
2437
+ skip_ref_check: bool = False,
2438
+ seed: int = 1111,
2439
+ ctas_per_sm: int = 2,
2440
+ no_l2_flush: bool = False,
2441
+ kv_stages: int = 0,
2442
+ contiguous_pages: bool = False,
2443
+ max_splits: int = 16,
2444
+ pad_work_to: int = 0,
2445
+ kv_layout: str = "nhd",
2446
+ strided_qo: bool = False,
2447
+ descale: str = "per_seq_head",
2448
+ pdl: bool = False,
2449
+ l2_prefetch: int = 0,
2450
+ kv_evict_first: int = -1, # -1 auto: on for fp8 Q (vLLM default), off for bf16 Q
2451
+ kv_format: str = "fp8",
2452
+ kv_from_float: bool = False,
2453
+ non_causal: bool = False,
2454
+ **kwargs,
2455
+ ):
2456
+ if not torch.cuda.is_available():
2457
+ raise RuntimeError("GPU is required to run this example!")
2458
+ torch.manual_seed(seed)
2459
+ dev = "cuda"
2460
+
2461
+ fp8 = q_dtype.width == 8
2462
+ num_seqs = batches
2463
+ if kv_evict_first < 0:
2464
+ # matches the vLLM backend default: EVICT_FIRST wins on the fp8 path,
2465
+ # but costs the bf16 path 4-18% at 32K-256K
2466
+ kv_evict_first = 1 if fp8 else 0
2467
+ h_r = heads_q // heads_k
2468
+ n_q = q_len * h_r
2469
+ PS = page_size
2470
+
2471
+ # Kernel config. Default 2 CTAs/SM (40-CTA one-wave plan for one
2472
+ # sequence) with a one-tile KV ring: measured best on Thor. At 2 CTAs/SM
2473
+ # the bf16 path uses a single cvt warpgroup so two CTAs fit the
2474
+ # 64K-register file.
2475
+ assert kv_format in KV_FORMATS
2476
+ if kv_format != "fp8":
2477
+ assert fp8, f"kv_format {kv_format} runs on the fp8-Q path (--q_dtype Float8E4M3FN)"
2478
+ kv_dtype = {
2479
+ "e3m2": cutlass.Float6E3M2FN,
2480
+ "e2m3": cutlass.Float6E2M3FN,
2481
+ "e2m1": cutlass.Float4E2M1FN,
2482
+ "nvfp4": cutlass.Float4E2M1FN,
2483
+ }[kv_format]
2484
+ kv_bits = KV_FORMAT_BITS[kv_format]
2485
+ from_float = kv_format != "fp8" or kv_from_float
2486
+ if fp8:
2487
+ convert_warpgroups = 1 if kv_format == "nvfp4" else 0
2488
+ elif headdim > 128 and ctas_per_sm == 1:
2489
+ convert_warpgroups = 2
2490
+ else:
2491
+ convert_warpgroups = 1
2492
+ ght_base = 16 if fp8 else 8
2493
+ # Rows per CTA tile: next power of two >= n_q (min 8 bf16 / 16 fp8, max 32;
2494
+ # wider n_q is tiled across CTAs). TMEM ld/st widths must be powers of two.
2495
+ grouped_head_tile = max(ght_base, min(32, 1 << max(0, (n_q - 1).bit_length())))
2496
+ n_tiles = math.ceil(n_q / grouped_head_tile)
2497
+
2498
+ # Sequence lengths (include the q_len new tokens). A decode step always
2499
+ # has at least the new token(s) in the cache, so L < q_len is clamped.
2500
+ if seqlen < q_len:
2501
+ print(f"\tseqlen {seqlen} < q_len {q_len}: using seqlen = {q_len}")
2502
+ seqlen = q_len
2503
+ if varlen:
2504
+ seq_lens = torch.randint(q_len, seqlen + 1, (num_seqs,), dtype=torch.int32)
2505
+ seq_lens[0] = seqlen # keep the max for the split heuristic
2506
+ else:
2507
+ seq_lens = torch.full((num_seqs,), seqlen, dtype=torch.int32)
2508
+ max_seqlen = int(seq_lens.max())
2509
+
2510
+ hw = cutlass.utils.HardwareInfo()
2511
+ sm_count = hw.get_device_multiprocessor_count()
2512
+ sm_count = 20 if sm_count <= 0 else sm_count # Jetson AGX Thor: 20 SMs
2513
+ # Occupancy-pinned split-KV plan (contiguous chunks, one wave).
2514
+ plan = plan_split_kv(
2515
+ seq_lens.tolist(),
2516
+ heads_k,
2517
+ n_tiles,
2518
+ sm_count,
2519
+ ctas_per_sm=ctas_per_sm,
2520
+ max_splits=max_splits,
2521
+ force_splits=kv_splits,
2522
+ pad_to=pad_work_to,
2523
+ )
2524
+
2525
+ print("Paged FMHA decode (sm_110) with:")
2526
+ print(f"\tnum_seqs: {num_seqs}, max seqlen: {max_seqlen}, varlen: {varlen}")
2527
+ print(f"\tseq_lens: {seq_lens.tolist()[:8]}{' ...' if num_seqs > 8 else ''}")
2528
+ print(f"\tq_len: {q_len}, heads_q: {heads_q}, heads_k: {heads_k}, headdim: {headdim}")
2529
+ grid_ctas = plan.num_work * heads_k * n_tiles
2530
+ print(
2531
+ f"\tpage_size: {PS}, sm_count: {sm_count}, ctas_per_sm: {ctas_per_sm}, "
2532
+ f"grid: {grid_ctas} CTAs, pages: {'contiguous' if contiguous_pages else 'shuffled'}"
2533
+ )
2534
+ print(f"\tplan: {plan}")
2535
+ print(f"\tq_dtype: {q_dtype}, kv_dtype: {kv_dtype}")
2536
+
2537
+ fmha = MixedInputFusedMultiHeadAttentionDecode(
2538
+ headdim=headdim,
2539
+ grouped_head_tile=grouped_head_tile,
2540
+ convert_warpgroups=convert_warpgroups,
2541
+ page_size=PS,
2542
+ q_len=q_len,
2543
+ max_splits=max_splits,
2544
+ ctas_per_sm=ctas_per_sm,
2545
+ max_kv_stages=kv_stages,
2546
+ with_reduction=max(plan.n_splits) > 1,
2547
+ pdl=pdl,
2548
+ l2_prefetch=l2_prefetch,
2549
+ kv_evict_first=bool(kv_evict_first),
2550
+ kv_format=kv_format,
2551
+ causal=not non_causal,
2552
+ )
2553
+ fmha.can_implement(num_seqs, heads_q, heads_k, headdim, q_dtype, kv_dtype)
2554
+
2555
+ if scale_s == 0.0:
2556
+ scale_s = 1.0 / math.sqrt(headdim)
2557
+
2558
+ #
2559
+ # Paged KV pool with a SHUFFLED page assignment; unreferenced pages are
2560
+ # poisoned with e4m3 NaN so any stray read shows up in the output.
2561
+ #
2562
+ pages_per_seq = [math.ceil(int(L) / PS) for L in seq_lens]
2563
+ max_blocks = max(pages_per_seq)
2564
+ used_pages = sum(pages_per_seq)
2565
+ num_pages = used_pages + max(4, used_pages // 4)
2566
+ # Shuffled pages exercise the paging logic; contiguous pages (identity-
2567
+ # ordered block table) match a freshly allocated cache and the FI driver.
2568
+ perm = torch.arange(num_pages) if contiguous_pages else torch.randperm(num_pages)
2569
+ block_table = torch.zeros(num_seqs, max_blocks, dtype=torch.int32)
2570
+ cursor = 0
2571
+ for bi, n_blk in enumerate(pages_per_seq):
2572
+ block_table[bi, :n_blk] = perm[cursor : cursor + n_blk].to(torch.int32)
2573
+ cursor += n_blk
2574
+ unused = perm[cursor:]
2575
+
2576
+ # Descales: per (seq, kv head), per kv head, or one per-tensor scale
2577
+ # (vLLM's layer._k_scale) — the kernel takes them via strides.
2578
+ def make_descale():
2579
+ if descale == "per_tensor":
2580
+ return torch.rand(1) * 1.5 + 0.5
2581
+ if descale == "per_head":
2582
+ return torch.rand(heads_k) * 1.5 + 0.5
2583
+ return torch.rand(num_seqs, heads_k) * 1.5 + 0.5
2584
+
2585
+ def expand_descale(t): # logical (b, h_k) for the reference
2586
+ t = t.float()
2587
+ if t.numel() == 1:
2588
+ return t.reshape(1, 1).expand(num_seqs, heads_k)
2589
+ if t.dim() == 1:
2590
+ return t.reshape(1, heads_k).expand(num_seqs, heads_k)
2591
+ return t
2592
+
2593
+ row_bytes = headdim * kv_bits // 8
2594
+
2595
+ def kv_pool():
2596
+ """-> (bytes (pages, PS, h_k, row_bytes) uint8, kernel values (float,
2597
+ what the MMA multiplies), descale, nvfp4 scale bytes or None,
2598
+ unquantized float pool or None). Unreferenced pages are poisoned:
2599
+ e4m3 NaN for fp8, the largest code (and scale) otherwise."""
2600
+ shape = (num_pages, PS, heads_k, headdim)
2601
+ if not from_float: # legacy fp8 test data: raw e4m3 values, random descales
2602
+ x8 = (torch.randn(shape) * 2.0).to(torch.float8_e4m3fn)
2603
+ xb = x8.view(torch.uint8).clone()
2604
+ xb[unused] = 0x7F # e4m3fn NaN
2605
+ return xb, xb.view(torch.float8_e4m3fn).float(), make_descale(), None, None
2606
+ x = torch.randn(shape) # the "real" KV the cache quantizes
2607
+ if kv_format == "nvfp4":
2608
+ codes, sc, gs, vals, desc = nvfp4_quantize(x)
2609
+ codes[unused] = 7 # +6.0
2610
+ sc = sc.view(torch.uint8).clone()
2611
+ sc[unused] = 0x7E # e4m3 448
2612
+ vals = vals.clone()
2613
+ vals[unused] = float("nan") # never read by the reference
2614
+ return pack_subbyte(codes, 4), vals, torch.tensor([desc]), sc, x
2615
+ amax = x.abs().amax(dim=(0, 1, 3)).clamp(min=1e-6) # per kv head
2616
+ if kv_format == "fp8":
2617
+ kd = amax / 448.0
2618
+ x8 = (x / kd.view(1, 1, -1, 1)).to(torch.float8_e4m3fn)
2619
+ xb = x8.view(torch.uint8).clone()
2620
+ xb[unused] = 0x7F
2621
+ return xb, xb.view(torch.float8_e4m3fn).float(), kd, None, x
2622
+ fmax = float(minifloat_table(kv_format).max())
2623
+ kd = amax / fmax
2624
+ codes, vals = minifloat_quantize(x / kd.view(1, 1, -1, 1), kv_format)
2625
+ codes[unused] = (1 << (kv_bits - 1)) - 1 # largest positive code
2626
+ return pack_subbyte(codes, kv_bits), vals, kd, None, x
2627
+
2628
+ k_bytes, k_pool, k_descale, k_sc, k_true = kv_pool()
2629
+ v_bytes, v_pool, v_descale, v_sc, v_true = kv_pool()
2630
+ if from_float:
2631
+ print(
2632
+ f"\tkv: format {kv_format} ({kv_bits} bit) quantized from float data; "
2633
+ f"descale {'per-tensor (nvfp4 global)' if kv_format == 'nvfp4' else 'per kv head'}"
2634
+ f" (--descale ignored)"
2635
+ )
2636
+
2637
+ q_vllm = torch.randn(num_seqs * q_len, heads_q, headdim).to(torch.bfloat16)
2638
+ if fp8:
2639
+ q_in, q_descale = quantize_q_fp8(q_vllm, num_seqs, q_len, heads_k)
2640
+ else:
2641
+ q_in, q_descale = q_vllm, torch.ones(1) # per-tensor 1.0
2642
+ q_packed = pack_q(q_in, num_seqs, q_len, heads_k).contiguous()
2643
+
2644
+ # Device tensors. Q/O optionally live inside wider rows (like vLLM's
2645
+ # fused-QKV `query` view); the KV cache optionally uses another vLLM
2646
+ # physical layout — the kernel sees only strided logical views.
2647
+ row = heads_k * n_q * headdim
2648
+ pad = (2 * heads_k * headdim) if strided_qo else 0
2649
+
2650
+ def strided_rows(src_packed, dtype):
2651
+ buf = torch.zeros(num_seqs, row + pad, dtype=dtype, device=dev)
2652
+ view = buf[:, :row].view(num_seqs, heads_k, n_q, headdim)
2653
+ if src_packed is not None:
2654
+ if src_packed.dtype == torch.float8_e4m3fn:
2655
+ view.view(torch.uint8).copy_(src_packed.view(torch.uint8).to(dev))
2656
+ else:
2657
+ view.copy_(src_packed.to(dev))
2658
+ return view
2659
+
2660
+ q_d = strided_rows(q_packed, q_packed.dtype)
2661
+ # KV bytes in the chosen physical layout; the kernel sees logical
2662
+ # (pages, PS, h_k, row) views. fp8 views are retyped e4m3, sub-byte
2663
+ # formats stay uint8 byte views (strides converted to elements).
2664
+ rb = row_bytes
2665
+ if kv_layout == "nhd": # separate (pages, PS, h_k, row) K and V tensors
2666
+ k_d, v_d = k_bytes.to(dev), v_bytes.to(dev)
2667
+ elif kv_layout == "hnd": # head-major: (pages, 2, h_k, PS, row)
2668
+ buf = torch.empty(num_pages, 2, heads_k, PS, rb, dtype=torch.uint8, device=dev)
2669
+ buf[:, 0] = k_bytes.permute(0, 2, 1, 3).to(dev)
2670
+ buf[:, 1] = v_bytes.permute(0, 2, 1, 3).to(dev)
2671
+ k_d, v_d = buf[:, 0].transpose(1, 2), buf[:, 1].transpose(1, 2)
2672
+ elif kv_layout == "interleaved": # vLLM main FA: (pages, h_k, PS, 2*row)
2673
+ buf = torch.empty(num_pages, heads_k, PS, 2 * rb, dtype=torch.uint8, device=dev)
2674
+ buf[..., :rb] = k_bytes.permute(0, 2, 1, 3).to(dev)
2675
+ buf[..., rb:] = v_bytes.permute(0, 2, 1, 3).to(dev)
2676
+ kv = buf.transpose(1, 2) # (pages, PS, h_k, 2*row)
2677
+ k_d, v_d = kv[..., :rb], kv[..., rb:]
2678
+ elif kv_layout == "flashinfer": # FlashInfer NHD: (pages, 2, PS, h_k, row)
2679
+ buf = torch.empty(num_pages, 2, PS, heads_k, rb, dtype=torch.uint8, device=dev)
2680
+ buf[:, 0] = k_bytes.to(dev)
2681
+ buf[:, 1] = v_bytes.to(dev)
2682
+ k_d, v_d = buf[:, 0], buf[:, 1]
2683
+ else:
2684
+ raise ValueError(f"unknown kv_layout {kv_layout}")
2685
+ if kv_bits == 8:
2686
+ k_d, v_d = k_d.view(torch.float8_e4m3fn), v_d.view(torch.float8_e4m3fn)
2687
+ if kv_format == "nvfp4":
2688
+ ks_d = k_sc.to(dev).view(torch.float8_e4m3fn)
2689
+ vs_d = v_sc.to(dev).view(torch.float8_e4m3fn)
2690
+ else: # unused by the kernel; any valid e4m3 pointer
2691
+ ks_d = vs_d = torch.zeros(1, 1, 1, NVFP4_GROUP, dtype=torch.uint8, device=dev).view(torch.float8_e4m3fn)
2692
+ bt_d = block_table.to(dev)
2693
+ sl_d = seq_lens.to(dev)
2694
+ work_d = plan.work_tensor(dev)
2695
+ ns_d = plan.n_splits_tensor(dev)
2696
+ qd_d = q_descale.float().contiguous().to(dev)
2697
+ kd_d = k_descale.float().contiguous().to(dev)
2698
+ vd_d = v_descale.float().contiguous().to(dev)
2699
+ o_d = strided_rows(None, torch.bfloat16)
2700
+ strides = kernel_strides(
2701
+ q_d,
2702
+ o_d,
2703
+ k_d,
2704
+ v_d,
2705
+ qd_d,
2706
+ kd_d,
2707
+ vd_d,
2708
+ kv_bits=kv_bits,
2709
+ k_scales=ks_d if kv_format == "nvfp4" else None,
2710
+ v_scales=vs_d if kv_format == "nvfp4" else None,
2711
+ )
2712
+ print(
2713
+ f"\tlayout: kv={kv_layout} strided_qo={strided_qo} descale={descale}; "
2714
+ f"strides={dict(zip(STRIDE_FIELDS, strides))}"
2715
+ )
2716
+ lse_d = torch.empty(num_seqs, heads_k, n_q, dtype=torch.float32, device=dev)
2717
+ # Workspace: deliberately NOT initialized (the kernel must not need it)
2718
+ op_d = torch.full((max_splits, num_seqs, heads_k, n_q, headdim), float("nan"), device=dev)
2719
+ mp_d = torch.full((max_splits, num_seqs, heads_k, n_q), float("nan"), device=dev)
2720
+ lp_d = torch.full((max_splits, num_seqs, heads_k, n_q), float("nan"), device=dev)
2721
+
2722
+ def kv_ptr(t):
2723
+ if kv_bits == 8:
2724
+ return _to_cute(t).iterator
2725
+ from cutlass.cute.runtime import make_ptr # sub-byte element pointer
2726
+
2727
+ return make_ptr(kv_dtype, t.data_ptr(), cute.AddressSpace.gmem, assumed_align=32)
2728
+
2729
+ kv_ptrs = [kv_ptr(k_d), kv_ptr(v_d), _to_cute(ks_d).iterator, _to_cute(vs_d).iterator]
2730
+ cts = [
2731
+ _to_cute(q_d),
2732
+ _to_cute(bt_d),
2733
+ _to_cute(sl_d),
2734
+ _to_cute(work_d),
2735
+ _to_cute(ns_d),
2736
+ _to_cute(qd_d),
2737
+ _to_cute(kd_d),
2738
+ _to_cute(vd_d),
2739
+ _to_cute(o_d),
2740
+ _to_cute(lse_d),
2741
+ _to_cute(op_d),
2742
+ _to_cute(mp_d),
2743
+ _to_cute(lp_d),
2744
+ ]
2745
+ stream = cutlass_torch.default_stream()
2746
+ args = (
2747
+ (num_seqs, heads_k, n_q, headdim),
2748
+ max_blocks,
2749
+ num_pages,
2750
+ max(1, plan.num_work),
2751
+ strides,
2752
+ cts[0].iterator,
2753
+ *kv_ptrs,
2754
+ *[t.iterator for t in cts[1:]],
2755
+ scale_s,
2756
+ scale_o,
2757
+ stream,
2758
+ )
2759
+ compiled = cute.compile(fmha, *args, options="--opt-level 2")
2760
+ print("Finished Compiling")
2761
+
2762
+ if not skip_ref_check:
2763
+ compiled(*args)
2764
+ torch.cuda.synchronize()
2765
+ print("Verifying results...")
2766
+ o_emu, lse_emu, o_exact = emulate_decode(
2767
+ q_packed.float(),
2768
+ k_pool.float(),
2769
+ v_pool.float(),
2770
+ block_table,
2771
+ seq_lens,
2772
+ expand_descale(q_descale.float()),
2773
+ expand_descale(k_descale.float()),
2774
+ expand_descale(v_descale.float()),
2775
+ scale_s,
2776
+ scale_o,
2777
+ q_len,
2778
+ plan.chunk_tiles,
2779
+ fp8,
2780
+ causal=not non_causal,
2781
+ )
2782
+ o_kern = o_d.float().cpu()
2783
+ lse_kern = lse_d.cpu()
2784
+ if from_float:
2785
+ # what the cache format costs: kernel vs attention over the
2786
+ # UNQUANTIZED float KV (same q, P rounding aside)
2787
+ ones = torch.ones(num_seqs, heads_k)
2788
+ _, _, o_true = emulate_decode(
2789
+ q_packed.float(),
2790
+ k_true,
2791
+ v_true,
2792
+ block_table,
2793
+ seq_lens,
2794
+ expand_descale(q_descale.float()),
2795
+ ones,
2796
+ ones,
2797
+ scale_s,
2798
+ scale_o,
2799
+ q_len,
2800
+ plan.chunk_tiles,
2801
+ fp8,
2802
+ causal=not non_causal,
2803
+ )
2804
+ d_true = (o_kern - o_true).abs()
2805
+ rel = (d_true.norm() / o_true.norm().clamp(min=1e-12)).item()
2806
+ print(
2807
+ f"kv quantization error ({kv_format}): |kern-fp32 unquantized| "
2808
+ f"max={d_true.max().item():.4g} mean={d_true.mean().item():.4g} "
2809
+ f"rel_l2={rel:.4g}"
2810
+ )
2811
+ d_emu = (o_kern - o_emu).abs()
2812
+ d_exact = (o_kern - o_exact).abs()
2813
+ d_lse = (lse_kern - lse_emu).abs()
2814
+ print(
2815
+ f"check: |kern-emu| max={d_emu.max().item():.4g} mean={d_emu.mean().item():.4g}; "
2816
+ f"|lse-emu| max={d_lse.max().item():.4g}; "
2817
+ f"|kern-exact fp32| max={d_exact.max().item():.4g} "
2818
+ f"(P rounding to {'e4m3' if fp8 else 'bf16'})"
2819
+ )
2820
+ if torch.isnan(o_kern).any():
2821
+ raise AssertionError("NaN in output (stray read of a poisoned page?)")
2822
+ torch.testing.assert_close(o_kern, o_emu, atol=tolerance, rtol=2e-2)
2823
+ torch.testing.assert_close(lse_kern, lse_emu, atol=1e-2, rtol=1e-3)
2824
+ # vLLM-layout round trip sanity
2825
+ _ = unpack_o(o_d, num_seqs, q_len, heads_k)
2826
+
2827
+ exec_us = 0.0
2828
+ if iterations > 0:
2829
+ # Cold L2 (32 MiB on Thor): flush between timed launches, unless an
2830
+ # external profiler controls the cache (ncu --cache-control all) —
2831
+ # then the flush kernel is just noise in the profile.
2832
+ flush = None
2833
+ if not no_l2_flush:
2834
+ flush = torch.empty(64 * 1024 * 1024, dtype=torch.uint8, device=dev)
2835
+ for _ in range(warmup_iterations):
2836
+ compiled(*args)
2837
+ times = []
2838
+ for _ in range(iterations):
2839
+ if flush is not None:
2840
+ flush.zero_()
2841
+ start = torch.cuda.Event(enable_timing=True)
2842
+ end = torch.cuda.Event(enable_timing=True)
2843
+ start.record()
2844
+ compiled(*args)
2845
+ end.record()
2846
+ end.synchronize()
2847
+ times.append(start.elapsed_time(end) * 1000.0)
2848
+ times.sort()
2849
+ exec_us = times[len(times) // 2]
2850
+ kv_bytes = int(seq_lens.sum()) * heads_k * headdim * 2 # K + V, 1 B each
2851
+ io_bytes = q_d.numel() * q_d.element_size() + o_d.numel() * 2
2852
+ gbps = (kv_bytes + io_bytes) / (exec_us * 1e-6) / 1e9
2853
+ cache = "hot/unflushed L2" if flush is None else "cold L2"
2854
+ if os.environ.get("CUDA_INJECTION64_PATH"):
2855
+ # ncu / nsys inject a library and serialize + replay every launch:
2856
+ # host-side event timing then measures the profiler, not the kernel.
2857
+ print(
2858
+ f"median {exec_us:.1f} us host-timed UNDER A PROFILER (includes "
2859
+ f"replay/flush overhead) — use the profiler's gpu__time_duration"
2860
+ )
2861
+ else:
2862
+ print(f"median {exec_us:.2f} us, {gbps:.3g} GB/s ({cache}, {kv_bytes + io_bytes} B of KV+Q+O)")
2863
+
2864
+ return exec_us
2865
+
2866
+
2867
+ if __name__ == "__main__":
2868
+ VERBOSE = True # the harness prints the kernel's compile-time notes
2869
+ parser = argparse.ArgumentParser(description="Paged fp8 decode attention (sm_110).")
2870
+ parser.add_argument("--batches", "--batch", "--b", "--num_seqs", type=int, default=1)
2871
+ parser.add_argument(
2872
+ "--seqlen", "--seqlen_k", "--s", type=int, default=1024, help="(max) kv sequence length incl. new tokens"
2873
+ )
2874
+ parser.add_argument("--varlen", action="store_true", help="random per-sequence lengths in [q_len, seqlen]")
2875
+ parser.add_argument("--q_len", type=int, default=1, help="query tokens per sequence (spec decode)")
2876
+ parser.add_argument("--heads_q", "--h_q", type=int, default=24)
2877
+ parser.add_argument("--heads_k", "--h_k", type=int, default=4)
2878
+ parser.add_argument("--headdim", "--d", type=int, default=256)
2879
+ parser.add_argument("--page_size", type=int, default=128)
2880
+ parser.add_argument(
2881
+ "--ctas_per_sm",
2882
+ type=int,
2883
+ default=2,
2884
+ choices=(1, 2),
2885
+ help="co-resident CTAs per SM (2 = 40 CTAs/wave, measured best on Thor with the default ring depth)",
2886
+ )
2887
+ parser.add_argument(
2888
+ "--kv_stages",
2889
+ type=int,
2890
+ default=0,
2891
+ help="KV ring depth (0 = default: 1 KV tile at 2 CTAs/SM, 2 tiles at 1 CTA/SM)",
2892
+ )
2893
+ parser.add_argument(
2894
+ "--contiguous_pages", action="store_true", help="identity-ordered block table (default: shuffled pages)"
2895
+ )
2896
+ parser.add_argument(
2897
+ "--kv_splits",
2898
+ "--splits",
2899
+ type=int,
2900
+ default=0,
2901
+ help="force N contiguous chunks per sequence (0 = occupancy-pinned plan)",
2902
+ )
2903
+ parser.add_argument("--max_splits", type=int, default=16, help="partial-slot capacity per sequence (compile-time)")
2904
+ parser.add_argument(
2905
+ "--pad_work_to", type=int, default=0, help="pad the work list to N items (fixed grid, CUDA graphs)"
2906
+ )
2907
+ parser.add_argument(
2908
+ "--kv_layout",
2909
+ default=HARNESS_KV_LAYOUT,
2910
+ choices=("nhd", "hnd", "interleaved", "flashinfer"),
2911
+ help="physical KV cache layout (vLLM NHD, head-major HND "
2912
+ "(pages, 2, h_k, PS, d), vLLM-main interleaved K|V, "
2913
+ f"FlashInfer NHD); default {HARNESS_KV_LAYOUT}",
2914
+ )
2915
+ parser.add_argument(
2916
+ "--kv_format",
2917
+ default="auto",
2918
+ choices=("auto",) + KV_FORMATS,
2919
+ help="KV cache format (fp8-Q path for non-fp8): e4m3; fp6 "
2920
+ "e3m2/e2m3 or fp4 e2m1 read directly by the MMA (per "
2921
+ "kv-head descale); nvfp4 (e2m1 + e4m3 per-16 scales, "
2922
+ "converted to e4m3 in-kernel); default auto = "
2923
+ f"{HARNESS_KV_FORMAT_FP8Q} for fp8 Q, fp8 for bf16 Q",
2924
+ )
2925
+ parser.add_argument(
2926
+ "--kv_from_float",
2927
+ action="store_true",
2928
+ help="fp8: quantize KV from float data with per-head "
2929
+ "descales and report the quantization error (always "
2930
+ "on for the other formats) — the accuracy A/B baseline",
2931
+ )
2932
+ parser.add_argument(
2933
+ "--non_causal",
2934
+ action="store_true",
2935
+ help="every query token sees all seq_len keys (q_len > 1): the block attention of "
2936
+ "parallel drafters (DSpark / DFlash)",
2937
+ )
2938
+ parser.add_argument(
2939
+ "--strided_qo", action="store_true", help="Q/O rows inside wider rows (like vLLM's fused-QKV view)"
2940
+ )
2941
+ parser.add_argument(
2942
+ "--pdl",
2943
+ action="store_true",
2944
+ help="programmatic dependent launch (decode waits on the previous "
2945
+ "kernel, reduction launches early); time with --iterations/nsys, "
2946
+ "ncu serializes kernels and hides the effect",
2947
+ )
2948
+ parser.add_argument(
2949
+ "--l2_prefetch", type=int, default=0, help="TMA-prefetch K/V this many 128-row tiles ahead into L2 (0 = off)"
2950
+ )
2951
+ parser.add_argument(
2952
+ "--kv_evict_first",
2953
+ type=int,
2954
+ nargs="?",
2955
+ const=1,
2956
+ default=-1,
2957
+ choices=(-1, 0, 1),
2958
+ help="TMA K/V loads with an L2 EVICT_FIRST cache policy; "
2959
+ "default auto = on for fp8 Q, off for bf16 Q (the vLLM "
2960
+ "defaults); 0/1 force it",
2961
+ )
2962
+ parser.add_argument(
2963
+ "--descale",
2964
+ default="per_seq_head",
2965
+ choices=("per_seq_head", "per_head", "per_tensor"),
2966
+ help="K/V descale granularity (vLLM default: per_tensor)",
2967
+ )
2968
+ parser.add_argument("--q_dtype", type=cutlass.dtype, default=cutlass.BFloat16)
2969
+ parser.add_argument("--kv_dtype", type=cutlass.dtype, default=cutlass.Float8E4M3FN)
2970
+ parser.add_argument("--tolerance", type=float, default=0.05, help="atol vs the bit-faithful emulation")
2971
+ parser.add_argument("--scale_s", "--scale", type=float, default=0.0)
2972
+ parser.add_argument("--scale_o", type=float, default=1.0)
2973
+ parser.add_argument("--warmup_iterations", type=int, default=0)
2974
+ parser.add_argument("--iterations", type=int, default=0)
2975
+ parser.add_argument("--skip_ref_check", action="store_true")
2976
+ parser.add_argument("--seed", type=int, default=1111)
2977
+ parser.add_argument(
2978
+ "--no_l2_flush",
2979
+ action="store_true",
2980
+ help="don't flush L2 between timed launches (use under ncu --cache-control all)",
2981
+ )
2982
+ # Accepted for old command lines; paging is always on now.
2983
+ parser.add_argument("--paged", action="store_true", help=argparse.SUPPRESS)
2984
+
2985
+ kwargs = vars(parser.parse_args())
2986
+ if kwargs["kv_format"] == "auto": # sub-8-bit formats need fp8 Q
2987
+ kwargs["kv_format"] = HARNESS_KV_FORMAT_FP8Q if kwargs["q_dtype"] is cutlass.Float8E4M3FN else "fp8"
2988
+ run(**kwargs)
2989
+
2990
+ print("PASS")