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,1105 @@
1
+ """Phase 2 B1 — the CuTe forward re-scan fused with dq's h consumer. See ALGORITHM.md.
2
+
3
+ Replaces stage 2 (fla's `chunk_gated_delta_rule_fwd_h` re-run) and the dq half of stage 5.
4
+ kernel_fwd.py's scan skeleton (per-dim decay: gd K-vector broadcast, kg carries the v~
5
+ scaling so v' stores un-decayed — fla's USE_GK contract) crossed with gdn 003
6
+ kernel_fwdh.py's store machinery (h checkpoint + v_new staging, the record-006 store-wait
7
+ deferrals). Per (b, hv, v_tile) CTA, h [K, BV] fp32 resident in SIMT registers, per chunk:
8
+
9
+ hck[c] = h (bf16 -> HBM pre-update, via COL_MAJOR trans staging)
10
+ WH = w @ h_c (MMA, h from smem bf16)
11
+ DQ = do @ h_c^T (MMA — dq's ONLY h dependence, stolen from wy_dqkg;
12
+ h's smem bytes double as the mn-major B operand)
13
+ v' = u - WH (SIMT) -> v_new HBM + DH's A operand
14
+ DH = kg^T @ v' (MMA)
15
+ h = exp2(gd) * h + DH (SIMT regs, per-dim K-vector decay)
16
+
17
+ DQ is RAW (no gate, no scale): the wy_dqkg variant (kernel_wy.py) applies scale*exp2(g2)
18
+ where it already loads g2. The V-tile reduction crosses CTAs, so dq is zero-initialized
19
+ host-side and the epilogue warpgroup (8..11, the warps that assembled o in the forward)
20
+ t2r's DQ and issues cp.reduce.async.bulk.tensor.add (TMA reduce) from a fp32 staging
21
+ buffer — fp32 adds in L2, no atomic loop, off the recurrence critical path.
22
+
23
+ Logical gmem mode order (M/N, K_contract, rest) per operand:
24
+ w: (T, K, HV, B) A of WH
25
+ do: (T, V, HV, B) A of DQ (same view as u)
26
+ kg^T: (K, T, HV, B) mn-major B of DH (same storage as kg)
27
+ u: (T, V, HV, B) SIMT-only, linear smem
28
+ gd: (K, NT, HV, B) fp32 per-chunk decay vectors (g2 last rows), K contiguous
29
+ h0: (K, V, HV, B) fp32 scalar gather, once per CTA
30
+ v_new: (T, V, HV, B) TMA store
31
+ hck: (V, K, NT, HV, B) TMA store (bf16); smem operand bytes are K-contiguous and
32
+ TMA cannot transpose -> COL_MAJOR staging (gdn 003's fix)
33
+ dq: (T, K, HV, B) fp32 TMA reduce-add
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from typing import Type
39
+
40
+ import torch
41
+
42
+ import cuda.bindings.driver as cuda
43
+ import cutlass
44
+ import cutlass.cute as cute
45
+ import cutlass.pipeline as pipeline
46
+ import cutlass.utils as utils
47
+ import cutlass.utils.blackwell_helpers as sm100_utils
48
+ from cutlass.cute.nvgpu import cpasync, tcgen05
49
+ from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
50
+
51
+ # The call cache lives in _common: one copy of the marshal/poke/release scheme for
52
+ # every kernel in this package. In the research ladder each kernel carried its own,
53
+ # which is how the keepalive leak was fixed in one of them and missed in three.
54
+ from ..._common.cache import ( # noqa: F401
55
+ alloc_outs as _alloc_outs,
56
+ cute_view as _cute_view,
57
+ out_specs as _out_specs,
58
+ release_keepalives as _release_keepalives,
59
+ retarget as _retarget,
60
+ )
61
+
62
+
63
+ class KdaB1ScanKernel:
64
+ """Fused re-scan + dq. One CTA per (b, hv, v_tile); serial over chunks.
65
+
66
+ Warps: 0 = TMA producer, 1 = MMA, 4..7 = SIMT recurrence (v', h update, h/v_new
67
+ stores), 8..11 = dq epilogue (DQ readout + TMA reduce-add). Warps 2,3 idle through
68
+ the role branch and only participate in alloc/dealloc barriers.
69
+ """
70
+
71
+ def __init__(self, io_dtype: Type[cutlass.Numeric], K: int, V_TILE: int,
72
+ enable_dq: bool = True):
73
+ # enable_dq=False compiles the pure rescan (no do load, no DQ MMA, no reduce)
74
+ # — the attribution knob that splits the dq fusion's cost into traffic vs
75
+ # pipeline stall (KDA002_B1=nodq at the dispatcher).
76
+ self.enable_dq = enable_dq
77
+ self.io_dtype = io_dtype
78
+ self.acc_dtype = cutlass.Float32
79
+ self.BT = 64
80
+ self.K = K
81
+ self.BV = V_TILE
82
+
83
+ assert K in (64, 128), "K must be 64 or 128"
84
+ assert self.BV == 64, "V tile is 64"
85
+
86
+ # MMA tile shapes (M, N, K_contract)
87
+ self.tile_wh = (self.BT, self.BV, self.K) # w @ h
88
+ self.tile_dq = (self.BT, self.K, self.BV) # do @ h^T
89
+ self.tile_dh = (self.BV, self.K, self.BT) # DH^T = v'^T @ kg
90
+
91
+ self.cta_group = tcgen05.CtaGroup.ONE
92
+
93
+ self.tma_warp_id = 0
94
+ self.mma_warp_id = 1
95
+ self.simt_warp_id = (4, 5, 6, 7)
96
+ self.epi_warp_id = (8, 9, 10, 11)
97
+ self.threads_per_cta = 32 * 12
98
+
99
+ self.input_stages = 2
100
+ self.h_stages = 2
101
+
102
+ self.simt_sync_barrier = pipeline.NamedBarrier(
103
+ barrier_id=1, num_threads=32 * len(self.simt_warp_id)
104
+ )
105
+ self.epi_sync_barrier = pipeline.NamedBarrier(
106
+ barrier_id=3, num_threads=32 * len(self.epi_warp_id)
107
+ )
108
+ self.tmem_dealloc_sync_barrier = pipeline.NamedBarrier(
109
+ barrier_id=2, num_threads=self.threads_per_cta
110
+ )
111
+ self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_100")
112
+
113
+ # ---------------------------------------------------------------------------------
114
+
115
+ def _make_tiled_mmas(self):
116
+ io, acc, grp = self.io_dtype, self.acc_dtype, self.cta_group
117
+ mma_wh = sm100_utils.make_trivial_tiled_mma(
118
+ io, tcgen05.OperandMajorMode("k"), tcgen05.OperandMajorMode("k"),
119
+ acc, grp, self.tile_wh[:2], tcgen05.OperandSource.SMEM,
120
+ )
121
+ # DQ: A = do [BT, BV] k-major; B = h as [K, BV] mn-major — the SAME smem bytes as
122
+ # WH's [BV, K] k-major B operand (both are "K-index fastest"), checked below.
123
+ mma_dq = sm100_utils.make_trivial_tiled_mma(
124
+ io, tcgen05.OperandMajorMode("k"), tcgen05.OperandMajorMode("mn"),
125
+ acc, grp, self.tile_dq[:2], tcgen05.OperandSource.SMEM,
126
+ )
127
+ mma_dh = sm100_utils.make_trivial_tiled_mma(
128
+ io, tcgen05.OperandMajorMode("k"), tcgen05.OperandMajorMode("mn"),
129
+ acc, grp, self.tile_dh[:2], tcgen05.OperandSource.SMEM,
130
+ )
131
+ return mma_wh, mma_dq, mma_dh
132
+
133
+ def _setup_attributes(self):
134
+ mma_wh, mma_dq, mma_dh = self._make_tiled_mmas()
135
+ BT, K, BV = self.BT, self.K, self.BV
136
+
137
+ self.w_smem_layout = sm100_utils.make_smem_layout_a(
138
+ mma_wh, self.tile_wh, self.io_dtype, self.input_stages
139
+ )
140
+ self.do_smem_layout = sm100_utils.make_smem_layout_a(
141
+ mma_dq, self.tile_dq, self.io_dtype, self.input_stages
142
+ )
143
+ self.kgt_smem_layout = sm100_utils.make_smem_layout_b(
144
+ mma_dh, self.tile_dh, self.io_dtype, self.input_stages
145
+ )
146
+ # u is SIMT-only: plain row-major (BT, BV), BV contiguous
147
+ self.u_smem_layout = cute.make_layout(
148
+ (BT, BV, self.input_stages), stride=(BV, 1, BT * BV)
149
+ )
150
+ # gd: the per-chunk decay K-vector (g2's last row), fp32
151
+ self.gd_smem_layout = cute.make_layout((K, self.input_stages))
152
+
153
+ # h as B operand of WH ([BV, K] k-major, staged); written by SIMT through the
154
+ # ROW_MAJOR epi view; ALSO read by the DQ MMA through the mn-major [K, BV] view
155
+ # of the same bytes.
156
+ self.h_smem_layout = sm100_utils.make_smem_layout_b(
157
+ mma_wh, self.tile_wh, self.io_dtype, self.h_stages
158
+ )
159
+ self.hdq_smem_layout = sm100_utils.make_smem_layout_b(
160
+ mma_dq, self.tile_dq, self.io_dtype, self.h_stages
161
+ )
162
+ self.h_epi_layout = sm100_utils.make_smem_layout_epi(
163
+ self.io_dtype, utils.LayoutEnum.ROW_MAJOR, (BV, K), self.h_stages
164
+ )
165
+ # h checkpoint staging: TMA cannot transpose; the V-contiguous gmem slice gets a
166
+ # COL_MAJOR (BV, K) buffer filled by a transpose stmatrix (gdn 003 record 004).
167
+ self.cks_smem_layout = sm100_utils.make_smem_layout_epi(
168
+ self.io_dtype, utils.LayoutEnum.COL_MAJOR, (BV, K), 1
169
+ )
170
+ # v' as A operand of DH ([BV, BT] k-major); written via transpose stmatrix
171
+ # through the COL_MAJOR (BT, BV) epi view of the same bytes.
172
+ self.vpd_smem_layout = sm100_utils.make_smem_layout_a(
173
+ mma_dh, self.tile_dh, self.io_dtype, 1
174
+ )
175
+ self.vp_epi_layout = sm100_utils.make_smem_layout_epi(
176
+ self.io_dtype, utils.LayoutEnum.COL_MAJOR, (BT, BV), 1
177
+ )
178
+ # v_new store staging: (BT, BV) BV-contiguous to match gmem
179
+ self.vns_smem_layout = cute.make_layout((BT, BV, 1), stride=(BV, 1, BT * BV))
180
+ # dq reduce staging: (BT, K) fp32, K contiguous to match gmem
181
+ self.dqs_smem_layout = cute.make_layout((BT, K, 1), stride=(K, 1, BT * K))
182
+
183
+ self.num_mma_load_bytes = (
184
+ cute.size_in_bytes(
185
+ self.io_dtype, cute.slice_(self.w_smem_layout, (None, None, None, 0))
186
+ )
187
+ + cute.size_in_bytes(
188
+ self.io_dtype, cute.slice_(self.kgt_smem_layout, (None, None, None, 0))
189
+ )
190
+ )
191
+ if self.enable_dq:
192
+ self.num_mma_load_bytes += cute.size_in_bytes(
193
+ self.io_dtype, cute.slice_(self.do_smem_layout, (None, None, None, 0))
194
+ )
195
+ self.num_simt_load_bytes = (
196
+ cute.size_in_bytes(
197
+ self.io_dtype, cute.slice_(self.u_smem_layout, (None, None, 0))
198
+ )
199
+ + cute.size_in_bytes(
200
+ cutlass.Float32, cute.slice_(self.gd_smem_layout, (None, 0))
201
+ )
202
+ )
203
+
204
+ (
205
+ self.tmem_wh_offset,
206
+ self.tmem_dq_offset,
207
+ self.tmem_dh_offset,
208
+ self.num_tmem_cols,
209
+ ) = self._plan_tmem(mma_wh, mma_dq, mma_dh)
210
+
211
+ def _plan_tmem(self, mma_wh, mma_dq, mma_dh):
212
+ def acc_cols(mma, tile):
213
+ shape = mma.partition_shape_C(tile[:2])
214
+ fake = mma.make_fragment_C(cute.append(shape, 1))
215
+ return tcgen05.find_tmem_tensor_col_offset(fake)
216
+
217
+ wh = acc_cols(mma_wh, self.tile_wh)
218
+ dq = acc_cols(mma_dq, self.tile_dq)
219
+ dh = acc_cols(mma_dh, self.tile_dh)
220
+ off_wh = 0
221
+ off_dq = off_wh + wh
222
+ off_dh = off_dq + dq
223
+ total_ = off_dh + dh
224
+ total = 1
225
+ while total < total_:
226
+ total *= 2
227
+ assert total <= 512, f"tmem overflow: {total_} cols"
228
+ return off_wh, off_dq, off_dh, total
229
+
230
+ def _check_h_alias(self):
231
+ """The DQ MMA reads h through hdq_smem_layout but the bytes are written once,
232
+ through h_smem_layout's epi view. (BV, K) k-major and (K, BV) mn-major are both
233
+ "K-index fastest" 256B-row swizzled layouts, so the physical mapping should
234
+ coincide — this checks the cheap invariants (swizzle atom, footprint); the
235
+ authoritative check is numerical: dbg_bwd's forced-cute lane, where a wrong
236
+ alias scrambles dq by O(1) (the atom-folded coordinate profiles make an
237
+ element-wise crd2idx comparison impossible at trace time)."""
238
+ lh = self.h_smem_layout # ((BV, K) k-major, stages) composed w/ swizzle
239
+ lq = self.hdq_smem_layout # ((K, BV) mn-major, stages)
240
+ if str(lh.inner) != str(lq.inner) or cute.cosize(lh) != cute.cosize(lq):
241
+ raise ValueError(
242
+ f"h alias mismatch: {lh} vs {lq} — give DQ its own staging buffer "
243
+ "(see ALGORITHM.md B1 design)"
244
+ )
245
+
246
+ # ---------------------------------------------------------------------------------
247
+
248
+ @cute.jit
249
+ def __call__(
250
+ self,
251
+ w: cute.Tensor, # (T, K, HV, B)
252
+ kgt: cute.Tensor, # (K, T, HV, B) — same storage as kg
253
+ do: cute.Tensor, # (T, V, HV, B)
254
+ u: cute.Tensor, # (T, V, HV, B)
255
+ gd: cute.Tensor, # (K, NT, HV, B) fp32
256
+ h0: cute.Tensor, # (K, V, HV, B) fp32
257
+ vnew: cute.Tensor, # (T, V, HV, B) out
258
+ hck: cute.Tensor, # (V, K, NT, HV, B) out, io dtype
259
+ dq: cute.Tensor, # (T, K, HV, B) fp32 out — ZEROED by the caller, reduce-add
260
+ stream: cuda.CUstream,
261
+ ):
262
+ self._setup_attributes()
263
+ self._check_h_alias()
264
+ mma_wh, mma_dq, mma_dh = self._make_tiled_mmas()
265
+ BT, K, BV = self.BT, self.K, self.BV
266
+ cluster_vmnk = (1, 1, 1, 1)
267
+
268
+ tma_w, tma_tensor_w = cute.nvgpu.make_tiled_tma_atom_A(
269
+ cpasync.CopyBulkTensorTileG2SOp(), w,
270
+ cute.slice_(self.w_smem_layout, (None, None, None, 0)),
271
+ self.tile_wh, mma_wh, cluster_vmnk,
272
+ )
273
+ tma_do, tma_tensor_do = cute.nvgpu.make_tiled_tma_atom_A(
274
+ cpasync.CopyBulkTensorTileG2SOp(), do,
275
+ cute.slice_(self.do_smem_layout, (None, None, None, 0)),
276
+ self.tile_dq, mma_dq, cluster_vmnk,
277
+ )
278
+ tma_kgt, tma_tensor_kgt = cute.nvgpu.make_tiled_tma_atom_B(
279
+ cpasync.CopyBulkTensorTileG2SOp(), kgt,
280
+ cute.slice_(self.kgt_smem_layout, (None, None, None, 0)),
281
+ self.tile_dh, mma_dh, cluster_vmnk,
282
+ )
283
+ tma_u, tma_tensor_u = cpasync.make_tiled_tma_atom(
284
+ cpasync.CopyBulkTensorTileG2SOp(), u,
285
+ cute.slice_(self.u_smem_layout, (None, None, 0)),
286
+ (BT, BV),
287
+ )
288
+ gd_cta_v_layout = cute.slice_(
289
+ cute.make_identity_layout(gd.shape), (None, 0, 0, 0)
290
+ )
291
+ tma_gd, tma_tensor_gd = cpasync.make_tiled_tma_atom(
292
+ cpasync.CopyBulkTensorTileG2SOp(), gd,
293
+ cute.slice_(self.gd_smem_layout, (None, 0)),
294
+ gd_cta_v_layout,
295
+ )
296
+ tma_vn, tma_tensor_vn = cpasync.make_tiled_tma_atom(
297
+ cpasync.CopyBulkTensorTileS2GOp(), vnew,
298
+ cute.slice_(self.vns_smem_layout, (None, None, 0)),
299
+ (BT, BV),
300
+ )
301
+ tma_ck, tma_tensor_ck = cpasync.make_tiled_tma_atom(
302
+ cpasync.CopyBulkTensorTileS2GOp(), hck,
303
+ cute.slice_(self.cks_smem_layout, (None, None, 0)),
304
+ (BV, K),
305
+ )
306
+ tma_dq, tma_tensor_dq = cpasync.make_tiled_tma_atom(
307
+ cpasync.CopyReduceBulkTensorTileS2GOp(), dq,
308
+ cute.slice_(self.dqs_smem_layout, (None, None, 0)),
309
+ (BT, K),
310
+ )
311
+
312
+ B = cute.size(w, mode=[3])
313
+ HV = cute.size(w, mode=[2])
314
+ NV = cute.size(u, mode=[1]) // BV
315
+ grid = (B * HV * NV, 1, 1)
316
+
317
+ swz_align, lin_align = 1024, 128
318
+
319
+ # Every `*_full` range backs both halves of a pipeline's mbarrier array —
320
+ # 2 * num_stages Int64s. Under-sizing aliases the next pipeline and only
321
+ # deadlocks once the pipe wraps.
322
+ @cute.struct
323
+ class SharedStorage:
324
+ mmain_full: cute.struct.MemRange[cutlass.Int64, self.input_stages * 2] # type: ignore
325
+ simtin_full: cute.struct.MemRange[cutlass.Int64, self.input_stages * 2] # type: ignore
326
+ h_full: cute.struct.MemRange[cutlass.Int64, self.h_stages * 2] # type: ignore
327
+ vpd_full: cute.struct.MemRange[cutlass.Int64, 2] # type: ignore
328
+ wh_full: cute.struct.MemRange[cutlass.Int64, 2] # type: ignore
329
+ dqf_full: cute.struct.MemRange[cutlass.Int64, 2] # type: ignore
330
+ dh_full: cute.struct.MemRange[cutlass.Int64, 2] # type: ignore
331
+ tmem_holding_buf: cutlass.Int32
332
+ smem_w: cute.struct.Align[
333
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.w_smem_layout)], swz_align # type: ignore
334
+ ]
335
+ smem_do: cute.struct.Align[
336
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.do_smem_layout)], swz_align # type: ignore
337
+ ]
338
+ smem_kgt: cute.struct.Align[
339
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.kgt_smem_layout)], swz_align # type: ignore
340
+ ]
341
+ smem_u: cute.struct.Align[
342
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.u_smem_layout)], lin_align # type: ignore
343
+ ]
344
+ smem_gd: cute.struct.Align[
345
+ cute.struct.MemRange[cutlass.Float32, cute.cosize(self.gd_smem_layout)], lin_align # type: ignore
346
+ ]
347
+ smem_h: cute.struct.Align[
348
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.h_smem_layout)], swz_align # type: ignore
349
+ ]
350
+ smem_cks: cute.struct.Align[
351
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.cks_smem_layout)], swz_align # type: ignore
352
+ ]
353
+ smem_vpd: cute.struct.Align[
354
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.vpd_smem_layout)], swz_align # type: ignore
355
+ ]
356
+ smem_vns: cute.struct.Align[
357
+ cute.struct.MemRange[self.io_dtype, cute.cosize(self.vns_smem_layout)], lin_align # type: ignore
358
+ ]
359
+ smem_dqs: cute.struct.Align[
360
+ cute.struct.MemRange[cutlass.Float32, cute.cosize(self.dqs_smem_layout)], lin_align # type: ignore
361
+ ]
362
+
363
+ self.shared_storage = SharedStorage
364
+ if cutlass.const_expr(self.shared_storage.size_in_bytes() > self.smem_capacity):
365
+ raise ValueError(
366
+ f"smem {self.shared_storage.size_in_bytes()} > {self.smem_capacity}"
367
+ )
368
+
369
+ self.kda_cute_b1(
370
+ tma_w, tma_tensor_w,
371
+ tma_do, tma_tensor_do,
372
+ tma_kgt, tma_tensor_kgt,
373
+ tma_u, tma_tensor_u,
374
+ tma_gd, tma_tensor_gd,
375
+ tma_vn, tma_tensor_vn,
376
+ tma_ck, tma_tensor_ck,
377
+ tma_dq, tma_tensor_dq,
378
+ h0,
379
+ ).launch(grid=grid, block=[self.threads_per_cta, 1, 1], stream=stream)
380
+
381
+ # ---------------------------------------------------------------------------------
382
+
383
+ @cute.kernel
384
+ def kda_cute_b1(
385
+ self,
386
+ tma_w: cute.CopyAtom, mW: cute.Tensor,
387
+ tma_do: cute.CopyAtom, mDO: cute.Tensor,
388
+ tma_kgt: cute.CopyAtom, mKGT: cute.Tensor,
389
+ tma_u: cute.CopyAtom, mU: cute.Tensor,
390
+ tma_gd: cute.CopyAtom, mGd: cute.Tensor,
391
+ tma_vn: cute.CopyAtom, mVN: cute.Tensor,
392
+ tma_ck: cute.CopyAtom, mCK: cute.Tensor,
393
+ tma_dq: cute.CopyAtom, mDQ: cute.Tensor,
394
+ mH0: cute.Tensor,
395
+ ):
396
+ BT, K, BV = self.BT, self.K, self.BV
397
+ io = self.io_dtype
398
+ f32 = self.acc_dtype
399
+ # Layouts/TiledMma from the host trace cannot cross the region boundary — rebuild.
400
+ self._setup_attributes()
401
+ mma_wh, mma_dq, mma_dh = self._make_tiled_mmas()
402
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
403
+ tidx, _, _ = cute.arch.thread_idx()
404
+ local_tidx = tidx % 128
405
+
406
+ if warp_idx == self.tma_warp_id:
407
+ for atom in [tma_w, tma_do, tma_kgt, tma_u, tma_gd, tma_vn, tma_ck, tma_dq]:
408
+ cpasync.prefetch_descriptor(atom)
409
+
410
+ bidx, _, _ = cute.arch.block_idx()
411
+ HV = cute.size(mW, mode=[2])
412
+ NV = cute.size(mU, mode=[1]) // BV
413
+ T = cute.size(mW, mode=[0])
414
+ NT = T // BT
415
+ v_idx = bidx % NV
416
+ hv_idx = (bidx // NV) % HV
417
+ b_idx = bidx // (NV * HV)
418
+
419
+ smem = utils.SmemAllocator()
420
+ storage = smem.allocate(self.shared_storage)
421
+
422
+ sW = storage.smem_w.get_tensor(self.w_smem_layout.outer, swizzle=self.w_smem_layout.inner)
423
+ sDO = storage.smem_do.get_tensor(self.do_smem_layout.outer, swizzle=self.do_smem_layout.inner)
424
+ sKGT = storage.smem_kgt.get_tensor(self.kgt_smem_layout.outer, swizzle=self.kgt_smem_layout.inner)
425
+ sU = storage.smem_u.get_tensor(self.u_smem_layout)
426
+ sGd = storage.smem_gd.get_tensor(self.gd_smem_layout)
427
+ sH = storage.smem_h.get_tensor(self.h_smem_layout.outer, swizzle=self.h_smem_layout.inner)
428
+ sHdq = storage.smem_h.get_tensor(self.hdq_smem_layout.outer, swizzle=self.hdq_smem_layout.inner)
429
+ sH_epi = storage.smem_h.get_tensor(self.h_epi_layout.outer, swizzle=self.h_epi_layout.inner)
430
+ sCKS = storage.smem_cks.get_tensor(self.cks_smem_layout.outer, swizzle=self.cks_smem_layout.inner)
431
+ sVpd = storage.smem_vpd.get_tensor(self.vpd_smem_layout.outer, swizzle=self.vpd_smem_layout.inner)
432
+ sVpd_epi = storage.smem_vpd.get_tensor(self.vp_epi_layout.outer, swizzle=self.vp_epi_layout.inner)
433
+ sVNS = storage.smem_vns.get_tensor(self.vns_smem_layout)
434
+ sDQS = storage.smem_dqs.get_tensor(self.dqs_smem_layout)
435
+
436
+ # ---- pipelines ----
437
+ simt_threads = 32 * len(self.simt_warp_id)
438
+ epi_threads = 32 * len(self.epi_warp_id)
439
+ mmain_pipe = pipeline.PipelineTmaUmma.create(
440
+ num_stages=self.input_stages,
441
+ producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
442
+ consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
443
+ tx_count=self.num_mma_load_bytes,
444
+ barrier_storage=storage.mmain_full.data_ptr(),
445
+ defer_sync=True,
446
+ )
447
+ # Not PipelineTmaAsync — see kernel_fwd.py (consumer_release arrives from lane 0
448
+ # only vs the full-count empty barrier; producer_tail deadlocks).
449
+ simtin_pipe = pipeline.PipelineTmaMultiConsumersAsync.create(
450
+ num_stages=self.input_stages,
451
+ producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
452
+ consumer_group_umma=pipeline.CooperativeGroup(pipeline.Agent.Thread),
453
+ consumer_group_async=pipeline.CooperativeGroup(
454
+ pipeline.Agent.Thread, simt_threads
455
+ ),
456
+ tx_count=self.num_simt_load_bytes,
457
+ barrier_storage=storage.simtin_full.data_ptr(),
458
+ defer_sync=True,
459
+ )
460
+
461
+ def make_simt_to_mma_pipe(ptr, stages, producer_threads):
462
+ return pipeline.PipelineAsyncUmma.create(
463
+ num_stages=stages,
464
+ producer_group=pipeline.CooperativeGroup(
465
+ pipeline.Agent.Thread, producer_threads
466
+ ),
467
+ consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
468
+ barrier_storage=ptr,
469
+ defer_sync=True,
470
+ )
471
+
472
+ def make_mma_to_simt_pipe(ptr, consumer_threads):
473
+ return pipeline.PipelineUmmaAsync.create(
474
+ num_stages=1,
475
+ producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
476
+ consumer_group=pipeline.CooperativeGroup(
477
+ pipeline.Agent.Thread, consumer_threads
478
+ ),
479
+ barrier_storage=ptr,
480
+ defer_sync=True,
481
+ )
482
+
483
+ h_pipe = make_simt_to_mma_pipe(storage.h_full.data_ptr(), self.h_stages, simt_threads)
484
+ vpd_pipe = make_simt_to_mma_pipe(storage.vpd_full.data_ptr(), 1, simt_threads)
485
+ wh_pipe = make_mma_to_simt_pipe(storage.wh_full.data_ptr(), simt_threads)
486
+ dqf_pipe = make_mma_to_simt_pipe(storage.dqf_full.data_ptr(), epi_threads)
487
+ dh_pipe = make_mma_to_simt_pipe(storage.dh_full.data_ptr(), simt_threads)
488
+
489
+ pipeline_init_arrive(cluster_shape_mn=(1, 1, 1), is_relaxed=True)
490
+ pipeline_init_wait(cluster_shape_mn=(1, 1, 1))
491
+
492
+ # ---- tmem ----
493
+ tmem_alloc_barrier = pipeline.NamedBarrier(
494
+ barrier_id=0, num_threads=self.threads_per_cta
495
+ )
496
+ tmem = utils.TmemAllocator(
497
+ storage.tmem_holding_buf.ptr,
498
+ barrier_for_retrieve=tmem_alloc_barrier,
499
+ allocator_warp_id=self.simt_warp_id[0],
500
+ )
501
+ tmem.allocate(self.num_tmem_cols)
502
+ tmem.wait_for_alloc()
503
+ tmem_ptr_base = tmem.retrieve_ptr(self.acc_dtype)
504
+
505
+ def acc_tensor(mma, tile, offset):
506
+ shape = mma.partition_shape_C(tile[:2])
507
+ fake = mma.make_fragment_C(cute.append(shape, 1))
508
+ return cute.make_tensor(tmem_ptr_base + offset, fake.layout)
509
+
510
+ tWH = acc_tensor(mma_wh, self.tile_wh, self.tmem_wh_offset)
511
+ tDQ = acc_tensor(mma_dq, self.tile_dq, self.tmem_dq_offset)
512
+ tDH = acc_tensor(mma_dh, self.tile_dh, self.tmem_dh_offset)
513
+
514
+ # ---- global tiles (per (b, hv, v_idx), open over chunks) ----
515
+ gW = cute.local_tile(mW, (BT, K), (None, 0, hv_idx, b_idx)) # (BT,K,NT)
516
+ gDO = cute.local_tile(mDO, (BT, BV), (None, v_idx, hv_idx, b_idx)) # (BT,BV,NT)
517
+ gKGT = cute.local_tile(mKGT, (K, BT), (0, None, hv_idx, b_idx)) # (K,BT,NT)
518
+ gU = cute.local_tile(mU, (BT, BV), (None, v_idx, hv_idx, b_idx)) # (BT,BV,NT)
519
+ gGd = mGd[(None, None, hv_idx, b_idx)] # (K, NT)
520
+ gVN = cute.local_tile(mVN, (BT, BV), (None, v_idx, hv_idx, b_idx))
521
+ gCK = cute.local_tile(mCK, (BV, K), (v_idx, 0, None, hv_idx, b_idx)) # (BV,K,NT)
522
+ gDQ = cute.local_tile(mDQ, (BT, K), (None, 0, hv_idx, b_idx)) # (BT,K,NT)
523
+
524
+ # ==========================================================================
525
+ # TMA warp
526
+ # ==========================================================================
527
+ if warp_idx == self.tma_warp_id:
528
+ thr_mma_wh = mma_wh.get_slice(0)
529
+ thr_mma_dq = mma_dq.get_slice(0)
530
+ thr_mma_dh = mma_dh.get_slice(0)
531
+
532
+ tW_mma = thr_mma_wh.partition_A(gW)
533
+ tDO_mma = thr_mma_dq.partition_A(gDO)
534
+ tKGT_mma = thr_mma_dh.partition_B(gKGT)
535
+
536
+ cta1 = cute.make_layout(1)
537
+ tWs, tWg = cpasync.tma_partition(
538
+ tma_w, 0, cta1, cute.group_modes(sW, 0, 3), cute.group_modes(tW_mma, 0, 3)
539
+ )
540
+ tDOs, tDOg = cpasync.tma_partition(
541
+ tma_do, 0, cta1, cute.group_modes(sDO, 0, 3), cute.group_modes(tDO_mma, 0, 3)
542
+ )
543
+ tKGTs, tKGTg = cpasync.tma_partition(
544
+ tma_kgt, 0, cta1, cute.group_modes(sKGT, 0, 3), cute.group_modes(tKGT_mma, 0, 3)
545
+ )
546
+ tUs, tUg = cpasync.tma_partition(
547
+ tma_u, 0, cta1, cute.group_modes(sU, 0, 2), cute.group_modes(gU, 0, 2)
548
+ )
549
+ tGds, tGdg = cpasync.tma_partition(
550
+ tma_gd, 0, cta1, cute.group_modes(sGd, 0, 1), cute.group_modes(gGd, 0, 1)
551
+ )
552
+
553
+ mmain_producer = pipeline.make_pipeline_state(
554
+ pipeline.PipelineUserType.Producer, self.input_stages
555
+ )
556
+ simtin_producer = pipeline.make_pipeline_state(
557
+ pipeline.PipelineUserType.Producer, self.input_stages
558
+ )
559
+
560
+ for c in cutlass.range(NT, unroll=1):
561
+ mmain_pipe.producer_acquire(mmain_producer)
562
+ bar = mmain_pipe.producer_get_barrier(mmain_producer)
563
+ cute.copy(tma_w, tWg[None, c], tWs[None, mmain_producer.index], tma_bar_ptr=bar)
564
+ if cutlass.const_expr(self.enable_dq):
565
+ cute.copy(tma_do, tDOg[None, c], tDOs[None, mmain_producer.index], tma_bar_ptr=bar)
566
+ cute.copy(tma_kgt, tKGTg[None, c], tKGTs[None, mmain_producer.index], tma_bar_ptr=bar)
567
+ mmain_producer.advance()
568
+
569
+ simtin_pipe.producer_acquire(simtin_producer)
570
+ sbar = simtin_pipe.producer_get_barrier(simtin_producer)
571
+ cute.copy(tma_u, tUg[None, c], tUs[None, simtin_producer.index], tma_bar_ptr=sbar)
572
+ cute.copy(tma_gd, tGdg[None, c], tGds[None, simtin_producer.index], tma_bar_ptr=sbar)
573
+ simtin_producer.advance()
574
+
575
+ mmain_pipe.producer_tail(mmain_producer)
576
+ simtin_pipe.producer_tail(simtin_producer)
577
+
578
+ # ==========================================================================
579
+ # MMA warp
580
+ # ==========================================================================
581
+ elif warp_idx == self.mma_warp_id:
582
+ tCrW = mma_wh.make_fragment_A(sW)
583
+ tCrH = mma_wh.make_fragment_B(sH)
584
+ tCrDO = mma_dq.make_fragment_A(sDO)
585
+ tCrHdq = mma_dq.make_fragment_B(sHdq)
586
+ tCrKGT = mma_dh.make_fragment_B(sKGT)
587
+ tCrVpd = mma_dh.make_fragment_A(sVpd)
588
+
589
+ mmain_consumer = pipeline.make_pipeline_state(
590
+ pipeline.PipelineUserType.Consumer, self.input_stages
591
+ )
592
+ simtin_mma_consumer = pipeline.make_pipeline_state(
593
+ pipeline.PipelineUserType.Consumer, self.input_stages
594
+ )
595
+ h_consumer = pipeline.make_pipeline_state(
596
+ pipeline.PipelineUserType.Consumer, self.h_stages
597
+ )
598
+ vpd_consumer = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, 1)
599
+ wh_producer = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, 1)
600
+ dqf_producer = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, 1)
601
+ dh_producer = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, 1)
602
+
603
+ for c in cutlass.range(NT, unroll=1):
604
+ # WH = w h — first: it heads the critical path.
605
+ mmain_pipe.consumer_wait(mmain_consumer)
606
+ h_pipe.consumer_wait(h_consumer)
607
+ wh_pipe.producer_acquire(wh_producer)
608
+ for kk in cutlass.range(cute.size(tCrH, mode=[2]), unroll_full=True):
609
+ mma_wh.set(tcgen05.Field.ACCUMULATE, cutlass.Boolean(kk != 0))
610
+ cute.gemm(
611
+ mma_wh, tWH[None, None, None, 0],
612
+ tCrW[None, None, kk, mmain_consumer.index],
613
+ tCrH[None, None, kk, h_consumer.index],
614
+ tWH[None, None, None, 0],
615
+ )
616
+ wh_pipe.producer_commit(wh_producer)
617
+ wh_producer.advance()
618
+
619
+ # DQ = do h^T — h's second (and last) consumer this chunk.
620
+ if cutlass.const_expr(self.enable_dq):
621
+ dqf_pipe.producer_acquire(dqf_producer)
622
+ for kk in cutlass.range(cute.size(tCrDO, mode=[2]), unroll_full=True):
623
+ mma_dq.set(tcgen05.Field.ACCUMULATE, cutlass.Boolean(kk != 0))
624
+ cute.gemm(
625
+ mma_dq, tDQ[None, None, None, 0],
626
+ tCrDO[None, None, kk, mmain_consumer.index],
627
+ tCrHdq[None, None, kk, h_consumer.index],
628
+ tDQ[None, None, None, 0],
629
+ )
630
+ dqf_pipe.producer_commit(dqf_producer)
631
+ dqf_producer.advance()
632
+ h_pipe.consumer_release(h_consumer)
633
+ h_consumer.advance()
634
+
635
+ # DH = kg^T v'
636
+ vpd_pipe.consumer_wait(vpd_consumer)
637
+ dh_pipe.producer_acquire(dh_producer)
638
+ for kk in cutlass.range(cute.size(tCrVpd, mode=[2]), unroll_full=True):
639
+ mma_dh.set(tcgen05.Field.ACCUMULATE, cutlass.Boolean(kk != 0))
640
+ cute.gemm(
641
+ mma_dh, tDH[None, None, None, 0],
642
+ tCrVpd[None, None, kk, vpd_consumer.index],
643
+ tCrKGT[None, None, kk, mmain_consumer.index],
644
+ tDH[None, None, None, 0],
645
+ )
646
+ dh_pipe.producer_commit(dh_producer)
647
+ dh_producer.advance()
648
+ vpd_pipe.consumer_release(vpd_consumer)
649
+ vpd_consumer.advance()
650
+ # kgt was this stage's last user
651
+ mmain_pipe.consumer_release(mmain_consumer)
652
+ mmain_consumer.advance()
653
+ # umma half of simtin's empty arrive (data untouched by this warp)
654
+ simtin_pipe.consumer_wait(simtin_mma_consumer)
655
+ simtin_pipe.consumer_release(
656
+ simtin_mma_consumer, pipeline.PipelineOp.TCGen05Mma
657
+ )
658
+ simtin_mma_consumer.advance()
659
+
660
+ wh_pipe.producer_tail(wh_producer)
661
+ dqf_pipe.producer_tail(dqf_producer)
662
+ dh_pipe.producer_tail(dh_producer)
663
+
664
+ # ==========================================================================
665
+ # SIMT recurrence warps 4..7: v' from WH, the h update from DH, h/v_new stores.
666
+ # ==========================================================================
667
+ elif (
668
+ warp_idx == self.simt_warp_id[0]
669
+ or warp_idx == self.simt_warp_id[1]
670
+ or warp_idx == self.simt_warp_id[2]
671
+ or warp_idx == self.simt_warp_id[3]
672
+ ):
673
+ t2r_64_atom = cute.make_copy_atom(
674
+ tcgen05.Ld16x256bOp(tcgen05.Repetition(8), tcgen05.Pack.NONE), f32
675
+ )
676
+ f32_cp_atom = cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), f32)
677
+ io_cp_atom = cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), io)
678
+
679
+ # --- WH -> v' ---
680
+ tWH_2d = tWH[((None, None), 0, 0, None)]
681
+ tiled_t2r_wh = tcgen05.make_tmem_copy(t2r_64_atom, tWH_2d[None, None, 0])
682
+ thr_t2r_wh = tiled_t2r_wh.get_slice(local_tidx)
683
+ tTR_tWH = thr_t2r_wh.partition_S(tWH_2d)
684
+ # rmem operands of a tmem copy must be sized from the D partition of a
685
+ # non-tmem tensor — partition_S folds the lane mode and oversizes.
686
+ tTR_rWH = cute.make_rmem_tensor(
687
+ thr_t2r_wh.partition_D(cute.make_identity_tensor((BT, BV))).shape, f32
688
+ )
689
+ tWHsU = thr_t2r_wh.partition_D(sU)
690
+ tWHrU = cute.make_rmem_tensor(
691
+ cute.slice_(tWHsU.shape, (None, None, None, 0)), io
692
+ )
693
+ r2s_x16t_atom = cute.make_copy_atom(
694
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=True, num_matrices=4), io
695
+ )
696
+ tiled_r2s_vpd = cute.make_tiled_copy_D(r2s_x16t_atom, tiled_t2r_wh)
697
+ thr_r2s_vpd = tiled_r2s_vpd.get_slice(local_tidx)
698
+ tRS_sVpd = thr_r2s_vpd.partition_D(sVpd_epi)
699
+ tRS_rVpd = cute.make_rmem_tensor(
700
+ cute.slice_(tRS_sVpd.shape, (None, None, None, 0)), io
701
+ )
702
+ r2s_x16_atom = cute.make_copy_atom(
703
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=False, num_matrices=4), io
704
+ )
705
+ tiled_r2s_vns = cute.make_tiled_copy_D(r2s_x16_atom, tiled_t2r_wh)
706
+ thr_r2s_vns = tiled_r2s_vns.get_slice(local_tidx)
707
+ tRS_sVNS = thr_r2s_vns.partition_D(sVNS)
708
+ tRS_rVNS = cute.make_rmem_tensor(
709
+ cute.slice_(tRS_sVNS.shape, (None, None, None, 0)), io
710
+ )
711
+
712
+ # --- DH -> h ---
713
+ tDH_2d = tDH[((None, None), 0, 0, None)]
714
+ t2r_128_atom = cute.make_copy_atom(
715
+ tcgen05.Ld16x256bOp(tcgen05.Repetition(8), tcgen05.Pack.NONE), f32
716
+ )
717
+ tiled_t2r_dh = tcgen05.make_tmem_copy(t2r_128_atom, tDH_2d[None, None, 0])
718
+ thr_t2r_dh = tiled_t2r_dh.get_slice(local_tidx)
719
+ tTR_tDH = thr_t2r_dh.partition_S(tDH_2d)
720
+ coordDH = thr_t2r_dh.partition_D(cute.make_identity_tensor((BV, K)))
721
+ tTR_rDH = cute.make_rmem_tensor(coordDH.shape, f32)
722
+ tHreg = cute.make_rmem_tensor(tTR_rDH.shape, f32)
723
+ # per-dim decay: broadcast the K-vector across the BV mode of the fragment
724
+ sGd_bcast = cute.make_tensor(
725
+ sGd.iterator,
726
+ cute.make_layout((BV, K, self.input_stages), stride=(0, 1, K)),
727
+ )
728
+ tDHsGd = thr_t2r_dh.partition_D(sGd_bcast)
729
+ tDHrGd = cute.make_rmem_tensor(
730
+ cute.slice_(tDHsGd.shape, (None, None, None, 0)), f32
731
+ )
732
+ r2s_h_atom = cute.make_copy_atom(
733
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=False, num_matrices=4), io
734
+ )
735
+ tiled_r2s_h = cute.make_tiled_copy_D(r2s_h_atom, tiled_t2r_dh)
736
+ thr_r2s_h = tiled_r2s_h.get_slice(local_tidx)
737
+ tRS_sH = thr_r2s_h.partition_D(sH_epi)
738
+ tRS_rH = cute.make_rmem_tensor(
739
+ cute.slice_(tRS_sH.shape, (None, None, None, 0)), io
740
+ )
741
+ # checkpoint staging: same values, transpose stmatrix into the BV-contiguous
742
+ # buffer the TMA store can express (gdn 003: TMA stores cannot transpose).
743
+ r2s_ck_atom = cute.make_copy_atom(
744
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=True, num_matrices=4), io
745
+ )
746
+ tiled_r2s_ck = cute.make_tiled_copy_D(r2s_ck_atom, tiled_t2r_dh)
747
+ thr_r2s_ck = tiled_r2s_ck.get_slice(local_tidx)
748
+ tRS_sCKS = thr_r2s_ck.partition_D(sCKS)
749
+ tRS_rCKS = cute.make_rmem_tensor(
750
+ cute.slice_(tRS_sCKS.shape, (None, None, None, 0)), io
751
+ )
752
+
753
+ # TMA store plumbing (v_new per chunk; h checkpoint per chunk)
754
+ bSG_sVNS, bSG_gVN = cpasync.tma_partition(
755
+ tma_vn, 0, cute.make_layout(1),
756
+ cute.group_modes(sVNS, 0, 2), cute.group_modes(gVN, 0, 2),
757
+ )
758
+ bSG_sCK, bSG_gCK = cpasync.tma_partition(
759
+ tma_ck, 0, cute.make_layout(1),
760
+ cute.group_modes(sCKS, 0, 2), cute.group_modes(gCK, 0, 2),
761
+ )
762
+ tma_store_pipeline = pipeline.PipelineTmaStore.create(
763
+ num_stages=1,
764
+ producer_group=pipeline.CooperativeGroup(
765
+ pipeline.Agent.Thread, simt_threads
766
+ ),
767
+ )
768
+
769
+ simtin_consumer = pipeline.make_pipeline_state(
770
+ pipeline.PipelineUserType.Consumer, self.input_stages
771
+ )
772
+ wh_consumer = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, 1)
773
+ dh_consumer = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, 1)
774
+ h_producer = pipeline.make_pipeline_state(
775
+ pipeline.PipelineUserType.Producer, self.h_stages
776
+ )
777
+ vpd_producer = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, 1)
778
+
779
+ # ---- h := h0; checkpoint 0 is exactly that value ----
780
+ for i in cutlass.range(cute.size(tHreg), unroll_full=True):
781
+ vv, kk = coordDH[i]
782
+ tHreg[i] = mH0[(kk, v_idx * BV + vv, hv_idx, b_idx)]
783
+ h_pipe.producer_acquire(h_producer)
784
+ for i in cutlass.range(cute.size(tHreg), unroll_full=True, vectorize=True):
785
+ vio = tHreg[i].to(io)
786
+ tRS_rH[i] = vio
787
+ tRS_rCKS[i] = vio
788
+ cute.copy(tiled_r2s_h, tRS_rH, tRS_sH[None, None, None, h_producer.index])
789
+ cute.copy(tiled_r2s_ck, tRS_rCKS, tRS_sCKS[None, None, None, 0])
790
+ cute.arch.fence_proxy("async.shared", space="cta")
791
+ self.simt_sync_barrier.arrive_and_wait()
792
+ # commit h BEFORE the checkpoint store: store completion stays off the
793
+ # recurrence critical path (gdn 003 record 006's deferral).
794
+ h_pipe.producer_commit(h_producer)
795
+ h_producer.advance()
796
+ if warp_idx == self.simt_warp_id[0]:
797
+ cute.copy(tma_ck, bSG_sCK[None, 0], bSG_gCK[None, 0])
798
+ tma_store_pipeline.producer_commit()
799
+
800
+ for c in cutlass.range(NT, unroll=1):
801
+ simtin_pipe.consumer_wait(simtin_consumer)
802
+ scrd = (None, None, None, simtin_consumer.index)
803
+ cute.copy(io_cp_atom, tWHsU[scrd], tWHrU)
804
+
805
+ # v' = u - WH (un-decayed: kg carries the exp2(G - g2) factor)
806
+ wh_pipe.consumer_wait(wh_consumer)
807
+ cute.copy(tiled_t2r_wh, tTR_tWH[None, None, None, 0], tTR_rWH)
808
+ cute.arch.fence_view_async_tmem_load()
809
+ wh_pipe.consumer_release(wh_consumer)
810
+ wh_consumer.advance()
811
+ vpd_pipe.producer_acquire(vpd_producer)
812
+ for i in cutlass.range(
813
+ cute.size(tTR_rWH), unroll_full=True, vectorize=True
814
+ ):
815
+ vp = (tWHrU[i].to(f32) - tTR_rWH[i]).to(io)
816
+ tRS_rVpd[i] = vp
817
+ tRS_rVNS[i] = vp
818
+ # the MMA operand first: it unblocks DH
819
+ cute.copy(tiled_r2s_vpd, tRS_rVpd, tRS_sVpd[None, None, None, 0])
820
+ cute.arch.fence_proxy("async.shared", space="cta")
821
+ self.simt_sync_barrier.arrive_and_wait()
822
+ vpd_pipe.producer_commit(vpd_producer)
823
+ vpd_producer.advance()
824
+ # acquire FIRST: waits on last chunk's stores (long complete), not the
825
+ # one about to be issued — sVNS/sCKS reuse stays safe via the barrier.
826
+ if warp_idx == self.simt_warp_id[0]:
827
+ tma_store_pipeline.producer_acquire()
828
+ self.simt_sync_barrier.arrive_and_wait()
829
+ cute.copy(tiled_r2s_vns, tRS_rVNS, tRS_sVNS[None, None, None, 0])
830
+ cute.arch.fence_proxy("async.shared", space="cta")
831
+ self.simt_sync_barrier.arrive_and_wait()
832
+ if warp_idx == self.simt_warp_id[0]:
833
+ cute.copy(tma_vn, bSG_sVNS[None, 0], bSG_gVN[None, c])
834
+ tma_store_pipeline.producer_commit()
835
+
836
+ # h update: per-dim decay — each fragment element's k picks its factor
837
+ cute.copy(f32_cp_atom, tDHsGd[scrd], tDHrGd)
838
+ dh_pipe.consumer_wait(dh_consumer)
839
+ cute.copy(tiled_t2r_dh, tTR_tDH[None, None, None, 0], tTR_rDH)
840
+ cute.arch.fence_view_async_tmem_load()
841
+ dh_pipe.consumer_release(dh_consumer)
842
+ dh_consumer.advance()
843
+ for i in cutlass.range(
844
+ cute.size(tHreg), unroll_full=True, vectorize=True
845
+ ):
846
+ dec = cute.math.exp2(tDHrGd[i], fastmath=True)
847
+ tHreg[i] = dec * tHreg[i] + tTR_rDH[i]
848
+ if c + 1 < NT:
849
+ h_pipe.producer_acquire(h_producer)
850
+ for i in cutlass.range(
851
+ cute.size(tHreg), unroll_full=True, vectorize=True
852
+ ):
853
+ vio = tHreg[i].to(io)
854
+ tRS_rH[i] = vio
855
+ tRS_rCKS[i] = vio
856
+ cute.copy(
857
+ tiled_r2s_h, tRS_rH, tRS_sH[None, None, None, h_producer.index]
858
+ )
859
+ cute.copy(tiled_r2s_ck, tRS_rCKS, tRS_sCKS[None, None, None, 0])
860
+ cute.arch.fence_proxy("async.shared", space="cta")
861
+ self.simt_sync_barrier.arrive_and_wait()
862
+ # h first — the checkpoint store is off the critical path
863
+ h_pipe.producer_commit(h_producer)
864
+ h_producer.advance()
865
+ if warp_idx == self.simt_warp_id[0]:
866
+ cute.copy(tma_ck, bSG_sCK[None, 0], bSG_gCK[None, c + 1])
867
+ tma_store_pipeline.producer_commit()
868
+
869
+ simtin_pipe.consumer_release(
870
+ simtin_consumer, pipeline.PipelineOp.AsyncThread
871
+ )
872
+ simtin_consumer.advance()
873
+
874
+ tma_store_pipeline.producer_tail()
875
+
876
+ # ==========================================================================
877
+ # SIMT epilogue warps 8..11: DQ readout -> fp32 staging -> TMA reduce-add.
878
+ # Off the critical path — trails the recurrence without stalling it.
879
+ # ==========================================================================
880
+ elif (
881
+ warp_idx == self.epi_warp_id[0]
882
+ or warp_idx == self.epi_warp_id[1]
883
+ or warp_idx == self.epi_warp_id[2]
884
+ or warp_idx == self.epi_warp_id[3]
885
+ ):
886
+ t2r_dq_atom = cute.make_copy_atom(
887
+ tcgen05.Ld16x256bOp(tcgen05.Repetition(8), tcgen05.Pack.NONE), f32
888
+ )
889
+ f32_cp_atom = cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), f32)
890
+
891
+ tDQ_2d = tDQ[((None, None), 0, 0, None)]
892
+ tiled_t2r_dq = tcgen05.make_tmem_copy(t2r_dq_atom, tDQ_2d[None, None, 0])
893
+ thr_t2r_dq = tiled_t2r_dq.get_slice(local_tidx)
894
+ tTR_tDQ = thr_t2r_dq.partition_S(tDQ_2d)
895
+ tTR_rDQ = cute.make_rmem_tensor(
896
+ thr_t2r_dq.partition_D(cute.make_identity_tensor((BT, K))).shape, f32
897
+ )
898
+ tDQsS = thr_t2r_dq.partition_D(sDQS)
899
+
900
+ bSG_sDQS, bSG_gDQ = cpasync.tma_partition(
901
+ tma_dq, 0, cute.make_layout(1),
902
+ cute.group_modes(sDQS, 0, 2), cute.group_modes(gDQ, 0, 2),
903
+ )
904
+ dq_store_pipeline = pipeline.PipelineTmaStore.create(
905
+ num_stages=1,
906
+ producer_group=pipeline.CooperativeGroup(
907
+ pipeline.Agent.Thread, epi_threads
908
+ ),
909
+ )
910
+
911
+ dqf_consumer = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, 1)
912
+
913
+ for c in cutlass.range(NT if cutlass.const_expr(self.enable_dq) else 0, unroll=1):
914
+ dqf_pipe.consumer_wait(dqf_consumer)
915
+ cute.copy(tiled_t2r_dq, tTR_tDQ[None, None, None, 0], tTR_rDQ)
916
+ cute.arch.fence_view_async_tmem_load()
917
+ dqf_pipe.consumer_release(dqf_consumer)
918
+ dqf_consumer.advance()
919
+ # acquire FIRST (deferral): waits on chunk c-1's reduce, protecting the
920
+ # sDQS overwrite below; the fresh reduce's completion never blocks us.
921
+ if warp_idx == self.epi_warp_id[0]:
922
+ dq_store_pipeline.producer_acquire()
923
+ self.epi_sync_barrier.arrive_and_wait()
924
+ cute.copy(f32_cp_atom, tTR_rDQ, tDQsS[None, None, None, 0])
925
+ cute.arch.fence_proxy("async.shared", space="cta")
926
+ self.epi_sync_barrier.arrive_and_wait()
927
+ if warp_idx == self.epi_warp_id[0]:
928
+ cute.copy(tma_dq, bSG_sDQS[None, 0], bSG_gDQ[None, c])
929
+ dq_store_pipeline.producer_commit()
930
+
931
+ dq_store_pipeline.producer_tail()
932
+
933
+ tmem.relinquish_alloc_permit()
934
+ self.tmem_dealloc_sync_barrier.arrive_and_wait()
935
+ tmem.free(tmem_ptr_base)
936
+ return
937
+
938
+
939
+ # --------------------------------------------------------------------------------------------
940
+ # host wrapper
941
+ # --------------------------------------------------------------------------------------------
942
+
943
+ _COMPILE_CACHE: dict = {}
944
+
945
+
946
+ # Layout-keyed call cache with ctypes pointer retargeting; outputs are NEVER cached —
947
+ # allocated per call and retargeted (see kernel_fwd.py's note above _CALL_CACHE).
948
+ _CALL_CACHE: dict = {}
949
+
950
+
951
+ def _dq_buffer(shape, device, enable_dq: bool) -> torch.Tensor:
952
+ """dq's [B,T,HV,K] fp32 buffer — or a 16-byte stand-in when the fusion is off.
953
+
954
+ With enable_dq=False (the default at prod — the dq fusion measured negative) the kernel
955
+ is COMPILED without its dq epilogue: it never reads or writes this tensor, and the
956
+ wrapper returns None instead of it. Only the descriptor is needed, so allocating the
957
+ real thing costs a gigabyte of allocator churn per backward at prod8192 (and pins the
958
+ same gigabyte in the call cache) for a buffer nothing touches. as_strided over a short
959
+ base is deliberately out of bounds — that is safe here and ONLY here, because no code
960
+ path dereferences it; if a torch version ever refuses, fall back to the allocation.
961
+ """
962
+ if enable_dq:
963
+ # reduce-add accumulated: MUST start zeroed on every call.
964
+ return torch.zeros(shape, device=device, dtype=torch.float32)
965
+ strides, acc = [], 1
966
+ for d in reversed(tuple(shape)):
967
+ strides.append(acc)
968
+ acc *= int(d)
969
+ try:
970
+ base = torch.empty(4, device=device, dtype=torch.float32)
971
+ return torch.as_strided(base, tuple(shape), tuple(reversed(strides)))
972
+ except RuntimeError:
973
+ return torch.empty(shape, device=device, dtype=torch.float32)
974
+
975
+
976
+ def _call_key(kg, w, u, g2, h0, do, enable_dq):
977
+ def sig(t):
978
+ return (t.shape, t.stride(), t.dtype)
979
+
980
+ return (sig(kg), sig(w), sig(u), sig(g2), sig(h0), sig(do), enable_dq,
981
+ torch.cuda.current_stream().cuda_stream)
982
+
983
+
984
+ def kda_cute_b1_call(
985
+ kg: torch.Tensor, # [B,T,HV,K] bf16/fp16 — k * exp2(G - g2), from recompute
986
+ w: torch.Tensor, # [B,T,HV,K]
987
+ u: torch.Tensor, # [B,T,HV,V]
988
+ g2: torch.Tensor, # [B,T,HV,K] fp32, chunk-local cumsum / ln2 (only last rows used)
989
+ h0: torch.Tensor, # [B,HV,K,V] fp32
990
+ do: torch.Tensor, # [B,T,HV,V]
991
+ enable_dq: bool = True,
992
+ ):
993
+ """Returns (h checkpoints [B,NT,HV,K,V] bf16, v_new [B,T,HV,V], dq_raw [B,T,HV,K] fp32).
994
+
995
+ dq_raw is Σ_v do@h^T with NO gate/scale — kernel_wy.py applies scale*exp2(g2).
996
+ enable_dq=False compiles/runs the pure rescan and returns dq_raw=None."""
997
+ key = _call_key(kg, w, u, g2, h0, do, enable_dq)
998
+ ent = _CALL_CACHE.get(key)
999
+ outs = None
1000
+ if ent is None:
1001
+ B, T, HV, K = kg.shape
1002
+ V = u.shape[3]
1003
+ NT = T // 64
1004
+ assert T % 64 == 0, "T must be a multiple of the chunk size"
1005
+ assert V % 64 == 0
1006
+
1007
+ h = torch.empty(B, NT, HV, K, V, device=kg.device, dtype=kg.dtype)
1008
+ v_new = torch.empty(B, T, HV, V, device=kg.device, dtype=u.dtype)
1009
+ dq = _dq_buffer((B, T, HV, K), kg.device, enable_dq)
1010
+ # Per-chunk decay vectors, K contiguous (persistent scratch, refilled per call)
1011
+ gdc = torch.empty(B, HV, NT, K, device=g2.device, dtype=g2.dtype)
1012
+
1013
+ io_dtype = cutlass.BFloat16 if kg.dtype == torch.bfloat16 else cutlass.Float16
1014
+ compile_key = (io_dtype, K, V, enable_dq)
1015
+
1016
+ cw = _cute_view(w, (1, 3, 2, 0), (0, 2, 3))
1017
+ ckgt = _cute_view(kg, (3, 1, 2, 0), (1, 2, 3))
1018
+ cdo = _cute_view(do, (1, 3, 2, 0), (0, 2, 3))
1019
+ cu = _cute_view(u, (1, 3, 2, 0), (0, 2, 3))
1020
+ cgd = _cute_view(gdc, (3, 2, 1, 0), (1, 2, 3))
1021
+ ch0 = _cute_view(h0, (2, 3, 1, 0), (2, 3))
1022
+ cvn = _cute_view(v_new, (1, 3, 2, 0), (0, 2, 3))
1023
+ chck = _cute_view(h, (4, 3, 1, 2, 0), (2, 3, 4))
1024
+ cdq = _cute_view(dq, (1, 3, 2, 0), (0, 2, 3))
1025
+
1026
+ stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
1027
+ compiled = _COMPILE_CACHE.get(compile_key)
1028
+ if compiled is None:
1029
+ kernel_obj = KdaB1ScanKernel(io_dtype, K, V_TILE=64, enable_dq=enable_dq)
1030
+ compiled = cute.compile(
1031
+ kernel_obj, cw, ckgt, cdo, cu, cgd, ch0, cvn, chck, cdq, stream,
1032
+ )
1033
+ _COMPILE_CACHE[compile_key] = compiled
1034
+ # See kernel_fwd._release_keepalives: without this the entry pins the first call's
1035
+ # w/kg/do/u/h0 and its outputs (h alone is 2 GiB at prod8192) forever.
1036
+ _release_keepalives(cw, ckgt, cdo, cu, cgd, ch0, cvn, chck, cdq)
1037
+ args = (cw, ckgt, cdo, cu, cgd, ch0, cvn, chck, cdq, stream)
1038
+ if len(_CALL_CACHE) >= 64:
1039
+ _CALL_CACHE.clear()
1040
+ outs = (h, v_new, dq)
1041
+ out_specs = tuple((tuple(t.shape), t.dtype) for t in outs)
1042
+ ent = (compiled, args, out_specs, gdc)
1043
+ _CALL_CACHE[key] = ent
1044
+
1045
+ compiled, args, out_specs, gdc = ent
1046
+ cw, ckgt, cdo, cu, _, ch0, cvn, chck, cdq, _ = args
1047
+ if outs is None:
1048
+ (hs, hd), (vs, vd), (qs, qd) = out_specs
1049
+ h = torch.empty(hs, device=kg.device, dtype=hd)
1050
+ v_new = torch.empty(vs, device=kg.device, dtype=vd)
1051
+ dq = _dq_buffer(qs, kg.device, enable_dq)
1052
+ outs = (h, v_new, dq)
1053
+ h, v_new, dq = outs
1054
+ _retarget(chck, h)
1055
+ _retarget(cvn, v_new)
1056
+ _retarget(cdq, dq)
1057
+ _retarget(cw, w)
1058
+ _retarget(ckgt, kg)
1059
+ _retarget(cdo, do)
1060
+ _retarget(cu, u)
1061
+ _retarget(ch0, h0)
1062
+ # refill the decay staging in place: g2's last row per chunk
1063
+ gdc.copy_(g2[:, 63::64].transpose(1, 2))
1064
+ compiled(*args)
1065
+ return h, v_new, (dq if enable_dq else None)
1066
+
1067
+
1068
+ # Minimum grid size (CTAs) for the cute path — serial scans need a full GPU (gdn 003
1069
+ # record 005: the dhu kernel lost 1.4ms to fla at a 64-CTA grid). KDA002_B1=cutedsl
1070
+ # forces past it so dbg-sized shapes exercise the cute kernel (gdn record 006's
1071
+ # process bug: without the override, every dbg case compared fla vs fla).
1072
+ _MIN_CTAS = 256
1073
+
1074
+
1075
+ def kda_rescan_b1(kg, w, u, g2, h0, do, chunk_size):
1076
+ """Stage-2 dispatcher. Returns (h, v_new, dq_raw | None); dq_raw None means the
1077
+ fla fallback ran and wy_dqkg must compute dq itself."""
1078
+ B, T, HV, K = kg.shape
1079
+ V = u.shape[-1]
1080
+ supported = (
1081
+ chunk_size == 64
1082
+ and T % 64 == 0
1083
+ and K in (64, 128)
1084
+ and V % 64 == 0
1085
+ and kg.dtype in (torch.bfloat16, torch.float16)
1086
+ and B * HV * (V // 64) >= _MIN_CTAS
1087
+ )
1088
+ if not supported:
1089
+ from fla.ops.common.chunk_delta_h import chunk_gated_delta_rule_fwd_h
1090
+
1091
+ h, v_new, _ = chunk_gated_delta_rule_fwd_h(
1092
+ k=kg, w=w, u=u, gk=g2, initial_state=h0,
1093
+ output_final_state=False, chunk_size=chunk_size,
1094
+ )
1095
+ return h, v_new, None
1096
+ # Default is the pure rescan: the dq fusion measured NEGATIVE at prod8192
1097
+ # (2026-08-18 dbg_b1perf: rescan-only 1.37ms vs fla 1.88 = +0.51ms; the dq add-on
1098
+ # costs 0.75ms in-kernel at its own traffic floor — 4.3GB fp32 cross-CTA reduce +
1099
+ # do load — plus 0.15ms zeros, while the no-dq wy variant only returned 0.12ms:
1100
+ # fla's wy had both dq operands loaded anyway and is latency-bound, not dot-bound).
1101
+ # KDA002_B1=cutedsl keeps the fusion compilable for B2 experiments, where the
1102
+ # reduce-add is the pattern every fused V-reduced output (dk/dw) will need.
1103
+ # enable_dq=False: the dq fusion (B1 computing do@h^T for the wy stage) measured
1104
+ # negative and wy_dqkg computes dq itself. The kernel compiles without its dq epilogue.
1105
+ return kda_cute_b1_call(kg, w, u, g2, h0, do, enable_dq=False)