kernel-fun 0.2.0.dev1__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.
@@ -0,0 +1,1089 @@
1
+ """003's bwd_intra — phase 1: tcgen05 MMA off-diagonals inside the 16-warp SIMT kernel.
2
+
3
+ The cross-block sweeps (2.59ms of the 5.87ms SIMT floor — the KDA003C_SKIP attribution)
4
+ move to 12 tiny tf32 GEMMs issued by warp 0 into tmem; the 16x16 diagonals, prescales,
5
+ grad io and epilogue stay exactly as the phase-0 SIMT kernel left them. No warp
6
+ specialization: one PipelineUmmaAsync commit/wait separates issue from a 4-warp-group
7
+ staging pass that applies the per-block exp2 post-factors during t2r and lands the
8
+ combined cross terms in smem, where the block loop reads them as one LDS.128 each.
9
+
10
+ The factorization is the SAME per-block-boundary form the SIMT cross sweeps used
11
+ (kb/qb/kbb through each block's own end/start boundary — every exp2 argument <= 0,
12
+ the gate-magnitude law), so the MMA computes bit-comparable inner sums, just in tf32:
13
+ row side, target block i, j < i:
14
+ Qq_j[d, r] = sum_{s in blk j} kb[s,d]·dAqk[r,s] (Qk_j with dAkk)
15
+ accq_cross[r,d] = sum_j exp2(g_r - g_e(j))[d] · Qq_j[d,r]
16
+ col side, source block i, j > i:
17
+ P_j[d, s] = sum_{r in blk j} qb[r,d]·dAqk[r,s] + kbb[r,d]·dAkk[r,s] (chained accs)
18
+ dkt_cross[s,d] = sum_j exp2(g_b0(j) - g_s)[d] · P_j[d,s]
19
+
20
+ Probe-validated pieces (probe_mma.py, 2026-08-24): tf32 trivial tiled MMAs at
21
+ (M=128, N in {16,32,48}, k=16) with A mn-major + B K-MAJOR (the mn-major B descriptor
22
+ produced deterministically wrong results at byte-identical smem placement — falsified
23
+ twice, so the col-side dA blocks are stored transposed instead); canonical operand smem
24
+ written through make_smem_layout_epi views; ONE wide A buffer k-sliced per GEMM by
25
+ fragment k-tile index; chained accumulation into one tmem acc; warp-0 issue +
26
+ PipelineUmmaAsync -> 512-thread consumer_wait; [128,16] strip t2r at tmem col offsets.
27
+
28
+ smem: the three canonical A buffers (kb/qb/kbb, 24KB each) are dead once the MMAs
29
+ commit, so the staging arrays (accq/acck/dkt, 24KB each) overlay them exactly.
30
+ Peak footprint ~192KB, 1 CTA/SM as before.
31
+
32
+ --- phase-0 header (still true for everything the MMAs didn't take) ---
33
+
34
+ Lane ownership is consecutive (d = 4*lane+c) with genuine vector accesses — every
35
+ per-lane read of its 4 columns is one LDS.128 (fp32) / LDS.64 (bf16), ditto the gmem
36
+ grad reads/writes. v2's 4-way bank conflicts on consecutive ownership were a property
37
+ of stride-4 SCALAR loads. Structure — one CTA per (chunk, b*hv), 512 threads as
38
+ (32 d-groups) x (16 row-lanes); mirror pairing balances the diagonal triangle work
39
+ (even blocks r0+rlane, odd blocks r0+15-rlane). The diagonal pairs keep EXACTLY one
40
+ one-sided exp2 per (r,s,d): nothing inside a diagonal block is ever factorized.
41
+ db is produced complete in-kernel (full-warp butterfly); the host adds the incoming db.
42
+
43
+ Every exp2 argument in this file is <= 0. The gx16 arm of dbg_intra_cute.py is the guard.
44
+ K=128, BT=64, BC=16, fixed-length only; the wrapper falls back to the Triton kernel
45
+ elsewhere. [[cutedsl-math-precise-by-default]]: all exp2 through the fastmath path.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+
51
+ import torch
52
+
53
+ import cuda.bindings.driver as cuda
54
+ import cutlass
55
+ import cutlass.cute as cute
56
+ import cutlass.pipeline as pipeline
57
+ import cutlass.utils as utils
58
+ import cutlass.utils.blackwell_helpers as sm100_utils
59
+ from cutlass.cute.nvgpu import tcgen05
60
+ from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
61
+
62
+ # The call cache lives in _common: one copy of the marshal/poke/release scheme for
63
+ # every kernel in this package. In the research ladder each kernel carried its own,
64
+ # which is how the keepalive leak was fixed in one of them and missed in three.
65
+ from ..._common.cache import ( # noqa: F401
66
+ alloc_outs as _alloc_outs,
67
+ cute_view as _cute_view,
68
+ out_specs as _out_specs,
69
+ release_keepalives as _release_keepalives,
70
+ retarget as _retarget,
71
+ )
72
+
73
+ BT = 64
74
+ BC = 16
75
+ K = 128
76
+ NBLK = BT // BC # 4
77
+ THREADS = 512 # 32 d-groups (4 cols each) x 16 row-lanes
78
+ NDG = 32 # d-groups per row: one warp spans a full row
79
+ VEC = K // NDG # 4 consecutive columns per thread (d = 4*lane + c), one vector load
80
+
81
+ # tmem column offsets of the nine accumulators (f32 cols; 288 of the 512 allocated).
82
+ # Bases are 16-col aligned — probe_mma validated an acc at col 48.
83
+ TM_QQ = (0, 48, 80) # Qq_j [128, 48/32/16], j = 0,1,2
84
+ TM_QK = (96, 144, 176) # Qk_j
85
+ TM_P = (192, 208, 240) # P_j [128, 16/32/48], j = 1,2,3
86
+
87
+ # staging tasks: (group, kind, dst_block). Weights (t2r count) ~balanced per group.
88
+ STAGE_TASKS = ((0, "row", 3), (1, "row", 2), (1, "col", 2),
89
+ (2, "row", 1), (2, "col", 0), (3, "col", 1))
90
+
91
+
92
+ def _exp2(x):
93
+ return cute.math.exp2(x, fastmath=True)
94
+
95
+
96
+ # The research tree drives these from KDA003C_SKIP to time half a kernel at a time; they
97
+ # produce WRONG results by construction, so in a library they are constants. Left in place
98
+ # rather than deleted so this file still diffs cleanly against the ladder it came from —
99
+ # every branch they guard is eliminated when the kernel is traced.
100
+ SKIP_DIAG = False
101
+ SKIP_CROSS = False
102
+ SKIP_IO = False
103
+ SKIP_STAGE = False
104
+ SKIP_MMA = False
105
+
106
+ # ptxas left alone targets 64 registers — an occupancy this smem footprint can never reach —
107
+ # and spills 1-2KB/thread. 128 fits one 512-thread CTA per SM, which is the shape this
108
+ # kernel is built for.
109
+ _MAXREG = 128
110
+
111
+ # One CTA per (chunk, b*hv): below a few waves of a 148-SM box the Triton fallback wins and
112
+ # the per-call marshaling is not amortized (T512 rows regressed to 0.90x).
113
+ _MIN_CTAS = 1024
114
+
115
+
116
+ class KdaIntraBwdKernel:
117
+ """See module docstring. io_dtype is q/k/beta's dtype (bf16/fp16); everything else fp32."""
118
+
119
+ def __init__(self, io_dtype, fold_dg=False, emit_bf16=False):
120
+ # fold_dg: emit dg already chunk-reverse-cumsum'd (fla's chunk_local_cumsum
121
+ # reverse=True contract), deleting the separate dg_cumsum launch from the chain.
122
+ # The CTA owns its whole 64-row chunk, so the fold is local: the block loop
123
+ # banks per-row dg into smem instead of gmem, then a segmented suffix scan
124
+ # (warp w owns rows 4w..4w+3) writes the final rows out coalesced.
125
+ # emit_bf16: write dq/dk in io_dtype instead of fp32, deleting the backward's
126
+ # two .to(q.dtype) cast launches. Bit-identical to cast-after (same fp32 value,
127
+ # same round-to-nearest); caller must keep it OFF when HV > H — the gva group
128
+ # reduction sums dq/dk AFTER intra and must stay fp32.
129
+ self.io_dtype = io_dtype
130
+ self.fold_dg = fold_dg
131
+ self.emit_bf16 = emit_bf16
132
+ self.f32 = cutlass.Float32
133
+ self.tf32 = cutlass.TFloat32
134
+ self.cta_group = tcgen05.CtaGroup.ONE
135
+ self.sync_barrier = pipeline.NamedBarrier(barrier_id=1, num_threads=THREADS)
136
+ self.tmem_dealloc_barrier = pipeline.NamedBarrier(
137
+ barrier_id=2, num_threads=THREADS
138
+ )
139
+ self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_100")
140
+
141
+ def _make_mmas(self):
142
+ # one trivial tiled MMA per N in {16, 32, 48}; A mn-major (d contiguous),
143
+ # B k-major everywhere (mn-major B falsified — module docstring), tf32 in,
144
+ # fp32 acc. Shared by both GEMM sides.
145
+ tf32, acc, grp = self.tf32, self.f32, self.cta_group
146
+ return {
147
+ n: sm100_utils.make_trivial_tiled_mma(
148
+ tf32, tf32,
149
+ tcgen05.OperandMajorMode("mn"), tcgen05.OperandMajorMode("k"),
150
+ acc, grp, (K, n), tcgen05.OperandSource.SMEM,
151
+ )
152
+ for n in (16, 32, 48)
153
+ }
154
+
155
+ def _setup_attributes(self):
156
+ # smem layouts, all row-major (row, d) unless stated. (Built here, not
157
+ # __init__ — cute.make_layout needs the jit MLIR context.)
158
+ self.q_layout = cute.make_layout((BT, K), stride=(K, 1))
159
+ self.g_layout = cute.make_layout((BT, K), stride=(K, 1))
160
+ self.da_layout = cute.make_layout((BT, BT), stride=(BT, 1))
161
+ self.beta_layout = cute.make_layout(BT)
162
+ # vector views over the SAME storage: inner mode = a lane's VEC consecutive
163
+ # columns, so its per-row read is one slice .load() -> LDS.128 f32 / LDS.64
164
+ # bf16. dvec_layout regroups a gmem K-row the same way for the grad io.
165
+ self.gv_layout = cute.make_layout((BT, NDG, VEC), stride=(K, VEC, 1))
166
+ self.dvec_layout = cute.make_layout((NDG, VEC), stride=(VEC, 1))
167
+
168
+ mmas = self._make_mmas()
169
+ tf32 = self.tf32
170
+ # canonical A operand: one [K, 48] mn-major buffer each for kb / qb / kbb;
171
+ # per-GEMM 16-wide k-slices via fragment k-tile indices. The epi view is the
172
+ # (m, k)-indexable layout over the same bytes (probe-validated byte match).
173
+ self.aop_layout = sm100_utils.make_smem_layout_a(
174
+ mmas[48], (K, 48, 3 * BC), tf32, 1
175
+ )
176
+ self.aop_epi = sm100_utils.make_smem_layout_epi(
177
+ tf32, utils.LayoutEnum.COL_MAJOR, (K, 3 * BC), 1
178
+ )
179
+ # canonical B operands, all k-major, one buffer per (side, j):
180
+ # row j: dA[r in (e(j), BT), s in blk j] as B[n=r-e(j), k=s-16j]
181
+ # col j: dA[r in blk j, s < 16j] as B[n=s, k=r-16j] (transposed store)
182
+ self.brow_layouts = tuple(
183
+ sm100_utils.make_smem_layout_b(
184
+ mmas[BT - BC * (j + 1)], (K, BT - BC * (j + 1), BC), tf32, 1
185
+ )
186
+ for j in range(3)
187
+ )
188
+ self.brow_epis = tuple(
189
+ sm100_utils.make_smem_layout_epi(
190
+ tf32, utils.LayoutEnum.ROW_MAJOR, (BT - BC * (j + 1), BC), 1
191
+ )
192
+ for j in range(3)
193
+ )
194
+ self.bcol_layouts = tuple(
195
+ sm100_utils.make_smem_layout_b(mmas[BC * j], (K, BC * j, BC), tf32, 1)
196
+ for j in (1, 2, 3)
197
+ )
198
+ self.bcol_epis = tuple(
199
+ sm100_utils.make_smem_layout_epi(
200
+ tf32, utils.LayoutEnum.ROW_MAJOR, (BC * j, BC), 1
201
+ )
202
+ for j in (1, 2, 3)
203
+ )
204
+ # staging overlays: plain f32 (row, d) views recast over the A buffers' bytes
205
+ # (dead once the MMAs commit). 48 rows x 128 d = exactly one A buffer each.
206
+ self.stage_layout = cute.make_layout((BT - BC, K), stride=(K, 1))
207
+ self.stagev_layout = cute.make_layout(
208
+ (BT - BC, NDG, VEC), stride=(K, VEC, 1)
209
+ )
210
+ # dg-fold scan: 16 per-segment totals (segment = 4 rows), vector-view shaped
211
+ # like one gv_layout row block. Overlays the dead sAccqS bytes post-block-loop.
212
+ self.segv_layout = cute.make_layout((BC, NDG, VEC), stride=(K, VEC, 1))
213
+
214
+ @cute.jit
215
+ def __call__(
216
+ self,
217
+ q: cute.Tensor, # (T, K, HV_q, B) io_dtype — indexed at head hv // (HV//H)
218
+ k: cute.Tensor, # (T, K, HV_q, B)
219
+ g: cute.Tensor, # (T, K, HV, B) fp32
220
+ beta: cute.Tensor, # (T, HV, B) io_dtype
221
+ dAqk: cute.Tensor, # (T, BT, HV, B) fp32
222
+ dAkk: cute.Tensor, # (T, BT, HV, B) fp32
223
+ dq_in: cute.Tensor, # (T, K, HV, B) fp32
224
+ dk_in: cute.Tensor, # (T, K, HV, B) fp32
225
+ dg_in: cute.Tensor, # (T, K, HV, B) fp32
226
+ dq_out: cute.Tensor, # (T, K, HV, B) fp32
227
+ dk_out: cute.Tensor, # (T, K, HV, B) fp32
228
+ dg_out: cute.Tensor, # (T, K, HV, B) fp32
229
+ db_out: cute.Tensor, # (T, HV, B) fp32
230
+ gsize: cutlass.Int32, # HV // H, to map hv -> q/k head
231
+ stream: cuda.CUstream,
232
+ ):
233
+ self._setup_attributes()
234
+ tf32 = self.tf32
235
+ if cutlass.const_expr(cute.cosize(self.aop_layout) != 48 * K):
236
+ raise ValueError(
237
+ f"A operand layout padded ({cute.cosize(self.aop_layout)} != {48 * K}):"
238
+ " the staging overlay assumption is broken"
239
+ )
240
+
241
+ @cute.struct
242
+ class SharedStorage:
243
+ umma_full: cute.struct.MemRange[cutlass.Int64, 2]
244
+ tmem_holding_buf: cutlass.Int32
245
+ smem_g: cute.struct.Align[
246
+ cute.struct.MemRange[self.f32, cute.cosize(self.g_layout)], 128 # type: ignore
247
+ ]
248
+ smem_daqk: cute.struct.Align[
249
+ cute.struct.MemRange[self.f32, cute.cosize(self.da_layout)], 128 # type: ignore
250
+ ]
251
+ smem_dakk: cute.struct.Align[
252
+ cute.struct.MemRange[self.f32, cute.cosize(self.da_layout)], 128 # type: ignore
253
+ ]
254
+ smem_q: cute.struct.Align[
255
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.q_layout)], 128 # type: ignore
256
+ ]
257
+ smem_k: cute.struct.Align[
258
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.q_layout)], 128 # type: ignore
259
+ ]
260
+ smem_beta: cute.struct.Align[
261
+ cute.struct.MemRange[self.f32, cute.cosize(self.beta_layout)], 128 # type: ignore
262
+ ]
263
+ # canonical MMA operands (tf32). kb/qb/kbb are overlaid by the staging
264
+ # arrays after the MMAs complete.
265
+ smem_kb: cute.struct.Align[
266
+ cute.struct.MemRange[tf32, cute.cosize(self.aop_layout)], 1024 # type: ignore
267
+ ]
268
+ smem_qb: cute.struct.Align[
269
+ cute.struct.MemRange[tf32, cute.cosize(self.aop_layout)], 1024 # type: ignore
270
+ ]
271
+ smem_kbb: cute.struct.Align[
272
+ cute.struct.MemRange[tf32, cute.cosize(self.aop_layout)], 1024 # type: ignore
273
+ ]
274
+ smem_brq0: cute.struct.Align[
275
+ cute.struct.MemRange[tf32, cute.cosize(self.brow_layouts[0])], 1024 # type: ignore
276
+ ]
277
+ smem_brq1: cute.struct.Align[
278
+ cute.struct.MemRange[tf32, cute.cosize(self.brow_layouts[1])], 1024 # type: ignore
279
+ ]
280
+ smem_brq2: cute.struct.Align[
281
+ cute.struct.MemRange[tf32, cute.cosize(self.brow_layouts[2])], 1024 # type: ignore
282
+ ]
283
+ smem_brk0: cute.struct.Align[
284
+ cute.struct.MemRange[tf32, cute.cosize(self.brow_layouts[0])], 1024 # type: ignore
285
+ ]
286
+ smem_brk1: cute.struct.Align[
287
+ cute.struct.MemRange[tf32, cute.cosize(self.brow_layouts[1])], 1024 # type: ignore
288
+ ]
289
+ smem_brk2: cute.struct.Align[
290
+ cute.struct.MemRange[tf32, cute.cosize(self.brow_layouts[2])], 1024 # type: ignore
291
+ ]
292
+ smem_bcq1: cute.struct.Align[
293
+ cute.struct.MemRange[tf32, cute.cosize(self.bcol_layouts[0])], 1024 # type: ignore
294
+ ]
295
+ smem_bcq2: cute.struct.Align[
296
+ cute.struct.MemRange[tf32, cute.cosize(self.bcol_layouts[1])], 1024 # type: ignore
297
+ ]
298
+ smem_bcq3: cute.struct.Align[
299
+ cute.struct.MemRange[tf32, cute.cosize(self.bcol_layouts[2])], 1024 # type: ignore
300
+ ]
301
+ smem_bck1: cute.struct.Align[
302
+ cute.struct.MemRange[tf32, cute.cosize(self.bcol_layouts[0])], 1024 # type: ignore
303
+ ]
304
+ smem_bck2: cute.struct.Align[
305
+ cute.struct.MemRange[tf32, cute.cosize(self.bcol_layouts[1])], 1024 # type: ignore
306
+ ]
307
+ smem_bck3: cute.struct.Align[
308
+ cute.struct.MemRange[tf32, cute.cosize(self.bcol_layouts[2])], 1024 # type: ignore
309
+ ]
310
+ # dg-fold bank: the block loop's per-row dg lands here instead of gmem,
311
+ # scanned + written out after the loop. Allocated unconditionally — the
312
+ # footprint is 1 CTA/SM with or without it (230.4KB of 232448B cap).
313
+ smem_dgacc: cute.struct.Align[
314
+ cute.struct.MemRange[self.f32, BT * K], 128 # type: ignore
315
+ ]
316
+
317
+ self.shared_storage = SharedStorage
318
+ if cutlass.const_expr(self.shared_storage.size_in_bytes() > self.smem_capacity):
319
+ raise ValueError(
320
+ f"smem {self.shared_storage.size_in_bytes()} > {self.smem_capacity}"
321
+ )
322
+
323
+ T = cute.size(g, mode=[0])
324
+ HV = cute.size(g, mode=[2])
325
+ B = cute.size(g, mode=[3])
326
+ NT = T // BT
327
+ grid = (NT * HV * B, 1, 1)
328
+
329
+ # min_blocks_per_mp=1 -> nvvm.minctasm: smem is dynamic so ptxas can't see that
330
+ # this footprint caps at 1 CTA/SM; without the hint it targets 2 CTAs (64 regs)
331
+ # and spills 2KB/thread.
332
+ self.kda_cute_intra(
333
+ q, k, g, beta, dAqk, dAkk,
334
+ dq_in, dk_in, dg_in, dq_out, dk_out, dg_out, db_out,
335
+ gsize,
336
+ ).launch(
337
+ grid=grid, block=[THREADS, 1, 1], min_blocks_per_mp=1, stream=stream
338
+ )
339
+
340
+ # Named, not `kernel`: the method name is what shows up in a CUDA launch trace, and the
341
+ # rest of this chain launches as kda_cute_fwd / kda_cute_b1 / kda_cute_dhu. A uniform
342
+ # prefix is what lets a witness check assert that ALL FOUR stages ran ours — the
343
+ # existing expect_kernels=("kda_cute",) passes if any single one did, so three stages
344
+ # could silently fall back to fla and the bench would still read green.
345
+ @cute.kernel
346
+ def kda_cute_intra(
347
+ self,
348
+ mQ: cute.Tensor, mK: cute.Tensor, mG: cute.Tensor, mBeta: cute.Tensor,
349
+ mAq: cute.Tensor, mAk: cute.Tensor,
350
+ mDqIn: cute.Tensor, mDkIn: cute.Tensor, mDgIn: cute.Tensor,
351
+ mDqOut: cute.Tensor, mDkOut: cute.Tensor, mDgOut: cute.Tensor,
352
+ mDbOut: cute.Tensor,
353
+ gsize: cutlass.Int32,
354
+ ):
355
+ f32 = self.f32
356
+ tf32 = self.tf32
357
+ # Region isolation: layouts/TiledMma built during the host trace cannot be
358
+ # referenced inside the kernel region — rebuild here.
359
+ self._setup_attributes()
360
+ mmas = self._make_mmas()
361
+ tidx, _, _ = cute.arch.thread_idx()
362
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
363
+ bidx, _, _ = cute.arch.block_idx()
364
+
365
+ HV = cute.size(mG, mode=[2])
366
+ T = cute.size(mG, mode=[0])
367
+ NT = T // BT
368
+ t_idx = bidx % NT
369
+ hv_idx = (bidx // NT) % HV
370
+ b_idx = bidx // (NT * HV)
371
+ h_idx = hv_idx // gsize
372
+ row0 = t_idx * BT # first T-row of this chunk
373
+
374
+ dg = tidx % NDG # this thread's 4 consecutive d columns: d = VEC*dg + c
375
+ rlane = tidx // NDG # 0..15, warp-uniform: one warp spans a full row
376
+
377
+ smem = utils.SmemAllocator()
378
+ storage = smem.allocate(self.shared_storage)
379
+ sG = storage.smem_g.get_tensor(self.g_layout)
380
+ sAq = storage.smem_daqk.get_tensor(self.da_layout)
381
+ sAk = storage.smem_dakk.get_tensor(self.da_layout)
382
+ sQ = storage.smem_q.get_tensor(self.q_layout)
383
+ sK = storage.smem_k.get_tensor(self.q_layout)
384
+ sBeta = storage.smem_beta.get_tensor(self.beta_layout)
385
+ # vector views of the same buffers
386
+ sGv = storage.smem_g.get_tensor(self.gv_layout)
387
+ sQv = storage.smem_q.get_tensor(self.gv_layout)
388
+ sKv = storage.smem_k.get_tensor(self.gv_layout)
389
+
390
+ # MMA operand tensors + their (m, k)-indexable epi write views
391
+ sKb = storage.smem_kb.get_tensor(
392
+ self.aop_layout.outer, swizzle=self.aop_layout.inner
393
+ )
394
+ sKbE = storage.smem_kb.get_tensor(
395
+ self.aop_epi.outer, swizzle=self.aop_epi.inner
396
+ )
397
+ sQb = storage.smem_qb.get_tensor(
398
+ self.aop_layout.outer, swizzle=self.aop_layout.inner
399
+ )
400
+ sQbE = storage.smem_qb.get_tensor(
401
+ self.aop_epi.outer, swizzle=self.aop_epi.inner
402
+ )
403
+ sKbb = storage.smem_kbb.get_tensor(
404
+ self.aop_layout.outer, swizzle=self.aop_layout.inner
405
+ )
406
+ sKbbE = storage.smem_kbb.get_tensor(
407
+ self.aop_epi.outer, swizzle=self.aop_epi.inner
408
+ )
409
+ _brq_mr = (storage.smem_brq0, storage.smem_brq1, storage.smem_brq2)
410
+ _brk_mr = (storage.smem_brk0, storage.smem_brk1, storage.smem_brk2)
411
+ _bcq_mr = (storage.smem_bcq1, storage.smem_bcq2, storage.smem_bcq3)
412
+ _bck_mr = (storage.smem_bck1, storage.smem_bck2, storage.smem_bck3)
413
+ sBrq = tuple(
414
+ mr.get_tensor(ly.outer, swizzle=ly.inner)
415
+ for mr, ly in zip(_brq_mr, self.brow_layouts)
416
+ )
417
+ sBrqE = tuple(
418
+ mr.get_tensor(ly.outer, swizzle=ly.inner)
419
+ for mr, ly in zip(_brq_mr, self.brow_epis)
420
+ )
421
+ sBrk = tuple(
422
+ mr.get_tensor(ly.outer, swizzle=ly.inner)
423
+ for mr, ly in zip(_brk_mr, self.brow_layouts)
424
+ )
425
+ sBrkE = tuple(
426
+ mr.get_tensor(ly.outer, swizzle=ly.inner)
427
+ for mr, ly in zip(_brk_mr, self.brow_epis)
428
+ )
429
+ sBcq = tuple(
430
+ mr.get_tensor(ly.outer, swizzle=ly.inner)
431
+ for mr, ly in zip(_bcq_mr, self.bcol_layouts)
432
+ )
433
+ sBcqE = tuple(
434
+ mr.get_tensor(ly.outer, swizzle=ly.inner)
435
+ for mr, ly in zip(_bcq_mr, self.bcol_epis)
436
+ )
437
+ sBck = tuple(
438
+ mr.get_tensor(ly.outer, swizzle=ly.inner)
439
+ for mr, ly in zip(_bck_mr, self.bcol_layouts)
440
+ )
441
+ sBckE = tuple(
442
+ mr.get_tensor(ly.outer, swizzle=ly.inner)
443
+ for mr, ly in zip(_bck_mr, self.bcol_epis)
444
+ )
445
+ # staging overlays over the A buffers (valid only after the MMAs commit)
446
+ _kb_flat = storage.smem_kb.get_tensor(cute.make_layout(48 * K))
447
+ _qb_flat = storage.smem_qb.get_tensor(cute.make_layout(48 * K))
448
+ _kbb_flat = storage.smem_kbb.get_tensor(cute.make_layout(48 * K))
449
+ sAccqS = cute.make_tensor(
450
+ cute.recast_ptr(_kb_flat.iterator, dtype=f32), self.stage_layout
451
+ )
452
+ sAccqSV = cute.make_tensor(
453
+ cute.recast_ptr(_kb_flat.iterator, dtype=f32), self.stagev_layout
454
+ )
455
+ sAcckS = cute.make_tensor(
456
+ cute.recast_ptr(_qb_flat.iterator, dtype=f32), self.stage_layout
457
+ )
458
+ sAcckSV = cute.make_tensor(
459
+ cute.recast_ptr(_qb_flat.iterator, dtype=f32), self.stagev_layout
460
+ )
461
+ sDktS = cute.make_tensor(
462
+ cute.recast_ptr(_kbb_flat.iterator, dtype=f32), self.stage_layout
463
+ )
464
+ sDktSV = cute.make_tensor(
465
+ cute.recast_ptr(_kbb_flat.iterator, dtype=f32), self.stagev_layout
466
+ )
467
+ # dg-fold views: the (row, dgroup, VEC) bank, and the 16 segment totals
468
+ # overlaying the dead sAccqS bytes (only touched after the post-loop barrier,
469
+ # which orders them against the block loop's staged reads).
470
+ sDgAcc = storage.smem_dgacc.get_tensor(self.gv_layout)
471
+ sSegV = cute.make_tensor(
472
+ cute.recast_ptr(_kb_flat.iterator, dtype=f32), self.segv_layout
473
+ )
474
+
475
+ # umma -> simt pipe: producer = warp 0's MMA commit, consumers = all threads
476
+ umma_pipe = pipeline.PipelineUmmaAsync.create(
477
+ num_stages=1,
478
+ producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
479
+ consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, THREADS),
480
+ barrier_storage=storage.umma_full.data_ptr(),
481
+ defer_sync=True,
482
+ )
483
+ pipeline_init_arrive(cluster_shape_mn=(1, 1, 1), is_relaxed=True)
484
+ pipeline_init_wait(cluster_shape_mn=(1, 1, 1))
485
+
486
+ # ---- tmem ----
487
+ tmem_alloc_barrier = pipeline.NamedBarrier(barrier_id=3, num_threads=THREADS)
488
+ tmem = utils.TmemAllocator(
489
+ storage.tmem_holding_buf.ptr,
490
+ barrier_for_retrieve=tmem_alloc_barrier,
491
+ allocator_warp_id=1,
492
+ )
493
+ tmem.allocate(512)
494
+ tmem.wait_for_alloc()
495
+ tmem_ptr_base = tmem.retrieve_ptr(f32)
496
+
497
+ # NOTE: no closures over tmem_ptr_base — a closure that captures a variable
498
+ # cannot be CALLED inside dynamic control flow (the warp-0 / group branches),
499
+ # so acc/strip tensors are built inline where needed.
500
+ def acc_tensor(mma, tile_n, offset, base):
501
+ shape = mma.partition_shape_C((K, tile_n))
502
+ fake = mma.make_fragment_C(cute.append(shape, 1))
503
+ return cute.make_tensor(base + offset, fake.layout)
504
+
505
+ # ---- cooperative loads: adjacent threads -> adjacent d (gmem K-innermost) ----
506
+ for i in cutlass.range_constexpr(BT * K // THREADS): # 16 rounds
507
+ idx = i * THREADS + tidx
508
+ r = idx // K
509
+ dd = idx % K
510
+ sG[r, dd] = mG[row0 + r, dd, hv_idx, b_idx]
511
+ sQ[r, dd] = mQ[row0 + r, dd, h_idx, b_idx]
512
+ sK[r, dd] = mK[row0 + r, dd, h_idx, b_idx]
513
+ for i in cutlass.range_constexpr(BT * BT // THREADS): # 8 rounds
514
+ idx = i * THREADS + tidx
515
+ r = idx // BT
516
+ ss = idx % BT
517
+ sAq[r, ss] = mAq[row0 + r, ss, hv_idx, b_idx]
518
+ sAk[r, ss] = mAk[row0 + r, ss, hv_idx, b_idx]
519
+ if tidx < BT:
520
+ sBeta[tidx] = f32(mBeta[row0 + tidx, hv_idx, b_idx])
521
+ self.sync_barrier.arrive_and_wait()
522
+
523
+ # ---- prescale into the canonical A operands + dA copies into B operands ----
524
+ # kb[s] = k[s] * exp2(g_e(s) - g_s) s in [0,48), e(s) = s's block END
525
+ # qb[r] = q[r] * exp2(g_r - g_b0(r)) r in [16,64), b0(r) = r's block START
526
+ # kbb[r] = beta_r * k[r] * exp2(g_r - g_b0(r))
527
+ # both factorization exponents <= 0 at any gate magnitude.
528
+ if cutlass.const_expr(not SKIP_CROSS):
529
+ for i in cutlass.range_constexpr((BT - BC) * K // THREADS): # 12 rounds
530
+ idx = i * THREADS + tidx
531
+ s = idx // K
532
+ dd = idx % K
533
+ e_row = (s // BC + 1) * BC
534
+ v = f32(sK[s, dd]) * _exp2(sG[e_row, dd] - sG[s, dd])
535
+ sKbE[dd, s, 0] = v.to(tf32)
536
+ for i in cutlass.range_constexpr((BT - BC) * K // THREADS): # 12 rounds
537
+ idx = i * THREADS + tidx
538
+ rr = idx // K + BC # r in [16, 64)
539
+ dd = idx % K
540
+ b0 = (rr // BC) * BC
541
+ f = _exp2(sG[rr, dd] - sG[b0, dd])
542
+ sQbE[dd, rr - BC, 0] = (f32(sQ[rr, dd]) * f).to(tf32)
543
+ sKbbE[dd, rr - BC, 0] = (sBeta[rr] * f32(sK[rr, dd]) * f).to(tf32)
544
+ # dA -> B operands (smem->smem). Row side keeps dA's orientation; the col
545
+ # side stores the block transposed (k-major B[n=s, k=r]).
546
+ for j in cutlass.range_constexpr(3):
547
+ nj = BT - BC * (j + 1)
548
+ for i in cutlass.range_constexpr((nj * BC + THREADS - 1) // THREADS):
549
+ idx = i * THREADS + tidx
550
+ if idx < nj * BC:
551
+ n = idx // BC
552
+ kx = idx % BC
553
+ sBrqE[j][n, kx, 0] = sAq[BC * (j + 1) + n, BC * j + kx].to(tf32)
554
+ sBrkE[j][n, kx, 0] = sAk[BC * (j + 1) + n, BC * j + kx].to(tf32)
555
+ for j in cutlass.range_constexpr(1, 4):
556
+ nj = BC * j
557
+ for i in cutlass.range_constexpr((nj * BC + THREADS - 1) // THREADS):
558
+ idx = i * THREADS + tidx
559
+ if idx < nj * BC:
560
+ n = idx // BC # s in [0, 16j)
561
+ kx = idx % BC # r - 16j
562
+ sBcqE[j - 1][n, kx, 0] = sAq[BC * j + kx, n].to(tf32)
563
+ sBckE[j - 1][n, kx, 0] = sAk[BC * j + kx, n].to(tf32)
564
+ self.sync_barrier.arrive_and_wait()
565
+
566
+ # ---- warp 0: issue the 12 GEMMs, commit ----
567
+ if cutlass.const_expr(not SKIP_CROSS):
568
+ # strip t2r infra FIRST: mma.set() inside the warp-0 region redefines the
569
+ # mma SSA values in a child region, so anything built from `mmas` must
570
+ # bind before that branch (IR dominance).
571
+ mma16 = mmas[16]
572
+ shape16 = mma16.partition_shape_C((K, BC))
573
+ fake16 = mma16.make_fragment_C(cute.append(shape16, 1))
574
+ t2r_atom = cute.make_copy_atom(
575
+ tcgen05.Ld16x256bOp(tcgen05.Repetition(2), tcgen05.Pack.NONE), f32
576
+ )
577
+ local_tidx = tidx % 128
578
+ group = warp_idx // 4 # warp group 4g..4g+3 spans all tmem subpartitions
579
+ strip0 = cute.make_tensor(tmem_ptr_base + 0, fake16.layout)
580
+ strip0_2d = strip0[((None, None), 0, 0, None)]
581
+ tiled_t2r = tcgen05.make_tmem_copy(t2r_atom, strip0_2d[None, None, 0])
582
+ thr_t2r = tiled_t2r.get_slice(local_tidx)
583
+ coords = thr_t2r.partition_D(cute.make_identity_tensor((K, BC)))
584
+ fragq = cute.make_rmem_tensor(coords.shape, f32)
585
+ fragk = cute.make_rmem_tensor(coords.shape, f32)
586
+ outq = cute.make_rmem_tensor(coords.shape, f32)
587
+ outk = cute.make_rmem_tensor(coords.shape, f32)
588
+
589
+ # (-1 under SKIP_MMA: issue code compiles identically but never runs)
590
+ if warp_idx == (0 if not SKIP_MMA else -1):
591
+ producer = pipeline.make_pipeline_state(
592
+ pipeline.PipelineUserType.Producer, 1
593
+ )
594
+ umma_pipe.producer_acquire(producer)
595
+ for j in cutlass.range_constexpr(3): # row side
596
+ nj = BT - BC * (j + 1)
597
+ mma = mmas[nj]
598
+ trA = mma.make_fragment_A(sKb)
599
+ trBq = mma.make_fragment_B(sBrq[j])
600
+ trBk = mma.make_fragment_B(sBrk[j])
601
+ tQq = acc_tensor(mma, nj, TM_QQ[j], tmem_ptr_base)
602
+ tQk = acc_tensor(mma, nj, TM_QK[j], tmem_ptr_base)
603
+ nkt = cute.size(trBq, mode=[2]) # k-tiles per 16-wide GEMM
604
+ for kk in cutlass.range_constexpr(nkt):
605
+ mma.set(tcgen05.Field.ACCUMULATE, cutlass.Boolean(kk != 0))
606
+ cute.gemm(
607
+ mma, tQq[None, None, None, 0],
608
+ trA[None, None, j * nkt + kk, 0],
609
+ trBq[None, None, kk, 0],
610
+ tQq[None, None, None, 0],
611
+ )
612
+ for kk in cutlass.range_constexpr(nkt):
613
+ mma.set(tcgen05.Field.ACCUMULATE, cutlass.Boolean(kk != 0))
614
+ cute.gemm(
615
+ mma, tQk[None, None, None, 0],
616
+ trA[None, None, j * nkt + kk, 0],
617
+ trBk[None, None, kk, 0],
618
+ tQk[None, None, None, 0],
619
+ )
620
+ for j in cutlass.range_constexpr(1, 4): # col side, chained pair
621
+ nj = BC * j
622
+ mma = mmas[nj]
623
+ trAq = mma.make_fragment_A(sQb)
624
+ trAk = mma.make_fragment_A(sKbb)
625
+ trBq = mma.make_fragment_B(sBcq[j - 1])
626
+ trBk = mma.make_fragment_B(sBck[j - 1])
627
+ tP = acc_tensor(mma, nj, TM_P[j - 1], tmem_ptr_base)
628
+ nkt = cute.size(trBq, mode=[2])
629
+ for kk in cutlass.range_constexpr(nkt):
630
+ mma.set(tcgen05.Field.ACCUMULATE, cutlass.Boolean(kk != 0))
631
+ cute.gemm(
632
+ mma, tP[None, None, None, 0],
633
+ trAq[None, None, (j - 1) * nkt + kk, 0],
634
+ trBq[None, None, kk, 0],
635
+ tP[None, None, None, 0],
636
+ )
637
+ for kk in cutlass.range_constexpr(nkt):
638
+ mma.set(tcgen05.Field.ACCUMULATE, cutlass.Boolean(True))
639
+ cute.gemm(
640
+ mma, tP[None, None, None, 0],
641
+ trAk[None, None, (j - 1) * nkt + kk, 0],
642
+ trBk[None, None, kk, 0],
643
+ tP[None, None, None, 0],
644
+ )
645
+ umma_pipe.producer_commit(producer)
646
+
647
+ if cutlass.const_expr(not SKIP_MMA):
648
+ consumer = pipeline.make_pipeline_state(
649
+ pipeline.PipelineUserType.Consumer, 1
650
+ )
651
+ umma_pipe.consumer_wait(consumer)
652
+
653
+ # ---- staging: 4 warp groups over the 6 dst blocks. Each task t2rs its
654
+ # [128,16] strips, applies the per-j exp2 post-factor elementwise (coords
655
+ # from the identity partition), and lands the combined cross terms in the
656
+ # (row, d) staging arrays the block loop reads back as LDS.128. ----
657
+ for tg, tkind, tdst in (() if SKIP_STAGE else STAGE_TASKS):
658
+ if group == tg:
659
+ if cutlass.const_expr(tkind == "row"):
660
+ for e in cutlass.range_constexpr(cute.size(outq)):
661
+ outq[e] = f32(0.0)
662
+ outk[e] = f32(0.0)
663
+ for j in cutlass.range_constexpr(tdst):
664
+ off = BC * (tdst - j - 1)
665
+ stq = cute.make_tensor(
666
+ tmem_ptr_base + TM_QQ[j] + off, fake16.layout
667
+ )
668
+ stk = cute.make_tensor(
669
+ tmem_ptr_base + TM_QK[j] + off, fake16.layout
670
+ )
671
+ srcq = thr_t2r.partition_S(stq[((None, None), 0, 0, None)])
672
+ srck = thr_t2r.partition_S(stk[((None, None), 0, 0, None)])
673
+ cute.copy(tiled_t2r, srcq[None, None, None, 0], fragq)
674
+ cute.copy(tiled_t2r, srck[None, None, None, 0], fragk)
675
+ cute.arch.fence_view_async_tmem_load()
676
+ for e in cutlass.range_constexpr(cute.size(outq)):
677
+ ddc, ccc = coords[e]
678
+ r = BC * tdst + ccc
679
+ fj = _exp2(sG[r, ddc] - sG[BC * (j + 1), ddc])
680
+ outq[e] += fj * fragq[e]
681
+ outk[e] += fj * fragk[e]
682
+ for e in cutlass.range_constexpr(cute.size(outq)):
683
+ ddc, ccc = coords[e]
684
+ sAccqS[BC * (tdst - 1) + ccc, ddc] = outq[e]
685
+ sAcckS[BC * (tdst - 1) + ccc, ddc] = outk[e]
686
+ if cutlass.const_expr(tkind == "col"):
687
+ for e in cutlass.range_constexpr(cute.size(outq)):
688
+ outq[e] = f32(0.0)
689
+ for j in cutlass.range_constexpr(tdst + 1, 4):
690
+ stp = cute.make_tensor(
691
+ tmem_ptr_base + TM_P[j - 1] + BC * tdst, fake16.layout
692
+ )
693
+ src = thr_t2r.partition_S(stp[((None, None), 0, 0, None)])
694
+ cute.copy(tiled_t2r, src[None, None, None, 0], fragq)
695
+ cute.arch.fence_view_async_tmem_load()
696
+ for e in cutlass.range_constexpr(cute.size(outq)):
697
+ ddc, ccc = coords[e]
698
+ s = BC * tdst + ccc
699
+ fj = _exp2(sG[BC * j, ddc] - sG[s, ddc])
700
+ outq[e] += fj * fragq[e]
701
+ for e in cutlass.range_constexpr(cute.size(outq)):
702
+ ddc, ccc = coords[e]
703
+ sDktS[BC * tdst + ccc, ddc] = outq[e]
704
+ self.sync_barrier.arrive_and_wait()
705
+
706
+ # dynamic-loop vars must exist before staged control flow; the prescale block
707
+ # that used to define them is compiled out under KDA003C_SKIP=cross
708
+ s = cutlass.Int32(0)
709
+ rr = cutlass.Int32(0)
710
+
711
+ # ==================== fused block loop: diagonals + epilogue ====================
712
+ # Each thread handles one row per 16-block (blk = i, constexpr), 4 d columns.
713
+ # Mirror pairing balances the triangle. The cross terms are one staged LDS.128
714
+ # per side now; the diagonal pairs keep EXACTLY one one-sided exp2 per (r,s,d).
715
+ for i in cutlass.range_constexpr(NBLK):
716
+ r0 = i * BC
717
+ r = r0 + (rlane if i % 2 == 0 else BC - 1 - rlane)
718
+ grv = sGv[r, dg, None].load()
719
+ gr = [grv[c] for c in range(VEC)]
720
+ # prefetch this block's incoming grads well before use
721
+ dqin = [f32(0.0) for _ in range(VEC)]
722
+ if cutlass.const_expr(not SKIP_IO):
723
+ rdqin = cute.make_tensor(
724
+ mDqIn[row0 + r, None, hv_idx, b_idx].iterator, self.dvec_layout
725
+ )
726
+ dqv = rdqin[dg, None].load()
727
+ dqin = [dqv[c] for c in range(VEC)]
728
+ accq = [f32(0.0) for _ in range(VEC)]
729
+ acck = [f32(0.0) for _ in range(VEC)]
730
+ if cutlass.const_expr(i > 0 and not SKIP_CROSS):
731
+ av = sAccqSV[r - BC, dg, None].load()
732
+ bv = sAcckSV[r - BC, dg, None].load()
733
+ accq = [av[c] for c in range(VEC)]
734
+ acck = [bv[c] for c in range(VEC)]
735
+ # diagonal: one one-sided exp2 per pair — the numerics law. Dynamic loop to
736
+ # the true (warp-uniform) bound r+1: no predicate, no wasted iterations, and
737
+ # no unroll-driven spills. Loop-carried values are named scalars.
738
+ a0 = accq[0]
739
+ a1 = accq[1]
740
+ a2 = accq[2]
741
+ a3 = accq[3]
742
+ b0 = acck[0]
743
+ b1 = acck[1]
744
+ b2 = acck[2]
745
+ b3 = acck[3]
746
+ for s in cutlass.range(r0, r0 if SKIP_DIAG else r + 1, unroll=4):
747
+ aq = sAq[r, s]
748
+ ak = sAk[r, s]
749
+ kv = sKv[s, dg, None].load() # LDS.64 (bf16 x4)
750
+ gv = sGv[s, dg, None].load() # LDS.128
751
+ t0 = f32(kv[0]) * _exp2(gr[0] - gv[0])
752
+ t1 = f32(kv[1]) * _exp2(gr[1] - gv[1])
753
+ t2 = f32(kv[2]) * _exp2(gr[2] - gv[2])
754
+ t3 = f32(kv[3]) * _exp2(gr[3] - gv[3])
755
+ a0 += aq * t0
756
+ a1 += aq * t1
757
+ a2 += aq * t2
758
+ a3 += aq * t3
759
+ b0 += ak * t0
760
+ b1 += ak * t1
761
+ b2 += ak * t2
762
+ b3 += ak * t3
763
+ accq[0] = a0
764
+ accq[1] = a1
765
+ accq[2] = a2
766
+ accq[3] = a3
767
+ acck[0] = b0
768
+ acck[1] = b1
769
+ acck[2] = b2
770
+ acck[3] = b3
771
+ # row-side outputs and short-lived staging for the column side
772
+ v = f32(0.0)
773
+ beta_r = sBeta[r]
774
+ qv = sQv[r, dg, None].load()
775
+ kvr = sKv[r, dg, None].load() # row r == col-side s: reused by the epilogue
776
+ p1r = [f32(0.0) for _ in range(VEC)]
777
+ p2r = [f32(0.0) for _ in range(VEC)]
778
+ oq = cute.make_fragment_like(cute.make_layout(VEC), f32)
779
+ for c in cutlass.range_constexpr(VEC):
780
+ oq[c] = accq[c] + dqin[c]
781
+ p1r[c] = beta_r * acck[c]
782
+ p2r[c] = f32(qv[c]) * accq[c]
783
+ v += acck[c] * f32(kvr[c])
784
+ rdqo = cute.make_tensor(
785
+ mDqOut[row0 + r, None, hv_idx, b_idx].iterator, self.dvec_layout
786
+ )
787
+ if cutlass.const_expr(self.emit_bf16):
788
+ oqh = cute.make_fragment_like(cute.make_layout(VEC), self.io_dtype)
789
+ for c in cutlass.range_constexpr(VEC):
790
+ oqh[c] = oq[c].to(self.io_dtype)
791
+ cute.autovec_copy(oqh, rdqo[dg, None])
792
+ else:
793
+ cute.autovec_copy(oq, rdqo[dg, None])
794
+ # db[r] = sum_d dwk * k: this warp owns the whole row — butterfly, lane 0
795
+ v += cute.arch.shuffle_sync_bfly(v, offset=16)
796
+ v += cute.arch.shuffle_sync_bfly(v, offset=8)
797
+ v += cute.arch.shuffle_sync_bfly(v, offset=4)
798
+ v += cute.arch.shuffle_sync_bfly(v, offset=2)
799
+ v += cute.arch.shuffle_sync_bfly(v, offset=1)
800
+ if tidx % NDG == 0:
801
+ mDbOut[row0 + r, hv_idx, b_idx] = v
802
+
803
+ # column side for s = r (same row, so p1/p2 are still in registers)
804
+ s = r
805
+ dkin = [f32(0.0) for _ in range(VEC)]
806
+ dgin = [f32(0.0) for _ in range(VEC)]
807
+ if cutlass.const_expr(not SKIP_IO):
808
+ rdkin = cute.make_tensor(
809
+ mDkIn[row0 + s, None, hv_idx, b_idx].iterator, self.dvec_layout
810
+ )
811
+ rdgin = cute.make_tensor(
812
+ mDgIn[row0 + s, None, hv_idx, b_idx].iterator, self.dvec_layout
813
+ )
814
+ dkv = rdkin[dg, None].load()
815
+ dgv = rdgin[dg, None].load()
816
+ dkin = [dkv[c] for c in range(VEC)]
817
+ dgin = [dgv[c] for c in range(VEC)]
818
+ dkt = [f32(0.0) for _ in range(VEC)]
819
+ if cutlass.const_expr(i < NBLK - 1 and not SKIP_CROSS):
820
+ dv = sDktSV[s, dg, None].load()
821
+ dkt = [dv[c] for c in range(VEC)]
822
+ # diagonal: r in [s, r0+BC), one one-sided exp2 per pair
823
+ k0 = dkt[0]
824
+ k1 = dkt[1]
825
+ k2 = dkt[2]
826
+ k3 = dkt[3]
827
+ for rr in cutlass.range(
828
+ (r0 + BC) if SKIP_DIAG else s, r0 + BC, unroll=4
829
+ ):
830
+ aq = sAq[rr, s]
831
+ ak = sAk[rr, s]
832
+ beta_rr = sBeta[rr]
833
+ qv2 = sQv[rr, dg, None].load()
834
+ kv2 = sKv[rr, dg, None].load()
835
+ gv2 = sGv[rr, dg, None].load()
836
+ k0 += (
837
+ aq * f32(qv2[0]) + ak * beta_rr * f32(kv2[0])
838
+ ) * _exp2(gv2[0] - gr[0])
839
+ k1 += (
840
+ aq * f32(qv2[1]) + ak * beta_rr * f32(kv2[1])
841
+ ) * _exp2(gv2[1] - gr[1])
842
+ k2 += (
843
+ aq * f32(qv2[2]) + ak * beta_rr * f32(kv2[2])
844
+ ) * _exp2(gv2[2] - gr[2])
845
+ k3 += (
846
+ aq * f32(qv2[3]) + ak * beta_rr * f32(kv2[3])
847
+ ) * _exp2(gv2[3] - gr[3])
848
+ dkt[0] = k0
849
+ dkt[1] = k1
850
+ dkt[2] = k2
851
+ dkt[3] = k3
852
+ # epilogue: combine with the row-side partials and the incoming grads
853
+ # (kvr is the row-side load of sK[r] and s == r)
854
+ ok_ = cute.make_fragment_like(cute.make_layout(VEC), f32)
855
+ og_ = cute.make_fragment_like(cute.make_layout(VEC), f32)
856
+ for c in cutlass.range_constexpr(VEC):
857
+ kf = f32(kvr[c])
858
+ ok_[c] = dkin[c] + p1r[c] + dkt[c]
859
+ og_[c] = dgin[c] + p2r[c] + (p1r[c] - dkt[c]) * kf
860
+ rdko = cute.make_tensor(
861
+ mDkOut[row0 + s, None, hv_idx, b_idx].iterator, self.dvec_layout
862
+ )
863
+ rdgo = cute.make_tensor(
864
+ mDgOut[row0 + s, None, hv_idx, b_idx].iterator, self.dvec_layout
865
+ )
866
+ if cutlass.const_expr(self.emit_bf16):
867
+ okh = cute.make_fragment_like(cute.make_layout(VEC), self.io_dtype)
868
+ for c in cutlass.range_constexpr(VEC):
869
+ okh[c] = ok_[c].to(self.io_dtype)
870
+ cute.autovec_copy(okh, rdko[dg, None])
871
+ else:
872
+ cute.autovec_copy(ok_, rdko[dg, None])
873
+ if cutlass.const_expr(self.fold_dg):
874
+ cute.autovec_copy(og_, sDgAcc[s, dg, None])
875
+ else:
876
+ cute.autovec_copy(og_, rdgo[dg, None])
877
+
878
+ if cutlass.const_expr(self.fold_dg):
879
+ # chunk-local reverse cumsum of dg (inclusive suffix sum — fla's
880
+ # chunk_local_cumsum(reverse=True) contract; the separate launch is gone
881
+ # from the chain). Warp w owns rows 4w..4w+3 over its lane's VEC columns,
882
+ # so every output row is one coalesced 512B store; segment totals cross
883
+ # warps through sSegV.
884
+ self.sync_barrier.arrive_and_wait()
885
+ t0 = f32(0.0)
886
+ t1 = f32(0.0)
887
+ t2 = f32(0.0)
888
+ t3 = f32(0.0)
889
+ for e in cutlass.range_constexpr(4):
890
+ v4 = sDgAcc[4 * rlane + e, dg, None].load()
891
+ t0 += v4[0]
892
+ t1 += v4[1]
893
+ t2 += v4[2]
894
+ t3 += v4[3]
895
+ segf = cute.make_fragment_like(cute.make_layout(VEC), f32)
896
+ segf[0] = t0
897
+ segf[1] = t1
898
+ segf[2] = t2
899
+ segf[3] = t3
900
+ cute.autovec_copy(segf, sSegV[rlane, dg, None])
901
+ self.sync_barrier.arrive_and_wait()
902
+ o0 = f32(0.0)
903
+ o1 = f32(0.0)
904
+ o2 = f32(0.0)
905
+ o3 = f32(0.0)
906
+ for rl2 in cutlass.range(rlane + 1, BC):
907
+ v4 = sSegV[rl2, dg, None].load()
908
+ o0 += v4[0]
909
+ o1 += v4[1]
910
+ o2 += v4[2]
911
+ o3 += v4[3]
912
+ og2 = cute.make_fragment_like(cute.make_layout(VEC), f32)
913
+ for e in cutlass.range_constexpr(4):
914
+ row = 4 * rlane + 3 - e
915
+ v4 = sDgAcc[row, dg, None].load()
916
+ o0 += v4[0]
917
+ o1 += v4[1]
918
+ o2 += v4[2]
919
+ o3 += v4[3]
920
+ og2[0] = o0
921
+ og2[1] = o1
922
+ og2[2] = o2
923
+ og2[3] = o3
924
+ rdgo2 = cute.make_tensor(
925
+ mDgOut[row0 + row, None, hv_idx, b_idx].iterator, self.dvec_layout
926
+ )
927
+ cute.autovec_copy(og2, rdgo2[dg, None])
928
+
929
+ tmem.relinquish_alloc_permit()
930
+ self.tmem_dealloc_barrier.arrive_and_wait()
931
+ tmem.free(tmem_ptr_base)
932
+
933
+
934
+ # ------------------------------- host side ------------------------------------------
935
+
936
+
937
+ _COMPILE_CACHE: dict = {}
938
+ _CALL_CACHE: dict = {}
939
+
940
+
941
+ def kda_cute_intra_call(
942
+ q: torch.Tensor, # [B,T,H,K] bf16/fp16
943
+ k: torch.Tensor, # [B,T,H,K]
944
+ g: torch.Tensor, # [B,T,HV,K] fp32 (chunk-local cumsum, log2)
945
+ beta: torch.Tensor, # [B,T,HV]
946
+ dAqk: torch.Tensor, # [B,T,HV,BT] fp32
947
+ dAkk: torch.Tensor, # [B,T,HV,BT] fp32
948
+ dq: torch.Tensor, # [B,T,HV,K] fp32 incoming
949
+ dk: torch.Tensor,
950
+ dg: torch.Tensor,
951
+ fold_dg: bool = False,
952
+ emit_bf16: bool = False,
953
+ ):
954
+ B, T, HV, Kdim = g.shape
955
+ H = q.shape[2]
956
+ assert Kdim == K and dAqk.shape[3] == BT and T % BT == 0
957
+
958
+ key = tuple(
959
+ (t.shape, t.stride(), t.dtype) for t in (q, k, g, beta, dAqk, dAkk, dq, dk, dg)
960
+ ) + (torch.cuda.current_stream().cuda_stream, fold_dg, emit_bf16)
961
+ ent = _CALL_CACHE.get(key)
962
+ outs = None
963
+ if ent is None:
964
+ out_dtype = q.dtype if emit_bf16 else dq.dtype
965
+ dq2 = torch.empty_like(dq, dtype=out_dtype)
966
+ dk2 = torch.empty_like(dk, dtype=out_dtype)
967
+ dg2 = torch.empty_like(dg, dtype=torch.float)
968
+ db2 = torch.empty(B, T, HV, device=beta.device, dtype=torch.float)
969
+
970
+ io_dtype = cutlass.BFloat16 if q.dtype == torch.bfloat16 else cutlass.Float16
971
+ compile_key = (io_dtype, fold_dg, emit_bf16)
972
+
973
+ cq = _cute_view(q, (1, 3, 2, 0), (0, 2, 3))
974
+ ck = _cute_view(k, (1, 3, 2, 0), (0, 2, 3))
975
+ cg = _cute_view(g, (1, 3, 2, 0), (0, 2, 3))
976
+ cbeta = _cute_view(beta, (1, 2, 0), (0, 1, 2))
977
+ cdaqk = _cute_view(dAqk, (1, 3, 2, 0), (0, 2, 3))
978
+ cdakk = _cute_view(dAkk, (1, 3, 2, 0), (0, 2, 3))
979
+ cdq = _cute_view(dq, (1, 3, 2, 0), (0, 2, 3))
980
+ cdk = _cute_view(dk, (1, 3, 2, 0), (0, 2, 3))
981
+ cdg = _cute_view(dg, (1, 3, 2, 0), (0, 2, 3))
982
+ cdq2 = _cute_view(dq2, (1, 3, 2, 0), (0, 2, 3))
983
+ cdk2 = _cute_view(dk2, (1, 3, 2, 0), (0, 2, 3))
984
+ cdg2 = _cute_view(dg2, (1, 3, 2, 0), (0, 2, 3))
985
+ cdb2 = _cute_view(db2, (1, 2, 0), (0, 1, 2))
986
+
987
+ stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
988
+ compiled = _COMPILE_CACHE.get(compile_key)
989
+ if compiled is None:
990
+ kernel_obj = KdaIntraBwdKernel(io_dtype, fold_dg, emit_bf16)
991
+ # ptxas left alone targets 64 regs (an occupancy this smem footprint can
992
+ # never reach) and spills 1-2KB/thread; cap at 128 (1 CTA x 512 thr fits).
993
+ maxreg = _MAXREG
994
+ compiled = cute.compile(
995
+ kernel_obj, cq, ck, cg, cbeta, cdaqk, cdakk,
996
+ cdq, cdk, cdg, cdq2, cdk2, cdg2, cdb2,
997
+ cutlass.Int32(HV // H), stream,
998
+ options=f"--ptxas-options --maxrregcount={maxreg}",
999
+ )
1000
+ _COMPILE_CACHE[compile_key] = compiled
1001
+ # See 002's kernel_fwd._release_keepalives: this entry would otherwise pin the
1002
+ # first backward's q/k/g2/beta/dAqk/dAkk/dq/dk/dg and its own four outputs.
1003
+ _release_keepalives(
1004
+ cq, ck, cg, cbeta, cdaqk, cdakk, cdq, cdk, cdg, cdq2, cdk2, cdg2, cdb2
1005
+ )
1006
+ args = (
1007
+ cq, ck, cg, cbeta, cdaqk, cdakk, cdq, cdk, cdg,
1008
+ cdq2, cdk2, cdg2, cdb2, cutlass.Int32(HV // H), stream,
1009
+ )
1010
+ if len(_CALL_CACHE) >= 64:
1011
+ _CALL_CACHE.clear()
1012
+ outs = (dq2, dk2, dg2, db2)
1013
+ out_specs = tuple((tuple(t.shape), t.dtype) for t in outs)
1014
+ ent = (compiled, args, out_specs)
1015
+ _CALL_CACHE[key] = ent
1016
+
1017
+ compiled, args, out_specs = ent
1018
+ if outs is None:
1019
+ outs = tuple(
1020
+ torch.empty(shape, device=q.device, dtype=dtype) for shape, dtype in out_specs
1021
+ )
1022
+ dq2, dk2, dg2, db2 = outs
1023
+ cq, ck, cg, cbeta, cdaqk, cdakk, cdq, cdk, cdg, cdq2, cdk2, cdg2, cdb2, _, _ = args
1024
+ _retarget(cq, q)
1025
+ _retarget(ck, k)
1026
+ _retarget(cg, g)
1027
+ _retarget(cbeta, beta)
1028
+ _retarget(cdaqk, dAqk)
1029
+ _retarget(cdakk, dAkk)
1030
+ _retarget(cdq, dq)
1031
+ _retarget(cdk, dk)
1032
+ _retarget(cdg, dg)
1033
+ _retarget(cdq2, dq2)
1034
+ _retarget(cdk2, dk2)
1035
+ _retarget(cdg2, dg2)
1036
+ _retarget(cdb2, db2)
1037
+ compiled(*args)
1038
+ return dq2, dk2, db2, dg2
1039
+
1040
+
1041
+ def chunk_kda_bwd_intra_cutedsl(
1042
+ q, k, g, beta, dAqk, dAkk, dq, dk, db, dg,
1043
+ cu_seqlens=None, chunk_indices=None, chunk_size=64, safe_gate=False,
1044
+ fold_dg=False, emit_bf16=False,
1045
+ ):
1046
+ """fla-wrapper-shaped entry; falls back to the Triton kernel off the supported box.
1047
+
1048
+ fold_dg=True changes the dg output contract: dg comes back already chunk-reverse-
1049
+ cumsum'd (fla's chunk_local_cumsum(reverse=True)), honored on BOTH paths so callers
1050
+ can drop the chain's dg_cumsum stage unconditionally.
1051
+
1052
+ emit_bf16=True writes dq/dk in q's dtype (bit-identical to casting the fp32 values
1053
+ after — the backward's .to(q.dtype) then no-ops). Best-effort: forced off for gva
1054
+ (the HV>H group reduction sums dq/dk after intra in fp32) and ignored by the
1055
+ fallback path, whose fp32 outputs the backward casts as before."""
1056
+ emit_bf16 = emit_bf16 and g.shape[2] == q.shape[2]
1057
+ if (
1058
+ cu_seqlens is not None
1059
+ or safe_gate
1060
+ or chunk_size != BT
1061
+ or k.shape[-1] != K
1062
+ or k.shape[1] % BT != 0
1063
+ # small grids underfill the SMs (1 CTA per (chunk, b*hv)) and the per-call
1064
+ # marshaling isn't amortized — T512 rows regressed 0.90x; the Triton kernel
1065
+ # wins below a few waves of the 148-SM box. KDA003_INTRA=cutedsl forces the
1066
+ # CuTe path anyway (dbg_intra_cute's small correctness arms need it).
1067
+ or g.shape[0] * (k.shape[1] // BT) * g.shape[2] < _MIN_CTAS
1068
+ ):
1069
+ # 003 has no Triton kernel of its own — the small-grid/off-shape fallback is
1070
+ # 002's, which in turn falls back to fla off the box.
1071
+ from .bwd_intra_triton import chunk_kda_bwd_intra_cute
1072
+
1073
+ out = chunk_kda_bwd_intra_cute(
1074
+ q=q, k=k, g=g, beta=beta, dAqk=dAqk, dAkk=dAkk,
1075
+ dq=dq, dk=dk, db=db, dg=dg,
1076
+ cu_seqlens=cu_seqlens, chunk_indices=chunk_indices,
1077
+ chunk_size=chunk_size, safe_gate=safe_gate,
1078
+ )
1079
+ if fold_dg:
1080
+ from fla.ops.utils import chunk_local_cumsum
1081
+
1082
+ dq2, dk2, db2, dg2 = out
1083
+ dg2 = chunk_local_cumsum(dg2, chunk_size=chunk_size, reverse=True)
1084
+ return dq2, dk2, db2, dg2
1085
+ return out
1086
+ dq2, dk2, db2, dg2 = kda_cute_intra_call(
1087
+ q, k, g, beta, dAqk, dAkk, dq, dk, dg, fold_dg=fold_dg, emit_bf16=emit_bf16
1088
+ )
1089
+ return dq2, dk2, db2.add_(db), dg2