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/__init__.py +76 -0
- mjolnir/backend.py +934 -0
- mjolnir/banner.py +127 -0
- mjolnir/calibrate.py +182 -0
- mjolnir/draftvocab.py +150 -0
- mjolnir/kvfp6.py +326 -0
- mjolnir/kvscales.py +162 -0
- mjolnir/lmhead.py +379 -0
- mjolnir/qpack.py +132 -0
- mjolnir/runner.py +754 -0
- mjolnir/t5_decode.py +2990 -0
- mjolnir_kernel-0.1.0.dist-info/METADATA +222 -0
- mjolnir_kernel-0.1.0.dist-info/RECORD +17 -0
- mjolnir_kernel-0.1.0.dist-info/WHEEL +4 -0
- mjolnir_kernel-0.1.0.dist-info/entry_points.txt +2 -0
- mjolnir_kernel-0.1.0.dist-info/licenses/LICENSE +202 -0
- mjolnir_kernel-0.1.0.dist-info/licenses/NOTICE +64 -0
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")
|