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,320 @@
1
+ """Phase 2 B2b — fla 0.5.2's `chunk_kda_bwd_wy_dqkg_fused`, restructured to run at full K.
2
+
3
+ fla's grid is already one CTA per (chunk, b·hv); the waste is entirely inside the CTA,
4
+ in `for i_k in range(cdiv(K, BK))`. Slabbing K costs three separate things:
5
+
6
+ 1. `v_new`, `do` and `dv` are loaded in the i_v loop, which sits INSIDE the i_k loop,
7
+ so each is re-read NK times per CTA (h/dh/q/k/g/A are read once). At prod8192 that
8
+ is +3.2GB at BK=64 (+9.7GB at BK=32) on top of a ~15.5GB useful footprint.
9
+ 2. Every inner dot is [BT,BV] @ [BV,BK] with BK ∈ {32,64} — a tiny N, three of them
10
+ chained on the same two operands. B1's probe already showed this stage is
11
+ latency-bound, not dot-bound (deleting one of the three dots bought 0.12ms of 6.15).
12
+ 3. `tl.debug_barrier()` — fla marks it DO NOT REMOVE — is a full CTA barrier executed
13
+ NK·NV times, which is there only because the `if i_k == 0` block reuses the loop's
14
+ smem. It defeats whatever `num_stages` was going to pipeline.
15
+
16
+ So: BK = K. The i_k loop is gone, `h`/`dh` load as [BV, K], the V-shaped tiles are read
17
+ ONCE, the three dots widen to [BT,BV] @ [BV,K], the `i_k == 0` guard disappears (and with
18
+ it the reason for the barrier), and the whole exp2/dg/dA epilogue runs once instead of NK
19
+ times. Accumulators: dq/dk/dw [BT,K] fp32 + dA [BT,BT] = 448 of 512 tmem cols on Blackwell.
20
+
21
+ Numerics vs fla: the accumulation order over V is unchanged (i_v ascending, fp32 acc).
22
+ What moves is the reduction blocking inside each dot and `b_dA += dot(b_dw, kg^T)`, which
23
+ was NK partial sums over BK-wide slices and is now one sum over K — fp32 reassociations of
24
+ the same terms, no new exp2 factorization and no gate rewrite. Every store is idempotent
25
+ (the autotuner re-runs the kernel once per config trial, and any read-modify-write tensor
26
+ would corrupt on the first call per key — the NOTES-002 CachedAutotuner trap).
27
+
28
+ Varlen is not plumbed (the staged chain never passes cu_seqlens); off the supported shape
29
+ or below the CTA floor the dispatcher falls back to fla's fused kernel.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+
35
+ import torch
36
+ import triton
37
+ import triton.language as tl
38
+
39
+ from fla.ops.utils.cache import fla_cache_autotune
40
+ from fla.ops.utils.op import exp2
41
+ from fla.utils import autotune_cache_kwargs, check_shared_mem
42
+
43
+ # Measured on b300 (dbg_wysweep.py, prod8192): BV=128 blows smem at num_stages>=2 and
44
+ # never wins at 1; num_warps 16/32 spill ~2KB/thread and run 40-80x slower, so 8 warps is
45
+ # the ceiling. Everything viable lands within ~1% of 4.95ms, which is the register wall,
46
+ # not a tiling choice — keep the space small so autotune trials stay cheap.
47
+ BV_LIST = [32, 64] if check_shared_mem('ampere') else [16, 32]
48
+
49
+ # Serial-ish per-CTA kernels want a full GPU before they beat fla's autotuned one; below
50
+ # this many CTAs the launch is grid-starved and the comparison is noise (the same floor
51
+ # the other 002 stages use). KDA002_WY=triton forces past it so dbg-sized shapes still
52
+ # exercise this kernel.
53
+ _MIN_CTAS = 256
54
+
55
+
56
+ @fla_cache_autotune(
57
+ configs=[
58
+ triton.Config({'BV': BV}, num_warps=num_warps, num_stages=num_stages)
59
+ for BV in BV_LIST
60
+ for num_warps in [4, 8]
61
+ for num_stages in [2, 3, 4]
62
+ ],
63
+ key=['BT', 'HV', 'K', 'V', 'STATE_V_FIRST'],
64
+ **autotune_cache_kwargs,
65
+ )
66
+ @triton.jit(do_not_specialize=['T'])
67
+ def chunk_kda_bwd_kernel_wy_dqkg_wide(
68
+ q,
69
+ k,
70
+ v,
71
+ v_new,
72
+ g,
73
+ beta,
74
+ A,
75
+ h,
76
+ do,
77
+ dh,
78
+ dq,
79
+ dk,
80
+ dv,
81
+ dv2,
82
+ dg,
83
+ db,
84
+ dA,
85
+ scale,
86
+ T,
87
+ H: tl.constexpr,
88
+ HV: tl.constexpr,
89
+ K: tl.constexpr,
90
+ V: tl.constexpr,
91
+ BT: tl.constexpr,
92
+ BV: tl.constexpr,
93
+ STATE_V_FIRST: tl.constexpr,
94
+ PROBE_SKIP: tl.constexpr = 0,
95
+ ):
96
+ # PROBE_SKIP is an attribution knob for dbg_wysweep.py only (the KDA002C_SKIP pattern
97
+ # from the CuTe intra kernel): 1 drops the dgk elementwise reduction, 2 drops the WY
98
+ # block, 3 drops the three main dots. Any nonzero value produces WRONG output — it
99
+ # exists to price a piece, not to run. The shipping path always passes 0.
100
+ i_t, i_bh = tl.program_id(0).to(tl.int64), tl.program_id(1)
101
+ i_b, i_hv = i_bh // HV, i_bh % HV
102
+ i_h = i_hv // (HV // H)
103
+
104
+ NT = tl.cdiv(T, BT)
105
+ i_tg = (i_b * NT + i_t).to(tl.int64)
106
+ bos = (i_b * T).to(tl.int64)
107
+
108
+ o_t = i_t * BT + tl.arange(0, BT)
109
+ m_t = o_t < T
110
+ m_last = (o_t == min(T, i_t * BT + BT) - 1)
111
+
112
+ q += (bos * H + i_h) * K
113
+ k += (bos * H + i_h) * K
114
+ v += (bos * HV + i_hv) * V
115
+ v_new += (bos * HV + i_hv) * V
116
+ g += (bos * HV + i_hv) * K
117
+ beta += bos * HV + i_hv
118
+ A += (bos * HV + i_hv) * BT
119
+ h += (i_tg * HV + i_hv) * K*V
120
+ do += (bos * HV + i_hv) * V
121
+ dh += (i_tg * HV + i_hv) * K*V
122
+ dq += (bos * HV + i_hv) * K
123
+ dk += (bos * HV + i_hv) * K
124
+ dv += (bos * HV + i_hv) * V
125
+ dv2 += (bos * HV + i_hv) * V
126
+ dg += (bos * HV + i_hv) * K
127
+ db += bos * HV + i_hv
128
+ dA += (bos * HV + i_hv) * BT
129
+
130
+ p_beta = beta + o_t * HV
131
+ b_beta = tl.load(p_beta, mask=m_t, other=0.0)
132
+
133
+ o_A = tl.arange(0, BT)
134
+ m_AT = (o_A[:, None] < BT) & m_t[None, :]
135
+ p_A = A + o_A[:, None] + o_t[None, :] * (HV * BT)
136
+ b_A = tl.load(p_A, mask=m_AT, other=0.0)
137
+
138
+ # K is the full head dim here — one slab, no i_k loop.
139
+ o_k = tl.arange(0, K)
140
+ m_tk = m_t[:, None] & (o_k < K)[None, :]
141
+
142
+ p_k = k + o_t[:, None] * (H*K) + o_k[None, :]
143
+ p_g = g + o_t[:, None] * (HV*K) + o_k[None, :]
144
+ b_k = tl.load(p_k, mask=m_tk, other=0.0)
145
+ b_g = tl.load(p_g, mask=m_tk, other=0.0).to(tl.float32)
146
+
147
+ p_gn = g + (min(T, i_t * BT + BT) - 1).to(tl.int64) * HV*K + o_k
148
+ b_gn = tl.load(p_gn, mask=o_k < K, other=0).to(tl.float32)
149
+
150
+ b_dA = tl.zeros([BT, BT], dtype=tl.float32)
151
+ b_db = tl.zeros([BT], dtype=tl.float32)
152
+ b_dq = tl.zeros([BT, K], dtype=tl.float32)
153
+ b_dk = tl.zeros([BT, K], dtype=tl.float32)
154
+ b_dw = tl.zeros([BT, K], dtype=tl.float32)
155
+ b_dgk = tl.zeros([K], dtype=tl.float32)
156
+
157
+ for i_v in range(tl.cdiv(V, BV)):
158
+ o_v = i_v * BV + tl.arange(0, BV)
159
+ m_tv = m_t[:, None] & (o_v[None, :] < V)
160
+ m_h = (o_v[:, None] < V) & (o_k < K)[None, :]
161
+ p_v_new = v_new + o_t[:, None] * (HV*V) + o_v[None, :]
162
+ p_do = do + o_t[:, None] * (HV*V) + o_v[None, :]
163
+ if STATE_V_FIRST:
164
+ p_h = h + o_v[:, None] * K + o_k[None, :]
165
+ p_dh = dh + o_v[:, None] * K + o_k[None, :]
166
+ else:
167
+ p_h = h + o_v[:, None] + o_k[None, :] * V
168
+ p_dh = dh + o_v[:, None] + o_k[None, :] * V
169
+ p_v = v + o_t[:, None] * (HV*V) + o_v[None, :]
170
+ p_dv = dv + o_t[:, None] * (HV*V) + o_v[None, :]
171
+ p_dv2 = dv2 + o_t[:, None] * (HV*V) + o_v[None, :]
172
+ # [BT, BV] — each read ONCE now, not once per K slab
173
+ b_v_new = tl.load(p_v_new, mask=m_tv, other=0.0)
174
+ b_do = tl.load(p_do, mask=m_tv, other=0.0)
175
+ b_v = tl.load(p_v, mask=m_tv, other=0.0)
176
+ b_dv = tl.load(p_dv, mask=m_tv, other=0.0)
177
+ # [BV, K]
178
+ b_h = tl.load(p_h, mask=m_h, other=0.0)
179
+ b_dh = tl.load(p_dh, mask=m_h, other=0.0)
180
+
181
+ if PROBE_SKIP != 1:
182
+ b_dgk += tl.sum(b_h * b_dh, axis=0)
183
+ if PROBE_SKIP != 3:
184
+ b_dq += tl.dot(b_do, b_h.to(b_do.dtype))
185
+ b_dk += tl.dot(b_v_new, b_dh.to(b_v_new.dtype))
186
+ b_dw += tl.dot(b_dv.to(b_v_new.dtype), b_h.to(b_v_new.dtype))
187
+
188
+ # fla ran this block under `if i_k == 0` to keep it from repeating per K slab;
189
+ # with the slab loop gone it is just the v-loop body, and the debug_barrier that
190
+ # guarded the smem reuse across that branch goes with it.
191
+ if PROBE_SKIP != 2:
192
+ b_dA += tl.dot(b_dv, tl.trans(b_v))
193
+ b_dvb = tl.dot(b_A, b_dv)
194
+ b_dv2 = b_dvb * b_beta[:, None]
195
+ b_db += tl.sum(b_dvb * b_v, 1)
196
+ tl.store(p_dv2, b_dv2.to(p_dv2.dtype.element_ty), mask=m_tv)
197
+
198
+ b_gk_exp = exp2(b_g)
199
+ b_gb = b_gk_exp * b_beta[:, None]
200
+ b_dgk *= exp2(b_gn)
201
+ b_dq = b_dq * b_gk_exp * scale
202
+ b_dk = b_dk * tl.where(m_t[:, None], exp2(b_gn[None, :] - b_g), 0)
203
+
204
+ b_kg = b_k * b_gk_exp
205
+
206
+ b_dw = -b_dw.to(b_A.dtype)
207
+ b_dA += tl.dot(b_dw, tl.trans(b_kg.to(b_A.dtype)))
208
+
209
+ b_dkgb = tl.dot(b_A, b_dw)
210
+ b_db += tl.sum(b_dkgb * b_kg, 1)
211
+
212
+ p_q = q + o_t[:, None] * (H*K) + o_k[None, :]
213
+ b_q = tl.load(p_q, mask=m_tk, other=0.0)
214
+ b_kdk = b_k * b_dk
215
+ b_dgk += tl.sum(b_kdk, axis=0)
216
+ b_dg = b_q * b_dq - b_kdk + m_last[:, None] * b_dgk + b_kg * b_dkgb * b_beta[:, None]
217
+ b_dk = b_dk + b_dkgb * b_gb
218
+
219
+ p_dq = dq + o_t[:, None] * (HV*K) + o_k[None, :]
220
+ p_dk = dk + o_t[:, None] * (HV*K) + o_k[None, :]
221
+ p_dg = dg + o_t[:, None] * (HV*K) + o_k[None, :]
222
+ tl.store(p_dq, b_dq.to(p_dq.dtype.element_ty), mask=m_tk)
223
+ tl.store(p_dk, b_dk.to(p_dk.dtype.element_ty), mask=m_tk)
224
+ tl.store(p_dg, b_dg.to(p_dg.dtype.element_ty), mask=m_tk)
225
+
226
+ m_A = (o_t[:, None] > o_t[None, :]) & (m_t[:, None] & m_t)
227
+ b_dA = tl.where(m_A, b_dA * b_beta[None, :], 0)
228
+ b_dA = tl.dot(b_dA.to(b_A.dtype), b_A)
229
+ b_dA = tl.dot(b_A, b_dA.to(b_A.dtype))
230
+ b_dA = tl.where(m_A, -b_dA, 0)
231
+
232
+ m_dA = m_t[:, None] & (o_A[None, :] < BT)
233
+ p_dA = dA + o_t[:, None] * (HV * BT) + o_A[None, :]
234
+ p_db = db + o_t * HV
235
+ tl.store(p_dA, b_dA.to(p_dA.dtype.element_ty), mask=m_dA)
236
+ tl.store(p_db, b_db.to(p_db.dtype.element_ty), mask=m_t)
237
+
238
+
239
+ def _supported(K: int, V: int, BT: int, ctas: int) -> bool:
240
+ """Full-K tiles need K to be a power of two `tl.arange` can produce and small enough
241
+ that dq/dk/dw fit tmem; V must tile by the smallest configured BV. The CTA floor keeps
242
+ grid-starved shapes on fla, where they are genuinely faster."""
243
+ return (
244
+ K in (32, 64, 128)
245
+ and V % min(BV_LIST) == 0
246
+ and BT == 64
247
+ and ctas >= _MIN_CTAS
248
+ )
249
+
250
+
251
+ def chunk_kda_bwd_wy_dqkg_wide(
252
+ q: torch.Tensor,
253
+ k: torch.Tensor,
254
+ v: torch.Tensor,
255
+ v_new: torch.Tensor,
256
+ g: torch.Tensor,
257
+ beta: torch.Tensor,
258
+ A: torch.Tensor,
259
+ h: torch.Tensor,
260
+ do: torch.Tensor,
261
+ dh: torch.Tensor,
262
+ dv: torch.Tensor,
263
+ scale: float | None = None,
264
+ state_v_first: bool = False,
265
+ chunk_size: int = 64,
266
+ ):
267
+ B, T, H, K, HV, V = *k.shape, v.shape[2], v.shape[-1]
268
+ BT = chunk_size
269
+ NT = triton.cdiv(T, BT)
270
+
271
+ # (The tmem CuTe port of this stage lives in the research ladder and is NOT shipped:
272
+ # it is correct but measured 6.93ms against this kernel's 4.98 at prod8192.)
273
+ if state_v_first or not _supported(K, V, BT, NT * B * HV):
274
+ from fla.ops.kda.chunk_bwd import chunk_kda_bwd_wy_dqkg_fused
275
+
276
+ return chunk_kda_bwd_wy_dqkg_fused(
277
+ q=q, k=k, v=v, v_new=v_new, g=g, beta=beta, A=A, h=h,
278
+ do=do, dh=dh, dv=dv, scale=scale, state_v_first=state_v_first,
279
+ chunk_size=chunk_size,
280
+ )
281
+
282
+ # dq, dk are allocated at HV; the caller reduces to H if GVA. All outputs are fresh
283
+ # (never read-modify-write — autotune trials re-run the kernel).
284
+ dq = g.new_empty(B, T, HV, K, dtype=torch.float)
285
+ dk = g.new_empty(B, T, HV, K, dtype=torch.float)
286
+ dv2 = torch.empty_like(v)
287
+ dg = torch.empty_like(g, dtype=torch.float)
288
+ db = torch.empty_like(beta, dtype=torch.float)
289
+ dA = torch.empty_like(A, dtype=torch.float)
290
+
291
+ grid = (NT, B * HV)
292
+ chunk_kda_bwd_kernel_wy_dqkg_wide[grid](
293
+ q=q,
294
+ k=k,
295
+ v=v,
296
+ v_new=v_new,
297
+ g=g,
298
+ beta=beta,
299
+ A=A,
300
+ h=h,
301
+ do=do,
302
+ dh=dh,
303
+ dq=dq,
304
+ dk=dk,
305
+ dv=dv,
306
+ dv2=dv2,
307
+ dg=dg,
308
+ db=db,
309
+ dA=dA,
310
+ scale=scale,
311
+ T=T,
312
+ H=H,
313
+ HV=HV,
314
+ K=K,
315
+ V=V,
316
+ BT=BT,
317
+ STATE_V_FIRST=state_v_first,
318
+ PROBE_SKIP=0, # explicit: the attribution knob is never on in the shipping path
319
+ )
320
+ return dq, dk, dv2, db, dg, dA
@@ -0,0 +1,309 @@
1
+ """Phase 2 B2b, transposed — the wy_dqkg stage as two Triton kernels (ladder idea 005).
2
+
3
+ Same math as bwd_wy.py (002's kernel_wy2: fla's fused wy_dqkg at full K), re-arranged
4
+ around the two occupancy walls the 2026-09-01 probes found in wy2 (the ledger is in
5
+ kernels/kda/ideas/005-wy-transposed/NOTES.md; the ncu profile there reads 24.6%/36.2% warps
6
+ active against wy2's 12.4%, stage 4.98 -> 4.54ms at prod8192 B=16):
7
+
8
+ * wy2's loop accumulates dq/dk/dw as [BT=64, K=128] tiles: M=64 tcgen05 MMAs, operands
9
+ through registers, 255 regs -> 1 CTA/SM. Here the accumulators are TRANSPOSED —
10
+ dq^T = h^T @ do^T, dw^T = h^T @ dv^T, dk^T = dh^T @ v_new^T, all M=K=128 — and the
11
+ loop alone streams at the DRAM roofline (1.7ms for its 13GB at prod8192, 78 regs).
12
+ * The epilogue's live set ([64,128] fp32 x ~8) is what pins wy2 at 255 regs. Here the
13
+ raw accumulators are parked TRANSPOSED in the fp32 outputs at loop exit and the
14
+ elementwise epilogue re-reads them from L2 in K-strips of KS rows, so the kernel
15
+ compiles to 128 regs / 0 spills and two CTAs share an SM. Two is also the ceiling:
16
+ Triton allocates 256 tmem columns for these accumulators, and tmem is 512/SM.
17
+ * The v-side of the loop (dA = dv @ v^T, dvb = A @ dv -> dv2, db), wy2's `dA += dw @ kg^T`,
18
+ and the dA chain (mask/beta, two [64,64] dots) are a separate kernel: in the fused
19
+ kernel they cost more (1.3ms + 0.95ms, both latency chains at 2 CTAs/SM) than the
20
+ ~0.9ms the side kernel takes at 4 CTAs/SM. The main kernel parks -dw^T (bf16, exactly
21
+ wy2's b_dw) and kg = k*exp2(g) (bf16, from its strips) in two [B,T,HV,K] buffers for it,
22
+ so the side kernel never touches g or exp2 (a first cut that recomputed kg there held
23
+ the fp32 g tile at 248 regs and cost 0.5ms); the side kernel finalizes db IN PLACE on
24
+ the main kernel's partial and writes dA, so it must run after it and is never autotuned.
25
+
26
+ Numerics vs wy2: dq/dk/dv2 bit-identical, dg/db ~1e-6 (reassociated rowsums). dA is
27
+ accumulated the way wy2 does it — dv @ v^T then dw @ kg^T into ONE tmem accumulator —
28
+ because the first cut (dw @ kg^T in the main kernel, IEEE-added to the side's partial)
29
+ flipped bf16 roundings in the chain and put prod-NT dk 30x over dbg_bwd's budget.
30
+ Varlen is not plumbed; off the supported shape (K=128, V%64, BT=64) or below the CTA floor
31
+ the dispatcher falls back to bwd_wy's kernel, which is why the chain can always call this.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import torch
37
+ import triton
38
+ import triton.language as tl
39
+
40
+ from fla.ops.utils.op import exp2
41
+
42
+ from ..._common.support import MIN_CTAS
43
+ from .bwd_wy import chunk_kda_bwd_wy_dqkg_wide
44
+
45
+ # Pinned configs (probed on b300 at prod8192, the ladder's NOTES.md): BV=32/3 stages/8
46
+ # warps/maxnreg=128 for the main kernel (84-92KB smem, 128 regs, 0 spills -> 2 CTAs/SM);
47
+ # KS=32 strips (64 spills at 128 regs, 16 no faster). The side kernel is occupancy-bound
48
+ # (tmem 128 -> at most 4 CTAs/SM): 8 warps at maxnreg=80 (10 spills, 3 CTAs/SM) 1.12ms beat
49
+ # 4 warps uncapped (255 regs, 2 CTAs) 1.27 and 8 warps at 64 (18 spills, 4 CTAs) 1.17; BV=32
50
+ # and KD=32 lost. The ladder reads these from KDA005_* env knobs; here they are constants.
51
+ MAIN_BV = 32
52
+ MAIN_KS = 32
53
+ MAIN_STAGES = 3
54
+ SIDE_BV = 64
55
+ SIDE_WARPS = 8
56
+ SIDE_MAXNREG = 80
57
+ SIDE_KD = 64 # K-slice of the side kernel's dA dot
58
+
59
+
60
+ @triton.jit(do_not_specialize=['T'])
61
+ def chunk_kda_bwd_kernel_wy_t_main(
62
+ q, k, v_new, g, beta, A, h, do, dh, dq, dk, dv, dg, db, dw_buf, kg_buf, dgk_buf, scale, T,
63
+ H: tl.constexpr, HV: tl.constexpr, K: tl.constexpr, V: tl.constexpr,
64
+ BT: tl.constexpr, BV: tl.constexpr, KS: tl.constexpr, STATE_V_FIRST: tl.constexpr,
65
+ ):
66
+ i_t, i_bh = tl.program_id(0).to(tl.int64), tl.program_id(1)
67
+ i_b, i_hv = i_bh // HV, i_bh % HV
68
+ i_h = i_hv // (HV // H)
69
+ NT = tl.cdiv(T, BT)
70
+ i_tg = (i_b * NT + i_t).to(tl.int64)
71
+ bos = (i_b * T).to(tl.int64)
72
+ o_t = i_t * BT + tl.arange(0, BT)
73
+ m_t = o_t < T
74
+ i_last = (min(T, i_t * BT + BT) - 1).to(tl.int64)
75
+ m_last = o_t == i_last
76
+
77
+ q += (bos * H + i_h) * K
78
+ k += (bos * H + i_h) * K
79
+ v_new += (bos * HV + i_hv) * V
80
+ g += (bos * HV + i_hv) * K
81
+ beta += bos * HV + i_hv
82
+ A += (bos * HV + i_hv) * BT
83
+ h += (i_tg * HV + i_hv) * K*V
84
+ do += (bos * HV + i_hv) * V
85
+ dh += (i_tg * HV + i_hv) * K*V
86
+ dq += (bos * HV + i_hv) * K
87
+ dk += (bos * HV + i_hv) * K
88
+ dv += (bos * HV + i_hv) * V
89
+ dg += (bos * HV + i_hv) * K
90
+ db += bos * HV + i_hv
91
+ dw_buf += (bos * HV + i_hv) * K
92
+ kg_buf += (bos * HV + i_hv) * K
93
+ dgk_buf += (i_tg * HV + i_hv) * K
94
+
95
+ p_beta = beta + o_t * HV
96
+ b_beta = tl.load(p_beta, mask=m_t, other=0.0)
97
+ o_A = tl.arange(0, BT)
98
+ # b_At[t, a] = A_mem[t, a]: the B operand of dkgb^T = dw^T @ A^T (wy2's dot(A, dw)
99
+ # transposed; wy2's b_A is A_mem^T).
100
+ p_At = A + o_t[:, None] * (HV * BT) + o_A[None, :]
101
+ b_At = tl.load(p_At, mask=m_t[:, None] & (o_A[None, :] < BT), other=0.0)
102
+
103
+ o_k = tl.arange(0, K)
104
+ m_kt = (o_k < K)[:, None] & m_t[None, :]
105
+
106
+ # [do; dv] stacked along rows so one M=K dot yields [dq^T | dw^T]
107
+ o_n = tl.arange(0, 2 * BT)
108
+ o_tn = i_t * BT + (o_n % BT)
109
+ m_tn = o_tn < T
110
+ acc_qw = tl.zeros([K, 2 * BT], dtype=tl.float32)
111
+ acc_k = tl.zeros([K, BT], dtype=tl.float32)
112
+ b_db = tl.zeros([BT], dtype=tl.float32) # strips' dkgb.kg colsum; the side kernel adds dvb.v
113
+ dgk = tl.zeros([K], dtype=tl.float32)
114
+ for i_v in range(tl.cdiv(V, BV)):
115
+ o_v = i_v * BV + tl.arange(0, BV)
116
+ m_v = o_v < V
117
+ m_tv = m_t[:, None] & m_v[None, :]
118
+ if STATE_V_FIRST:
119
+ p_hT = h + o_k[:, None] + o_v[None, :] * K
120
+ p_dhT = dh + o_k[:, None] + o_v[None, :] * K
121
+ else:
122
+ p_hT = h + o_k[:, None] * V + o_v[None, :]
123
+ p_dhT = dh + o_k[:, None] * V + o_v[None, :]
124
+ b_hT = tl.load(p_hT, mask=m_v[None, :], other=0.0)
125
+ b_dhT = tl.load(p_dhT, mask=m_v[None, :], other=0.0)
126
+ p_dodv = tl.where((o_n < BT)[:, None], do + o_tn[:, None] * (HV*V) + o_v[None, :],
127
+ dv + o_tn[:, None] * (HV*V) + o_v[None, :])
128
+ b_dodv = tl.load(p_dodv, mask=m_tn[:, None] & m_v[None, :], other=0.0)
129
+ p_vn = v_new + o_t[:, None] * (HV*V) + o_v[None, :]
130
+ b_vn = tl.load(p_vn, mask=m_tv, other=0.0)
131
+
132
+ dgk += tl.sum(b_hT * b_dhT, axis=1)
133
+ acc_qw += tl.dot(b_hT, tl.trans(b_dodv))
134
+ acc_k += tl.dot(b_dhT, tl.trans(b_vn))
135
+
136
+ # Park dq^T/dk^T raw (fp32) in dq/dk, -dw^T (bf16, = wy2's b_dw) in dw_buf for the side
137
+ # kernel's dA dot, and dkgb^T = dw^T @ A^T (the one dot here, M=128) raw in dg.
138
+ acc_qw3 = tl.permute(tl.reshape(acc_qw, [K, 2, BT]), (0, 2, 1))
139
+ dqT_raw, dwT_raw = tl.split(acc_qw3)
140
+ tl.store(dq + o_t[None, :] * (HV*K) + o_k[:, None], dqT_raw, mask=m_kt)
141
+ tl.store(dk + o_t[None, :] * (HV*K) + o_k[:, None], acc_k, mask=m_kt)
142
+ tl.store(dgk_buf + o_k, dgk)
143
+ b_dwT = -dwT_raw.to(b_At.dtype)
144
+ tl.store(dw_buf + o_t[None, :] * (HV*K) + o_k[:, None], b_dwT, mask=m_kt)
145
+ b_dkgbT = tl.dot(b_dwT, b_At)
146
+ tl.store(dg + o_t[None, :] * (HV*K) + o_k[:, None], b_dkgbT, mask=m_kt)
147
+ tl.debug_barrier()
148
+
149
+ # K-strip epilogue over the parked raw tiles ([KS, BT], k contiguous). Dot-free: every
150
+ # strip is loads -> elementwise -> stores, so the live set stays under 128 regs.
151
+ for s in tl.range(0, K // KS, num_stages=1):
152
+ o_ks = s * KS + tl.arange(0, KS)
153
+ m_kst = (o_ks < K)[:, None] & m_t[None, :]
154
+ off_tk = o_t[None, :] * (HV*K) + o_ks[:, None]
155
+ b_gn = tl.load(g + i_last * HV*K + o_ks).to(tl.float32)
156
+ b_gT = tl.load(g + off_tk, mask=m_kst, other=0.0).to(tl.float32)
157
+ b_kT = tl.load(k + o_t[None, :] * (H*K) + o_ks[:, None], mask=m_kst, other=0.0)
158
+ b_qT = tl.load(q + o_t[None, :] * (H*K) + o_ks[:, None], mask=m_kst, other=0.0)
159
+ b_dqT = tl.load(dq + off_tk, mask=m_kst, other=0.0)
160
+ b_dkT = tl.load(dk + off_tk, mask=m_kst, other=0.0)
161
+ b_dkgb_s = tl.load(dg + off_tk, mask=m_kst, other=0.0)
162
+ b_dgk = tl.load(dgk_buf + o_ks)
163
+
164
+ # wy2's epilogue, transposed ([k, t] tiles), same one-sided exp2 forms
165
+ b_e = exp2(b_gT)
166
+ b_dqT = b_dqT * b_e * scale
167
+ b_dkT = b_dkT * tl.where(m_t[None, :], exp2(b_gn[:, None] - b_gT), 0)
168
+ b_kg_s = b_kT * b_e
169
+ tl.store(kg_buf + off_tk, b_kg_s.to(kg_buf.dtype.element_ty), mask=m_kst) # side's dA operand
170
+ b_db += tl.sum(b_dkgb_s * b_kg_s, 0)
171
+ b_kdkT = b_kT * b_dkT
172
+ b_dgk = b_dgk * exp2(b_gn) + tl.sum(b_kdkT, axis=1)
173
+ b_dgT = (b_qT * b_dqT - b_kdkT + m_last[None, :] * b_dgk[:, None]
174
+ + b_kg_s * b_dkgb_s * b_beta[None, :])
175
+ b_dkT = b_dkT + b_dkgb_s * (b_e * b_beta[None, :])
176
+ tl.store(dq + off_tk, b_dqT.to(dq.dtype.element_ty), mask=m_kst)
177
+ tl.store(dk + off_tk, b_dkT.to(dk.dtype.element_ty), mask=m_kst)
178
+ tl.store(dg + off_tk, b_dgT.to(dg.dtype.element_ty), mask=m_kst)
179
+
180
+ tl.store(db + o_t * HV, b_db.to(db.dtype.element_ty), mask=m_t) # partial; side adds
181
+
182
+
183
+ @triton.jit(do_not_specialize=['T'])
184
+ def chunk_kda_bwd_kernel_wy_t_side(
185
+ v, beta, A, dv, dw_buf, kg_buf, dv2, db, dA, T,
186
+ HV: tl.constexpr, K: tl.constexpr, V: tl.constexpr, BT: tl.constexpr, BV: tl.constexpr,
187
+ KD: tl.constexpr,
188
+ ):
189
+ """The v-side of wy2's loop plus its dA path: dv2 = (A @ dv)*beta,
190
+ db += rowsum((A @ dv) * v), dA = chain(dv @ v^T + dw @ kg^T) with dw and kg read from
191
+ the main kernel's buffers and the two dots accumulated in wy2's order into one
192
+ accumulator. db is finalized IN PLACE on the main kernel's partial: launch after it,
193
+ never autotune it."""
194
+ i_t, i_bh = tl.program_id(0).to(tl.int64), tl.program_id(1)
195
+ i_b, i_hv = i_bh // HV, i_bh % HV
196
+ bos = (i_b * T).to(tl.int64)
197
+ o_t = i_t * BT + tl.arange(0, BT)
198
+ m_t = o_t < T
199
+ o_A = tl.arange(0, BT)
200
+ dw_buf += (bos * HV + i_hv) * K
201
+ kg_buf += (bos * HV + i_hv) * K
202
+ v += (bos * HV + i_hv) * V
203
+ dv += (bos * HV + i_hv) * V
204
+ dv2 += (bos * HV + i_hv) * V
205
+ beta += bos * HV + i_hv
206
+ A += (bos * HV + i_hv) * BT
207
+ dA += (bos * HV + i_hv) * BT
208
+ db += bos * HV + i_hv
209
+ b_beta = tl.load(beta + o_t * HV, mask=m_t, other=0.0)
210
+ p_A = A + o_A[:, None] + o_t[None, :] * (HV * BT) # wy2's b_A = A_mem^T
211
+ b_A = tl.load(p_A, mask=(o_A[:, None] < BT) & m_t[None, :], other=0.0)
212
+ b_dA = tl.zeros([BT, BT], dtype=tl.float32)
213
+ b_db = tl.zeros([BT], dtype=tl.float32)
214
+ for i_v in range(tl.cdiv(V, BV)):
215
+ o_v = i_v * BV + tl.arange(0, BV)
216
+ m_tv = m_t[:, None] & (o_v[None, :] < V)
217
+ b_v = tl.load(v + o_t[:, None] * (HV*V) + o_v[None, :], mask=m_tv, other=0.0)
218
+ b_dv = tl.load(dv + o_t[:, None] * (HV*V) + o_v[None, :], mask=m_tv, other=0.0)
219
+ b_dA += tl.dot(b_dv, tl.trans(b_v))
220
+ b_dvb = tl.dot(b_A, b_dv)
221
+ b_db += tl.sum(b_dvb * b_v, 1)
222
+ p_dv2 = dv2 + o_t[:, None] * (HV*V) + o_v[None, :]
223
+ tl.store(p_dv2, (b_dvb * b_beta[:, None]).to(p_dv2.dtype.element_ty), mask=m_tv)
224
+ # wy2's epilogue dot, into the same accumulator: b_dw / b_kg are wy2's -dw.bf16 and
225
+ # kg.bf16 bit-for-bit (the transposed loop's dw matched wy2's), so dA's chain sees
226
+ # wy2's fp32 sum. In K-slices of KD so the operands never sit in registers whole
227
+ # (hoisting the full [64,128] pair above the loop cost 2 CTAs/SM).
228
+ for s in tl.static_range(K // KD):
229
+ o_k = s * KD + tl.arange(0, KD)
230
+ m_tk = m_t[:, None] & (o_k < K)[None, :]
231
+ b_dw = tl.load(dw_buf + o_t[:, None] * (HV*K) + o_k[None, :], mask=m_tk, other=0.0)
232
+ b_kg = tl.load(kg_buf + o_t[:, None] * (HV*K) + o_k[None, :], mask=m_tk, other=0.0)
233
+ b_dA += tl.dot(b_dw, tl.trans(b_kg))
234
+ m_dA = m_t[:, None] & (o_A[None, :] < BT)
235
+ p_dA = dA + o_t[:, None] * (HV * BT) + o_A[None, :]
236
+ m_A = (o_t[:, None] > o_t[None, :]) & (m_t[:, None] & m_t)
237
+ b_dA = tl.where(m_A, b_dA * b_beta[None, :], 0)
238
+ b_dA = tl.dot(b_dA.to(b_A.dtype), b_A)
239
+ b_dA = tl.dot(b_A, b_dA.to(b_A.dtype))
240
+ b_dA = tl.where(m_A, -b_dA, 0)
241
+ tl.store(p_dA, b_dA.to(p_dA.dtype.element_ty), mask=m_dA)
242
+ p_db = db + o_t * HV
243
+ b_db += tl.load(p_db, mask=m_t, other=0.0)
244
+ tl.store(p_db, b_db.to(p_db.dtype.element_ty), mask=m_t)
245
+
246
+
247
+ def _supported(K: int, V: int, BT: int, ctas: int) -> bool:
248
+ """M=K=128 tcgen05 tiles need K=128; V must tile by both BVs; below the CTA floor a
249
+ grid-starved launch loses to wy2's autotuned kernel, so those shapes stay on it."""
250
+ if K != 128 or V % max(MAIN_BV, SIDE_BV) != 0 or BT != 64:
251
+ return False
252
+ return ctas >= MIN_CTAS
253
+
254
+
255
+ def chunk_kda_bwd_wy_dqkg_t(
256
+ q: torch.Tensor,
257
+ k: torch.Tensor,
258
+ v: torch.Tensor,
259
+ v_new: torch.Tensor,
260
+ g: torch.Tensor,
261
+ beta: torch.Tensor,
262
+ A: torch.Tensor,
263
+ h: torch.Tensor,
264
+ do: torch.Tensor,
265
+ dh: torch.Tensor,
266
+ dv: torch.Tensor,
267
+ scale: float | None = None,
268
+ state_v_first: bool = False,
269
+ chunk_size: int = 64,
270
+ ):
271
+ """Drop-in for bwd_wy.chunk_kda_bwd_wy_dqkg_wide: returns (dq, dk, dv2, db, dg, dA).
272
+
273
+ Off the supported shape or below the CTA floor it IS that function."""
274
+ B, T, H, K, HV, V = *k.shape, v.shape[2], v.shape[-1]
275
+ BT = chunk_size
276
+ NT = triton.cdiv(T, BT)
277
+ if not _supported(K, V, BT, NT * B * HV):
278
+ return chunk_kda_bwd_wy_dqkg_wide(
279
+ q=q, k=k, v=v, v_new=v_new, g=g, beta=beta, A=A, h=h, do=do, dh=dh, dv=dv,
280
+ scale=scale, state_v_first=state_v_first, chunk_size=chunk_size,
281
+ )
282
+ if scale is None:
283
+ scale = K ** -0.5
284
+
285
+ # dq, dk are allocated at HV; the caller reduces to H if GVA. dq/dk/dg double as the
286
+ # parking space for the raw transposed accumulators (fp32, overwritten in place).
287
+ dq = g.new_empty(B, T, HV, K, dtype=torch.float)
288
+ dk = g.new_empty(B, T, HV, K, dtype=torch.float)
289
+ dg = g.new_empty(B, T, HV, K, dtype=torch.float)
290
+ dv2 = torch.empty_like(v)
291
+ db = torch.empty(B, T, HV, device=g.device, dtype=torch.float)
292
+ dA = torch.empty_like(A, dtype=torch.float)
293
+ dgk_buf = torch.empty(B, NT, HV, K, device=g.device, dtype=torch.float)
294
+ # main -> side hand-off: -dw.bf16 and kg.bf16, the dA dot's operands
295
+ dw_buf = torch.empty(B, T, HV, K, device=g.device, dtype=A.dtype)
296
+ kg_buf = torch.empty(B, T, HV, K, device=g.device, dtype=A.dtype)
297
+
298
+ grid = (NT, B * HV)
299
+ chunk_kda_bwd_kernel_wy_t_main[grid](
300
+ q, k, v_new, g, beta, A, h, do, dh, dq, dk, dv, dg, db, dw_buf, kg_buf, dgk_buf, scale, T,
301
+ H=H, HV=HV, K=K, V=V, BT=BT, BV=MAIN_BV, KS=MAIN_KS, STATE_V_FIRST=state_v_first,
302
+ num_warps=8, num_stages=MAIN_STAGES, maxnreg=128,
303
+ )
304
+ chunk_kda_bwd_kernel_wy_t_side[grid](
305
+ v, beta, A, dv, dw_buf, kg_buf, dv2, db, dA, T,
306
+ HV=HV, K=K, V=V, BT=BT, BV=SIDE_BV, KD=SIDE_KD,
307
+ num_warps=SIDE_WARPS, num_stages=3, maxnreg=SIDE_MAXNREG,
308
+ )
309
+ return dq, dk, dv2, db, dg, dA