sparse-delta-memory 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. sparse_delta_memory/__init__.py +13 -0
  2. sparse_delta_memory/backward.py +367 -0
  3. sparse_delta_memory/cache.py +70 -0
  4. sparse_delta_memory/cuda/__init__.py +3 -0
  5. sparse_delta_memory/cuda/_build.py +254 -0
  6. sparse_delta_memory/cuda/_device.py +158 -0
  7. sparse_delta_memory/cuda/_devmemo.py +64 -0
  8. sparse_delta_memory/cuda/_image.py +128 -0
  9. sparse_delta_memory/cuda/bwd_steps_cuda.py +111 -0
  10. sparse_delta_memory/cuda/gather_cuda.py +796 -0
  11. sparse_delta_memory/cuda/gather_ns_cuda.py +134 -0
  12. sparse_delta_memory/cuda/gx_gds_cuda.py +1297 -0
  13. sparse_delta_memory/cuda/gx_gemm_cuda.py +857 -0
  14. sparse_delta_memory/cuda/gx_pa2_cuda.py +1320 -0
  15. sparse_delta_memory/cuda/mover_da2_cuda.py +1802 -0
  16. sparse_delta_memory/cuda/mover_fx_cuda.py +2229 -0
  17. sparse_delta_memory/cuda/pa_dc_cuda.py +1500 -0
  18. sparse_delta_memory/cuda/phase1_bwd_cuda.py +852 -0
  19. sparse_delta_memory/cuda/phase1_common.cuh +181 -0
  20. sparse_delta_memory/cuda/phase1_cuda.py +3801 -0
  21. sparse_delta_memory/cuda/product_key_topk_anyf_cuda.py +668 -0
  22. sparse_delta_memory/cuda/product_key_topk_cuda.py +735 -0
  23. sparse_delta_memory/cuda/px_qrec_cuda.py +167 -0
  24. sparse_delta_memory/cuda/sdm_copy.cuh +90 -0
  25. sparse_delta_memory/cuda/sdm_plancap.cuh +150 -0
  26. sparse_delta_memory/cuda_graph.py +605 -0
  27. sparse_delta_memory/decode.py +140 -0
  28. sparse_delta_memory/forward.py +464 -0
  29. sparse_delta_memory/layer.py +663 -0
  30. sparse_delta_memory/memory_ops.py +1069 -0
  31. sparse_delta_memory/memory_stats.py +221 -0
  32. sparse_delta_memory/movers.py +198 -0
  33. sparse_delta_memory/phase2_kernels.py +267 -0
  34. sparse_delta_memory/product_key_topk.py +113 -0
  35. sparse_delta_memory/wy_solve.py +273 -0
  36. sparse_delta_memory-2.0.0.dist-info/METADATA +482 -0
  37. sparse_delta_memory-2.0.0.dist-info/RECORD +40 -0
  38. sparse_delta_memory-2.0.0.dist-info/WHEEL +5 -0
  39. sparse_delta_memory-2.0.0.dist-info/licenses/LICENSE +407 -0
  40. sparse_delta_memory-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,852 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """phase1_bwd_cuda: the backward of Phase 1 (phase1_cuda): gradients of k_val, q_val and the gates.
3
+
4
+ It reads the forward's sorted payload, so it neither sorts nor searches. With D(p, r) the
5
+ decay between entries p < r of one group and i, j the tokens of the two entries:
6
+
7
+ d_row[r] = sum over earlier K p of d_a[i, j] v_p D(p, r) (K entry r)
8
+ d_col[r] = sum over later K s of d_a[j, i] v_s D(r, s) (K entry r)
9
+ d_kvqk[r] = sum over later Q s of d_qk[j, i] v_s D(r, s) (K entry r)
10
+ d_qv[r] = sum over earlier K p of d_qk[i, j] v_p D(p, r) (Q entry r)
11
+
12
+ Every sum has one writer and a fixed order, so the backward is deterministic. The
13
+ per-entry chain then gives the value gradients and the adjoints of the log-cumsums and of
14
+ rex, and two segmented scans turn those into the gradient of each entry's log-decay x:
15
+ the adjoints of the log-cumsums at or after it (log-cumsums are inclusive sums of x) plus
16
+ the adjoints of rex before it (rex is an exclusive reverse sum).
17
+
18
+ Kernels, one block per chunk: the pair kernel recomputes the log-cumsums and pulls the
19
+ sums (one block per product when the two triangles do not fit together), and the gate
20
+ kernel runs the chain and the scans. Launches use the current stream, allocate through
21
+ the caching allocator and do not synchronize, so they can be captured in a CUDA graph.
22
+
23
+ The source also holds the deterministic-mode variant of both kernels (preprocessor variant P1_BWD_DET): the same sums
24
+ in the same order, hence the same bits, with more latency hidden: the recompute fetches the entries' values before its
25
+ block scan, and the pulls broadcast the before / after batches of a window's first and last group from shared memory
26
+ (one 16-byte load per entry instead of four shuffles) when that buffer costs no resident block. The extension compiles
27
+ the source twice, without and with P1_BWD_DET, as two units of one module; phase1_backward_det takes the variant at the
28
+ chunk sizes where it is faster."""
29
+
30
+ from __future__ import annotations
31
+
32
+ import functools
33
+ import os
34
+
35
+ import torch
36
+
37
+ from ._build import load_cuda_extension
38
+ from ._device import smem_cap
39
+
40
+ __all__ = ["phase1_backward", "phase1_backward_det"]
41
+
42
+ _HEADER = "phase1_common.cuh"
43
+
44
+ _ARGS = """(
45
+ at::Tensor payload, at::Tensor log_k, at::Tensor log_q, at::Tensor rex, at::Tensor k_val,
46
+ at::Tensor q_val, at::Tensor g, at::Tensor d_a, at::Tensor d_qk, at::Tensor d_k_eff,
47
+ at::Tensor d_k_pd, at::Tensor d_q_w, int64_t smem_cap);"""
48
+ _CPP_SOURCE = "std::vector<at::Tensor> p1_backward" + _ARGS + "\nstd::vector<at::Tensor> p1_backward_det" + _ARGS + "\n"
49
+
50
+ _CUDA_SOURCE = r"""
51
+ #include <torch/extension.h>
52
+ #include <ATen/cuda/CUDAContext.h>
53
+ #include <c10/cuda/CUDAException.h>
54
+ #include <c10/cuda/CUDAGuard.h>
55
+
56
+ #include <algorithm>
57
+
58
+ #include "phase1_common.cuh"
59
+
60
+ using namespace sdm_p1;
61
+
62
+ constexpr int kThreads = 512;
63
+ constexpr int kItems = 8;
64
+ constexpr int kTile = kThreads * kItems;
65
+ static_assert(kRowAlign % kItems == 0, "row padding");
66
+ // Shared memory of a block, so that two fit on an SM.
67
+ constexpr size_t kMaxSmemBytes = 112 * 1024;
68
+ constexpr uint32_t kFull = 0xFFFFFFFFu;
69
+
70
+ // The pair kernel keeps a copy of the payload whose bits 18-26 hold the number of K
71
+ // entries carrying an x at or before the entry in its group.
72
+ constexpr int kCountShift = 18;
73
+ __device__ __forceinline__ int count_of(uint32_t p) { return (p >> kCountShift) & 0x1FF; }
74
+
75
+ // Position of d_a[i, j], j < i, in a packed strictly lower triangle, and of d_qk[i, j],
76
+ // j <= i, in a packed lower triangle.
77
+ __device__ __forceinline__ int strict_at(int i, int j) { return i * (i - 1) / 2 + j; }
78
+ __device__ __forceinline__ int lower_at(int i, int j) { return i * (i + 1) / 2 + j; }
79
+
80
+ // 2^x, flushing subnormal results to zero: such decays are far below the precision of
81
+ // the sums they enter.
82
+ __device__ __forceinline__ float exp2_ftz(float x) {
83
+ float y;
84
+ asm("ex2.approx.ftz.f32 %0, %1;" : "=f"(y) : "f"(x));
85
+ return y;
86
+ }
87
+
88
+ // Compensated running sum (value s + c): a pulled sum can have hundreds of terms whose
89
+ // cancellation the gate scans would otherwise amplify. Terms arrive as plain partial sums
90
+ // of at most 32.
91
+ struct CompSum {
92
+ float s = 0.0f;
93
+ float c = 0.0f;
94
+
95
+ __device__ __forceinline__ void add(float t) {
96
+ const float sum = s + t;
97
+ const float bb = sum - s;
98
+ c += (s - (sum - bb)) + (t - bb);
99
+ s = sum;
100
+ }
101
+ __device__ __forceinline__ float value() const { return s + c; }
102
+ };
103
+
104
+ // Sorted-order rows [N, row] of the pulled sums: `grad`, their part of the value gradient,
105
+ // and `log`, their part of the log-cumsum adjoint (divided by the value). One plane per
106
+ // block kind of the pair kernel.
107
+ struct Plane {
108
+ float* grad;
109
+ float* log;
110
+ };
111
+ struct Planes {
112
+ Plane p[2];
113
+ };
114
+
115
+ // A sorted entry as the pulls read it: payload, log-cumsum in log2 units (hi + lo), value.
116
+ #if defined(P1_BWD_DET)
117
+ #define Entry EntryDet // the deterministic-mode unit's own types (both units are linked into one extension)
118
+ #define Puller PullerDet
119
+ struct __align__(16) Entry {
120
+ #else
121
+ struct Entry {
122
+ #endif
123
+ uint32_t pay;
124
+ float hi;
125
+ float lo;
126
+ float v;
127
+ };
128
+
129
+ __device__ __forceinline__ Entry shfl_entry(const Entry& e, int src) {
130
+ return {__shfl_sync(kFull, e.pay, src), __shfl_sync(kFull, e.hi, src),
131
+ __shfl_sync(kFull, e.lo, src), __shfl_sync(kFull, e.v, src)};
132
+ }
133
+ __device__ __forceinline__ Entry shfl_entry_up(const Entry& e, int d) {
134
+ return {__shfl_up_sync(kFull, e.pay, d), __shfl_up_sync(kFull, e.hi, d),
135
+ __shfl_up_sync(kFull, e.lo, d), __shfl_up_sync(kFull, e.v, d)};
136
+ }
137
+
138
+ // One entry's view of its group, pairing it with earlier K entries (its row of d_a or d_qk
139
+ // at `row`) and, for a K entry, with later entries (column i of d_a or d_qk). `tri` holds
140
+ // the packed d_a triangle, then the d_qk one at `qk`. Weights are d * decay, or 0 for a
141
+ // pair the block does not pull. A pair with no x between its entries (a Q entry and the
142
+ // last K entry of its slot at or before its token) has decay exactly 1 whatever the gates,
143
+ // so its term enters the value gradient but not the log-cumsum adjoint, where it would
144
+ // cancel against itself.
145
+ struct Puller {
146
+ Entry own;
147
+ const float* tri;
148
+ int qk;
149
+ int row; // offset of this entry's row, when `pulls_back`
150
+ bool pulls_back; // a K entry pulling from d_a, or a Q entry pulling from d_qk
151
+ bool pulls_later; // a K entry
152
+ bool do_a, do_qk;
153
+
154
+ __device__ __forceinline__ float weight_earlier(const Entry& e, bool pair) const {
155
+ const int j = token_of(e.pay);
156
+ if (!pair || !pulls_back || !is_k(e.pay) || (!is_q(own.pay) && j == token_of(own.pay))) {
157
+ return 0.0f;
158
+ }
159
+ return tri[row + j] * exp2_ftz(log2_decay(own.hi, own.lo, e.hi, e.lo));
160
+ }
161
+
162
+ __device__ __forceinline__ float weight_later(const Entry& e, bool pair) const {
163
+ const int i = token_of(own.pay), j = token_of(e.pay);
164
+ const bool q = is_q(e.pay);
165
+ if (!pair || !pulls_later || (q ? !do_qk : !do_a || j == i)) return 0.0f;
166
+ return tri[q ? qk + lower_at(j, i) : strict_at(j, i)] *
167
+ exp2_ftz(log2_decay(e.hi, e.lo, own.hi, own.lo));
168
+ }
169
+ };
170
+
171
+ // One block per chunk, or per (chunk, product) when `split`: even blocks pull from d_a
172
+ // into plane 0, odd blocks from d_qk into plane 1. The block first recomputes the chunk's
173
+ // sorted log-cumsums with the forward's scan, the values and the counted payloads, into its
174
+ // slice of `work`.
175
+ // Each warp then pulls for windows of 32 consecutive entries, one per lane, in a fixed
176
+ // order. A pair inside the window is visited once, by its later entry, which sends the
177
+ // earlier entry its share through a shuffle. The part of the window's first (last) group
178
+ // before (after) the window is read in coalesced batches of 32 and broadcast. The pulls
179
+ // are latency bound, hence three blocks per SM.
180
+ template <typename ValT>
181
+ #if defined(P1_BWD_DET)
182
+ __global__ void __launch_bounds__(kThreads, 3) p1_bwd_pairs_det_kernel(
183
+ #else
184
+ __global__ void __launch_bounds__(kThreads, 3) p1_bwd_pairs_kernel(
185
+ #endif
186
+ const uint32_t* __restrict__ pay, const float* __restrict__ g,
187
+ const ValT* __restrict__ k_val, const ValT* __restrict__ q_val,
188
+ const float* __restrict__ d_a, const float* __restrict__ d_qk, float* work,
189
+ #if defined(P1_BWD_DET)
190
+ Planes planes, int C, int W, int R, int row, int g_width, int g_lane_stride, bool split, int sb_off) {
191
+ // 16-byte aligned base (the batch buffer at tri + sb_off holds 16-byte Entry records).
192
+ extern __shared__ __align__(16) float tri[];
193
+ #else
194
+ Planes planes, int C, int W, int R, int row, int g_width, int g_lane_stride, bool split) {
195
+ extern __shared__ float tri[];
196
+ #endif
197
+ __shared__ BlockScanSmem scan_smem;
198
+ const int c = split ? blockIdx.x >> 1 : blockIdx.x;
199
+ const bool do_a = !split || !(blockIdx.x & 1);
200
+ const bool do_qk = !split || (blockIdx.x & 1);
201
+ const int n = C * (W + R);
202
+ const int tiles = (n + kTile - 1) / kTile;
203
+ const int64_t base = static_cast<int64_t>(c) * row;
204
+ const uint32_t* pay_row = pay + base;
205
+ const float* g_chunk = g + static_cast<int64_t>(c) * C * g_width;
206
+ const ValT* kv_chunk = k_val + static_cast<int64_t>(c) * C * W;
207
+ const ValT* qv_chunk = q_val + static_cast<int64_t>(c) * C * R;
208
+ float* hi_row = work + static_cast<int64_t>(blockIdx.x) * 4 * row;
209
+ float* lo_row = hi_row + row;
210
+ float* v_row = lo_row + row;
211
+ uint32_t* pc_row = reinterpret_cast<uint32_t*>(v_row + row);
212
+ const int qk = do_a ? C * (C - 1) / 2 : 0;
213
+
214
+ SegSum carry{0.0f, 0.0f, 0u};
215
+ for (int tile = 0; tile < tiles; ++tile) {
216
+ const int r0 = tile * kTile + threadIdx.x * kItems;
217
+ uint32_t p[kItems];
218
+ if (r0 < row) load_items(pay_row + r0, p);
219
+ const uint32_t prev = r0 > 0 && r0 <= n ? pay_row[r0 - 1] : kInvalid;
220
+ SegSum item[kItems];
221
+ #pragma unroll
222
+ for (int k = 0; k < kItems; ++k) {
223
+ if (r0 + k >= n) p[k] = kInvalid | kHead;
224
+ const bool carries = is_k(p[k]) && !repeats_k(k ? p[k - 1] : prev, p[k]);
225
+ const float x = carries
226
+ ? g_chunk[token_of(p[k]) * g_width + lane_of(p[k]) * g_lane_stride] : 0.0f;
227
+ item[k] = {x, 0.0f, (carries ? 1u : 0u) | (p[k] & kHead)};
228
+ }
229
+ #if defined(P1_BWD_DET)
230
+ // The values are gathered before the block scan (their latency overlaps it).
231
+ float hi[kItems], lo[kItems], v[kItems];
232
+ #pragma unroll
233
+ for (int k = 0; k < kItems; ++k) {
234
+ const uint32_t pk = p[k];
235
+ v[k] = is_k(pk) ? to_float(kv_chunk[token_of(pk) * W + lane_of(pk)])
236
+ : is_q(pk) ? to_float(qv_chunk[token_of(pk) * R + lane_of(pk)]) : 0.0f;
237
+ }
238
+ #endif
239
+ SegSum own = item[0];
240
+ #pragma unroll
241
+ for (int k = 1; k < kItems; ++k) own = seg_combine(own, item[k]);
242
+ SegSum run = block_scan_exclusive<kThreads, false>(own, carry, scan_smem);
243
+ #if !defined(P1_BWD_DET)
244
+ float hi[kItems], lo[kItems], v[kItems];
245
+ #endif
246
+ uint32_t pc[kItems];
247
+ #pragma unroll
248
+ for (int k = 0; k < kItems; ++k) {
249
+ run = seg_combine(run, item[k]);
250
+ log2_parts(run, hi[k], lo[k]);
251
+ const uint32_t pk = p[k];
252
+ pc[k] = pk | seg_count(run) << kCountShift;
253
+ #if !defined(P1_BWD_DET)
254
+ v[k] = is_k(pk) ? to_float(kv_chunk[token_of(pk) * W + lane_of(pk)])
255
+ : is_q(pk) ? to_float(qv_chunk[token_of(pk) * R + lane_of(pk)]) : 0.0f;
256
+ #endif
257
+ }
258
+ if (r0 < row) {
259
+ store_items(hi_row + r0, hi);
260
+ store_items(lo_row + r0, lo);
261
+ store_items(v_row + r0, v);
262
+ store_items(pc_row + r0, pc);
263
+ }
264
+ }
265
+ const int64_t mat = static_cast<int64_t>(c) * C * C;
266
+ for (int e = threadIdx.x; e < C * C; e += kThreads) {
267
+ const int i = e / C;
268
+ const int j = e - i * C;
269
+ if (do_a && j < i) tri[strict_at(i, j)] = d_a[mat + e];
270
+ if (do_qk && j <= i) tri[qk + lower_at(i, j)] = d_qk[mat + e];
271
+ }
272
+ __syncthreads();
273
+
274
+ const Plane out = split && (blockIdx.x & 1) ? planes.p[1] : planes.p[0];
275
+ const int lane = threadIdx.x & 31;
276
+ auto load = [&](int r) {
277
+ return r < row ? Entry{pc_row[r], hi_row[r], lo_row[r], v_row[r]}
278
+ : Entry{kInvalid | kHead, 0.0f, 0.0f, 0.0f};
279
+ };
280
+ for (int w0 = (threadIdx.x >> 5) * 32; w0 < row; w0 += kThreads) {
281
+ const int r = w0 + lane;
282
+ const Entry own = load(r);
283
+ const int i = token_of(own.pay);
284
+ const bool q = is_q(own.pay);
285
+ const Puller pull{own, tri, qk, q ? qk + lower_at(i, 0) : strict_at(i, 0),
286
+ q ? do_qk : is_k(own.pay) && do_a, is_k(own.pay), do_a, do_qk};
287
+ // Lanes of this entry's group within the window: [first, last]; first < 0 (last > 31)
288
+ // when the group starts before (ends after) the window.
289
+ const uint32_t heads = __ballot_sync(kFull, is_head(own.pay));
290
+ const uint32_t upto = heads & (kFull >> (31 - lane));
291
+ const uint32_t after = heads & ~(kFull >> (31 - lane));
292
+ const int first = upto ? 31 - __clz(upto) : -1;
293
+ const int last = after ? __ffs(after) - 2 : 32;
294
+
295
+ CompSum back, fwd;
296
+ float exact = 0.0f;
297
+ // Pairs inside the window: lane r visits (r - d, r) and sends lane r - d its share.
298
+ const int span = pull.pulls_back ? lane - max(first, 0) : 0;
299
+ const int reach = __reduce_max_sync(kFull, span);
300
+ float back_part = 0.0f, fwd_part = 0.0f;
301
+ for (int d = 1; d <= reach; ++d) {
302
+ const Entry e = shfl_entry_up(own, d);
303
+ const float w = pull.weight_earlier(e, d <= span);
304
+ const bool is_exact = count_of(e.pay) == count_of(own.pay);
305
+ const float mine = w * e.v, theirs = w * own.v;
306
+ if (is_exact) {
307
+ exact += mine;
308
+ } else {
309
+ back_part += mine;
310
+ }
311
+ const float got = __shfl_down_sync(kFull, is_exact ? 0.0f : theirs, d);
312
+ const float got_exact = __shfl_down_sync(kFull, is_exact ? theirs : 0.0f, d);
313
+ if (lane + d < 32) {
314
+ fwd_part += got;
315
+ exact += got_exact;
316
+ }
317
+ }
318
+ back.add(back_part);
319
+ fwd.add(fwd_part);
320
+
321
+ #if defined(P1_BWD_DET)
322
+ // A warp's current before / after batch (32 entries) in dynamic shared memory after the triangles, when the host
323
+ // found room for it without losing resident blocks (sb_off >= 0); else the shuffles.
324
+ Entry* const sb = sb_off >= 0
325
+ ? reinterpret_cast<Entry*>(reinterpret_cast<char*>(tri) + sb_off) + (threadIdx.x >> 5) * 32 : nullptr;
326
+ #endif
327
+ if (!(heads & 1u)) {
328
+ const bool part = first < 0;
329
+ for (int p0 = w0 - 32;; p0 -= 32) {
330
+ const Entry batch = load(p0 + lane);
331
+ const uint32_t bh = __ballot_sync(kFull, is_head(batch.pay));
332
+ const int stop = bh ? 31 - __clz(bh) : 0;
333
+ #if defined(P1_BWD_DET)
334
+ if (sb != nullptr) {
335
+ __syncwarp(); // the previous batch's broadcasts are done
336
+ sb[lane] = batch;
337
+ __syncwarp();
338
+ }
339
+ #endif
340
+ float part_sum = 0.0f;
341
+ for (int s = 31; s >= stop; --s) {
342
+ #if defined(P1_BWD_DET)
343
+ // one broadcast load instead of four shuffles (same values)
344
+ const Entry e = sb != nullptr ? sb[s] : shfl_entry(batch, s);
345
+ #else
346
+ const Entry e = shfl_entry(batch, s);
347
+ #endif
348
+ const float t = pull.weight_earlier(e, part) * e.v;
349
+ if (count_of(e.pay) == count_of(own.pay)) {
350
+ exact += t;
351
+ } else {
352
+ part_sum += t;
353
+ }
354
+ }
355
+ back.add(part_sum);
356
+ if (bh) break;
357
+ }
358
+ }
359
+ if (w0 + 32 < n && !is_head(pay_row[w0 + 32])) {
360
+ const bool part = last > 31;
361
+ for (int p0 = w0 + 32; p0 < n; p0 += 32) {
362
+ const Entry batch = load(p0 + lane);
363
+ const uint32_t bh = __ballot_sync(kFull, is_head(batch.pay));
364
+ const int stop = bh ? __ffs(bh) - 1 : 32;
365
+ #if defined(P1_BWD_DET)
366
+ if (sb != nullptr) {
367
+ __syncwarp();
368
+ sb[lane] = batch;
369
+ __syncwarp();
370
+ }
371
+ #endif
372
+ float part_sum = 0.0f;
373
+ for (int s = 0; s < stop; ++s) {
374
+ #if defined(P1_BWD_DET)
375
+ const Entry e = sb != nullptr ? sb[s] : shfl_entry(batch, s);
376
+ #else
377
+ const Entry e = shfl_entry(batch, s);
378
+ #endif
379
+ const float t = pull.weight_later(e, part) * e.v;
380
+ if (count_of(e.pay) == count_of(own.pay)) {
381
+ exact += t;
382
+ } else {
383
+ part_sum += t;
384
+ }
385
+ }
386
+ fwd.add(part_sum);
387
+ if (bh) break;
388
+ }
389
+ }
390
+ if (r < row) {
391
+ out.grad[base + r] = back.value() + fwd.value() + exact;
392
+ out.log[base + r] = back.value() - fwd.value();
393
+ }
394
+ }
395
+ }
396
+
397
+ // Per-entry outputs of one chunk: [C, W] K fields, [C, R] Q fields.
398
+ template <typename ValT>
399
+ struct ChunkGrads {
400
+ ValT* grad_kv;
401
+ ValT* grad_qv;
402
+ float* grad_g;
403
+
404
+ __host__ __device__ static size_t bytes(int C, int W, int R) {
405
+ return (sizeof(float) + sizeof(ValT)) * C * W + sizeof(ValT) * C * R;
406
+ }
407
+
408
+ // The tile laid out in `mem`, widest field first so that each field stays aligned.
409
+ __device__ static ChunkGrads carve(char* mem, int C, int W, int R) {
410
+ ChunkGrads o;
411
+ o.grad_g = reinterpret_cast<float*>(mem);
412
+ o.grad_kv = reinterpret_cast<ValT*>(o.grad_g + C * W);
413
+ o.grad_qv = o.grad_kv + C * W;
414
+ return o;
415
+ }
416
+
417
+ __device__ ChunkGrads at(int64_t kw, int64_t qr) const {
418
+ return {grad_kv + kw, grad_qv + qr, grad_g + kw};
419
+ }
420
+
421
+ // Copies this tile to `dst` with the whole block.
422
+ __device__ void copy_to(const ChunkGrads& dst, int cw, int cr) const {
423
+ for (int i = threadIdx.x; i < cw; i += blockDim.x) {
424
+ dst.grad_kv[i] = grad_kv[i];
425
+ dst.grad_g[i] = grad_g[i];
426
+ }
427
+ for (int i = threadIdx.x; i < cr; i += blockDim.x) dst.grad_qv[i] = grad_qv[i];
428
+ }
429
+ };
430
+
431
+ // Per-entry forward values and gradients of the forward outputs, [N, C, W] or [N, C, R].
432
+ template <typename ValT>
433
+ struct EntryInputs {
434
+ const ValT* k_val;
435
+ const ValT* q_val;
436
+ const float* log_k;
437
+ const float* log_q;
438
+ const float* rex;
439
+ const float* d_k_eff;
440
+ const float* d_k_pd;
441
+ const float* d_q_w;
442
+ };
443
+
444
+ // One block per chunk. Pass 1 runs forward over the row: the per-entry chain (value
445
+ // gradients, adjoint of the log-cumsum `dl` and of rex `dr`) and the exclusive scan of dr.
446
+ // Pass 2 runs backwards: the inclusive reverse scan of dl, which completes the gradient of
447
+ // each K entry's x. The last tile of pass 1 stays in registers for pass 2; the others park
448
+ // dl and the scan of dr in the plane-0 rows they consumed. The chunk's gradients are
449
+ // assembled in shared memory when they fit (`staged`) and written out contiguously.
450
+ template <typename ValT>
451
+ #if defined(P1_BWD_DET)
452
+ __global__ void __launch_bounds__(kThreads, 2) p1_bwd_gate_det_kernel(
453
+ #else
454
+ __global__ void __launch_bounds__(kThreads, 2) p1_bwd_gate_kernel(
455
+ #endif
456
+ const uint32_t* __restrict__ pay, Planes planes, int num_planes, EntryInputs<ValT> in,
457
+ ChunkGrads<ValT> out, int C, int W, int R, int row, bool staged) {
458
+ extern __shared__ __align__(16) char stage_mem[];
459
+ __shared__ BlockScanSmem scan_smem;
460
+ const int c = blockIdx.x;
461
+ const int n = C * (W + R);
462
+ const int tiles = (n + kTile - 1) / kTile;
463
+ const int64_t base = static_cast<int64_t>(c) * row;
464
+ const int64_t kw = static_cast<int64_t>(c) * C * W;
465
+ const int64_t qr = static_cast<int64_t>(c) * C * R;
466
+ const uint32_t* pay_row = pay + base;
467
+ float* park_dl = planes.p[0].grad + base;
468
+ float* park_dr = planes.p[0].log + base;
469
+ const ChunkGrads<ValT> chunk_out = out.at(kw, qr);
470
+ const ChunkGrads<ValT> dst = staged ? ChunkGrads<ValT>::carve(stage_mem, C, W, R) : chunk_out;
471
+
472
+ uint32_t p[kItems];
473
+ float dl[kItems];
474
+ float before[kItems];
475
+ SegSum carry{0.0f, 0.0f, 0u};
476
+ for (int tile = 0; tile < tiles; ++tile) {
477
+ const int r0 = tile * kTile + threadIdx.x * kItems;
478
+ float pg[kItems] = {}, pl[kItems] = {}; // pulled sums: grad and log parts
479
+ if (r0 < row) {
480
+ load_items(pay_row + r0, p);
481
+ #pragma unroll
482
+ for (int s = 0; s < 2; ++s) {
483
+ if (s == num_planes) break;
484
+ float a[kItems], b[kItems];
485
+ load_items(planes.p[s].grad + base + r0, a);
486
+ load_items(planes.p[s].log + base + r0, b);
487
+ #pragma unroll
488
+ for (int k = 0; k < kItems; ++k) {
489
+ pg[k] += a[k];
490
+ pl[k] += b[k];
491
+ }
492
+ }
493
+ }
494
+ SegSum item[kItems];
495
+ #pragma unroll
496
+ for (int k = 0; k < kItems; ++k) {
497
+ if (r0 + k >= n) p[k] = kInvalid | kHead;
498
+ const uint32_t pk = p[k];
499
+ const bool valid = !(pk & kInvalid);
500
+ float grad = 0.0f, dr = 0.0f;
501
+ dl[k] = 0.0f;
502
+ if (r0 + k >= n) {
503
+ } else if (pk & kIsQ) {
504
+ const int o = token_of(pk) * R + lane_of(pk);
505
+ if (valid) {
506
+ const float w = in.d_q_w[qr + o] * expf(in.log_q[qr + o]);
507
+ grad = pg[k] + w;
508
+ dl[k] = to_float(in.q_val[qr + o]) * (pl[k] + w);
509
+ }
510
+ dst.grad_qv[o] = from_float<ValT>(grad);
511
+ } else {
512
+ const int o = token_of(pk) * W + lane_of(pk);
513
+ if (valid) {
514
+ const float kv = to_float(in.k_val[kw + o]);
515
+ const float eff = in.d_k_eff[kw + o] * expf(in.log_k[kw + o]);
516
+ const float post = in.d_k_pd[kw + o] * expf(in.rex[kw + o]);
517
+ grad = pg[k] + eff + post;
518
+ dl[k] = kv * (pl[k] + eff);
519
+ dr = kv * post;
520
+ }
521
+ dst.grad_kv[o] = from_float<ValT>(grad);
522
+ }
523
+ item[k] = {dr, 0.0f, pk & kHead};
524
+ }
525
+ SegSum own = item[0];
526
+ #pragma unroll
527
+ for (int k = 1; k < kItems; ++k) own = seg_combine(own, item[k]);
528
+ SegSum run = block_scan_exclusive<kThreads, false>(own, carry, scan_smem);
529
+ #pragma unroll
530
+ for (int k = 0; k < kItems; ++k) {
531
+ before[k] = (item[k].kf & kHead) ? 0.0f : seg_value(run);
532
+ run = seg_combine(run, item[k]);
533
+ }
534
+ if (tile < tiles - 1 && r0 < row) {
535
+ store_items(park_dl + r0, dl);
536
+ store_items(park_dr + r0, before);
537
+ }
538
+ }
539
+
540
+ carry = {0.0f, 0.0f, 0u};
541
+ for (int tile = tiles - 1; tile >= 0; --tile) {
542
+ const int r0 = tile * kTile + threadIdx.x * kItems;
543
+ if (tile < tiles - 1 && r0 < row) {
544
+ load_items(pay_row + r0, p);
545
+ load_items(park_dl + r0, dl);
546
+ load_items(park_dr + r0, before);
547
+ }
548
+ const uint32_t next = r0 + kItems < n ? pay_row[r0 + kItems] : kHead;
549
+ const uint32_t prev = r0 > 0 && r0 <= n ? pay_row[r0 - 1] : kInvalid;
550
+ SegSum item[kItems];
551
+ #pragma unroll
552
+ for (int k = 0; k < kItems; ++k) {
553
+ if (r0 + k >= n) p[k] = kInvalid | kHead;
554
+ item[k] = {dl[k], 0.0f, (k + 1 < kItems ? p[k + 1] : next) & kHead};
555
+ }
556
+ SegSum own = item[kItems - 1];
557
+ #pragma unroll
558
+ for (int k = kItems - 2; k >= 0; --k) own = seg_combine(own, item[k]);
559
+ SegSum run = block_scan_exclusive<kThreads, true>(own, carry, scan_smem);
560
+ #pragma unroll
561
+ for (int k = kItems - 1; k >= 0; --k) {
562
+ run = seg_combine(run, item[k]);
563
+ const uint32_t pk = p[k];
564
+ if (r0 + k >= n || (pk & kIsQ)) continue;
565
+ const bool owns_x = is_k(pk) && !repeats_k(k ? p[k - 1] : prev, pk);
566
+ dst.grad_g[token_of(pk) * W + lane_of(pk)] =
567
+ owns_x ? run.x + (run.c + before[k]) : 0.0f;
568
+ }
569
+ }
570
+ if (staged) {
571
+ __syncthreads();
572
+ dst.copy_to(chunk_out, C * W, C * R);
573
+ }
574
+ }
575
+
576
+ #if defined(P1_BWD_DET)
577
+ // The pair kernel's dynamic shared memory plan. The 8 KB batch buffer (one 16-byte Entry per thread) goes at the
578
+ // 16-byte aligned end of the triangles only when the CUDA occupancy calculator (allocation granularity, the per-block
579
+ // reservation, the static shared memory and the registers included) gives at least as many resident blocks per SM with
580
+ // it as without it, and the block still fits the device's opt-in maximum. Speed only: both give the same bits.
581
+ struct PairsDetConfig {
582
+ bool use_sb;
583
+ size_t sb_off, dyn_bytes;
584
+ int blocks_tri, blocks_sb;
585
+ };
586
+
587
+ #endif
588
+ template <typename ValT>
589
+ #if defined(P1_BWD_DET)
590
+ PairsDetConfig pairs_det_config(size_t tri_bytes, int64_t smem_cap) {
591
+ PairsDetConfig cfg{false, (tri_bytes + 15) / 16 * 16, tri_bytes, 0, 0};
592
+ const size_t sb_dyn = cfg.sb_off + sizeof(Entry) * kThreads;
593
+ int dev = 0, optin = 0;
594
+ C10_CUDA_CHECK(cudaGetDevice(&dev));
595
+ C10_CUDA_CHECK(cudaDeviceGetAttribute(&optin, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev));
596
+ optin = static_cast<int>(std::min<int64_t>(optin, smem_cap)); // the caller's plan limit (_device.smem_cap)
597
+ cudaFuncAttributes fa;
598
+ C10_CUDA_CHECK(cudaFuncGetAttributes(&fa, p1_bwd_pairs_det_kernel<ValT>));
599
+ if (sb_dyn + fa.sharedSizeBytes > static_cast<size_t>(optin)) return cfg;
600
+ // The calculator honours the kernel's dynamic smem limit: raise it to the larger size before both queries.
601
+ C10_CUDA_CHECK(cudaFuncSetAttribute(p1_bwd_pairs_det_kernel<ValT>,
602
+ cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast<int>(sb_dyn)));
603
+ C10_CUDA_CHECK(cudaOccupancyMaxActiveBlocksPerMultiprocessor(&cfg.blocks_tri, p1_bwd_pairs_det_kernel<ValT>,
604
+ kThreads, tri_bytes));
605
+ C10_CUDA_CHECK(cudaOccupancyMaxActiveBlocksPerMultiprocessor(&cfg.blocks_sb, p1_bwd_pairs_det_kernel<ValT>,
606
+ kThreads, sb_dyn));
607
+ if (cfg.blocks_sb > 0 && cfg.blocks_sb >= cfg.blocks_tri) {
608
+ cfg.use_sb = true;
609
+ cfg.dyn_bytes = sb_dyn;
610
+ }
611
+ return cfg;
612
+ }
613
+
614
+ template <typename ValT>
615
+ std::vector<at::Tensor> run_backward_det(
616
+ #else
617
+ std::vector<at::Tensor> run_backward(
618
+ #endif
619
+ const at::Tensor& payload, const at::Tensor& log_k, const at::Tensor& log_q,
620
+ const at::Tensor& rex, const at::Tensor& k_val, const at::Tensor& q_val,
621
+ const at::Tensor& g, const at::Tensor& d_a, const at::Tensor& d_qk,
622
+ const at::Tensor& d_k_eff, const at::Tensor& d_k_pd, const at::Tensor& d_q_w, int64_t smem_cap) {
623
+ const int64_t N = k_val.size(0);
624
+ const int C = k_val.size(1), W = k_val.size(2), R = q_val.size(2);
625
+ const int row = payload.size(1);
626
+ const auto f32 = k_val.options().dtype(at::kFloat);
627
+ const auto stream = at::cuda::getCurrentCUDAStream();
628
+ const auto* pay = reinterpret_cast<const uint32_t*>(payload.data_ptr<int32_t>());
629
+
630
+ const size_t strict_bytes = sizeof(float) * C * (C - 1) / 2;
631
+ const size_t lower_bytes = sizeof(float) * C * (C + 1) / 2;
632
+ const bool split = strict_bytes + lower_bytes > kMaxSmemBytes;
633
+ const int num_planes = split ? 2 : 1;
634
+ const size_t tri_bytes = split ? lower_bytes : strict_bytes + lower_bytes;
635
+ const int blocks = N * num_planes;
636
+ auto work = at::empty({blocks, 4, row}, f32);
637
+ auto sums = at::empty({num_planes, 2, N, row}, f32);
638
+ Planes planes{};
639
+ for (int s = 0; s < num_planes; ++s) {
640
+ planes.p[s] = {sums[s][0].data_ptr<float>(), sums[s][1].data_ptr<float>()};
641
+ }
642
+
643
+ #if defined(P1_BWD_DET)
644
+ // The shared-memory batch buffers (8 KB) go after the triangles only when they cost no resident block (speed only:
645
+ // both paths give the same bits); residency from the CUDA occupancy calculator (see pairs_det_config).
646
+ const PairsDetConfig cfg = pairs_det_config<ValT>(tri_bytes, smem_cap);
647
+ const bool use_sb = cfg.use_sb;
648
+ const size_t sb_off = cfg.sb_off;
649
+ const size_t dyn_bytes = cfg.dyn_bytes;
650
+ C10_CUDA_CHECK(cudaFuncSetAttribute(p1_bwd_pairs_det_kernel<ValT>,
651
+ cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast<int>(dyn_bytes)));
652
+ #else
653
+ C10_CUDA_CHECK(cudaFuncSetAttribute(p1_bwd_pairs_kernel<ValT>,
654
+ cudaFuncAttributeMaxDynamicSharedMemorySize, tri_bytes));
655
+ #endif
656
+ const int g_width = g.size(2);
657
+ #if defined(P1_BWD_DET)
658
+ p1_bwd_pairs_det_kernel<ValT><<<blocks, kThreads, dyn_bytes, stream>>>(
659
+ #else
660
+ p1_bwd_pairs_kernel<ValT><<<blocks, kThreads, tri_bytes, stream>>>(
661
+ #endif
662
+ pay, g.data_ptr<float>(), reinterpret_cast<const ValT*>(k_val.data_ptr()),
663
+ reinterpret_cast<const ValT*>(q_val.data_ptr()), d_a.data_ptr<float>(),
664
+ d_qk.data_ptr<float>(), work.data_ptr<float>(), planes, C, W, R, row, g_width,
665
+ #if defined(P1_BWD_DET)
666
+ g_width == 1 ? 0 : 1, split, use_sb ? static_cast<int>(sb_off) : -1);
667
+ #else
668
+ g_width == 1 ? 0 : 1, split);
669
+ #endif
670
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
671
+
672
+ auto grad_kv = at::empty_like(k_val);
673
+ auto grad_qv = at::empty_like(q_val);
674
+ auto grad_g = at::empty({N, C, W}, f32);
675
+ const ChunkGrads<ValT> out{reinterpret_cast<ValT*>(grad_kv.data_ptr()),
676
+ reinterpret_cast<ValT*>(grad_qv.data_ptr()),
677
+ grad_g.data_ptr<float>()};
678
+ const EntryInputs<ValT> in{
679
+ reinterpret_cast<const ValT*>(k_val.data_ptr()),
680
+ reinterpret_cast<const ValT*>(q_val.data_ptr()), log_k.data_ptr<float>(),
681
+ log_q.data_ptr<float>(), rex.data_ptr<float>(), d_k_eff.data_ptr<float>(),
682
+ d_k_pd.data_ptr<float>(), d_q_w.data_ptr<float>()};
683
+ const size_t stage_bytes = ChunkGrads<ValT>::bytes(C, W, R);
684
+ // The outputs are staged in shared memory where they fit: at most kMaxSmemBytes (the tuned threshold) and at most
685
+ // the device's opt-in limit (the caller's _device.smem_cap) minus the gate kernel's static shared memory (RTX parts:
686
+ // bf16, C = 128, W = R = 112 needs 114,688 B > 101,376 B). Speed only: both paths give the same bits.
687
+ cudaFuncAttributes gate_attr{};
688
+ #if defined(P1_BWD_DET)
689
+ C10_CUDA_CHECK(cudaFuncGetAttributes(&gate_attr, p1_bwd_gate_det_kernel<ValT>));
690
+ #else
691
+ C10_CUDA_CHECK(cudaFuncGetAttributes(&gate_attr, p1_bwd_gate_kernel<ValT>));
692
+ #endif
693
+ const size_t gate_cap = smem_cap > static_cast<int64_t>(gate_attr.sharedSizeBytes)
694
+ ? static_cast<size_t>(smem_cap) - gate_attr.sharedSizeBytes : 0;
695
+ const bool stage_out = stage_bytes <= std::min(kMaxSmemBytes, gate_cap);
696
+ const size_t gate_smem = stage_out ? stage_bytes : 0;
697
+ #if defined(P1_BWD_DET)
698
+ C10_CUDA_CHECK(cudaFuncSetAttribute(p1_bwd_gate_det_kernel<ValT>,
699
+ #else
700
+ C10_CUDA_CHECK(cudaFuncSetAttribute(p1_bwd_gate_kernel<ValT>,
701
+ #endif
702
+ cudaFuncAttributeMaxDynamicSharedMemorySize, gate_smem));
703
+ #if defined(P1_BWD_DET)
704
+ p1_bwd_gate_det_kernel<ValT><<<N, kThreads, gate_smem, stream>>>(
705
+ #else
706
+ p1_bwd_gate_kernel<ValT><<<N, kThreads, gate_smem, stream>>>(
707
+ #endif
708
+ pay, planes, num_planes, in, out, C, W, R, row, stage_out);
709
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
710
+ return {grad_kv, grad_qv, grad_g};
711
+ }
712
+
713
+ #if defined(P1_BWD_DET)
714
+ std::vector<at::Tensor> p1_backward_det(
715
+ #else
716
+ std::vector<at::Tensor> p1_backward(
717
+ #endif
718
+ at::Tensor payload, at::Tensor log_k, at::Tensor log_q, at::Tensor rex, at::Tensor k_val,
719
+ at::Tensor q_val, at::Tensor g, at::Tensor d_a, at::Tensor d_qk, at::Tensor d_k_eff,
720
+ at::Tensor d_k_pd, at::Tensor d_q_w, int64_t smem_cap) {
721
+ TORCH_CHECK(k_val.dim() == 3 && q_val.dim() == 3,
722
+ #if defined(P1_BWD_DET)
723
+ "p1_backward_det: expected k_val [N, C, W] and q_val [N, C, R]");
724
+ #else
725
+ "p1_backward: expected k_val [N, C, W] and q_val [N, C, R]");
726
+ #endif
727
+ const int64_t N = k_val.size(0), C = k_val.size(1), W = k_val.size(2), R = q_val.size(2);
728
+ TORCH_CHECK(N >= 1 && C >= 1 && C <= 256 && W >= 1 && W <= 256 && R >= 1 && R <= 256,
729
+ #if defined(P1_BWD_DET)
730
+ "p1_backward_det: needs chunk size 1..256 and 1..256 write/read slots per token");
731
+ #else
732
+ "p1_backward: needs chunk size 1..256 and 1..256 write/read slots per token");
733
+ #endif
734
+ const int64_t row = (C * (W + R) + kRowAlign - 1) / kRowAlign * kRowAlign;
735
+ auto is = [](const at::Tensor& t, std::initializer_list<int64_t> shape) {
736
+ return t.sizes() == at::IntArrayRef(shape);
737
+ };
738
+ TORCH_CHECK(q_val.size(0) == N && q_val.size(1) == C && is(payload, {N, row}) &&
739
+ is(log_k, {N, C, W}) && is(rex, {N, C, W}) && is(d_k_eff, {N, C, W}) &&
740
+ is(d_k_pd, {N, C, W}) && is(log_q, {N, C, R}) && is(d_q_w, {N, C, R}) &&
741
+ is(d_a, {N, C, C}) && is(d_qk, {N, C, C}) && g.dim() == 3 && g.size(0) == N &&
742
+ g.size(1) == C && (g.size(2) == 1 || g.size(2) == W),
743
+ #if defined(P1_BWD_DET)
744
+ "p1_backward_det: inconsistent shapes");
745
+ #else
746
+ "p1_backward: inconsistent shapes");
747
+ #endif
748
+ TORCH_CHECK(k_val.scalar_type() == q_val.scalar_type() &&
749
+ (k_val.scalar_type() == at::kFloat || k_val.scalar_type() == at::kBFloat16),
750
+ #if defined(P1_BWD_DET)
751
+ "p1_backward_det: k_val and q_val must both be float32 or bfloat16");
752
+ TORCH_CHECK(payload.scalar_type() == at::kInt, "p1_backward_det: payload must be int32");
753
+ #else
754
+ "p1_backward: k_val and q_val must both be float32 or bfloat16");
755
+ TORCH_CHECK(payload.scalar_type() == at::kInt, "p1_backward: payload must be int32");
756
+ #endif
757
+ for (const auto* t : {&log_k, &log_q, &rex, &g, &d_a, &d_qk, &d_k_eff, &d_k_pd, &d_q_w}) {
758
+ TORCH_CHECK(t->scalar_type() == at::kFloat,
759
+ #if defined(P1_BWD_DET)
760
+ "p1_backward_det: log-cumsums, rex, gates and gradients must be float32");
761
+ #else
762
+ "p1_backward: log-cumsums, rex, gates and gradients must be float32");
763
+ #endif
764
+ }
765
+ for (const auto* t : {&payload, &log_k, &log_q, &rex, &k_val, &q_val, &g, &d_a, &d_qk,
766
+ &d_k_eff, &d_k_pd, &d_q_w}) {
767
+ TORCH_CHECK(t->is_cuda() && t->is_contiguous() && t->device() == k_val.device(),
768
+ #if defined(P1_BWD_DET)
769
+ "p1_backward_det: inputs must be contiguous tensors on one CUDA device");
770
+ #else
771
+ "p1_backward: inputs must be contiguous tensors on one CUDA device");
772
+ #endif
773
+ }
774
+ const c10::cuda::CUDAGuard guard(k_val.device());
775
+ return k_val.scalar_type() == at::kFloat
776
+ #if defined(P1_BWD_DET)
777
+ ? run_backward_det<float>(payload, log_k, log_q, rex, k_val, q_val, g, d_a, d_qk, d_k_eff,
778
+ #else
779
+ ? run_backward<float>(payload, log_k, log_q, rex, k_val, q_val, g, d_a, d_qk, d_k_eff,
780
+ #endif
781
+ d_k_pd, d_q_w, smem_cap)
782
+ #if defined(P1_BWD_DET)
783
+ : run_backward_det<__nv_bfloat16>(payload, log_k, log_q, rex, k_val, q_val, g, d_a, d_qk,
784
+ #else
785
+ : run_backward<__nv_bfloat16>(payload, log_k, log_q, rex, k_val, q_val, g, d_a, d_qk,
786
+ #endif
787
+ d_k_eff, d_k_pd, d_q_w, smem_cap);
788
+ }
789
+ """
790
+
791
+
792
+ @functools.cache
793
+ def _module():
794
+ with open(os.path.join(os.path.dirname(__file__), _HEADER), encoding="utf-8") as f:
795
+ header = f.read()
796
+ return load_cuda_extension(
797
+ "sdm_phase1_bwd",
798
+ cpp_sources=_CPP_SOURCE,
799
+ cuda_sources=(_CUDA_SOURCE, "#define P1_BWD_DET 1\n" + _CUDA_SOURCE),
800
+ functions=("p1_backward", "p1_backward_det"),
801
+ headers={_HEADER: header},
802
+ extra_cuda_cflags=("-O3",),
803
+ )
804
+
805
+
806
+ def phase1_backward(
807
+ payload: torch.Tensor,
808
+ log_k: torch.Tensor,
809
+ log_q: torch.Tensor,
810
+ rex: torch.Tensor,
811
+ k_val: torch.Tensor,
812
+ q_val: torch.Tensor,
813
+ g: torch.Tensor,
814
+ d_a: torch.Tensor,
815
+ d_qk: torch.Tensor,
816
+ d_k_eff: torch.Tensor,
817
+ d_k_pd: torch.Tensor,
818
+ d_q_w: torch.Tensor,
819
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
820
+ """Gradients of the Phase-1 outputs with respect to k_val, q_val and the gates.
821
+
822
+ Args:
823
+ payload, log_k, log_q, rex: the forward's outputs of the same name.
824
+ k_val, q_val, g: the forward's inputs.
825
+ d_a, d_qk: [N, C, C] float32 gradients of ``a`` and ``qk``; entries outside their
826
+ triangles are ignored.
827
+ d_k_eff: [N, C, W] float32 gradient of ``k_eff``.
828
+ d_k_pd: [N, C, W] float32 gradient of the post-decayed write weight
829
+ ``k_val * exp(rex)``.
830
+ d_q_w: [N, C, R] float32 gradient of ``q_w``.
831
+
832
+ Returns grad_k_val and grad_q_val in the dtype of ``k_val``, and grad_g [N, C, W]
833
+ float32: the gradient of each write's log-decay x (sum it over W for per-token gates).
834
+ Entries that contribute no x (repeated slots of a token, invalid tokens) get zero.
835
+ """
836
+ return tuple(_module().p1_backward(
837
+ payload, log_k, log_q, rex, k_val, q_val, g, d_a, d_qk, d_k_eff, d_k_pd, d_q_w, smem_cap(k_val.device)))
838
+
839
+
840
+ # Chunk sizes outside [_DET_MIN_C, _DET_MAX_C] take phase1_backward, which gives the same bits and is the faster kernel
841
+ # there. A shape-only rule; it never changes a bit.
842
+ _DET_MIN_C = 128
843
+ _DET_MAX_C = 128
844
+
845
+
846
+ def phase1_backward_det(payload, log_k, log_q, rex, k_val, q_val, g, d_a, d_qk, d_k_eff, d_k_pd, d_q_w):
847
+ """phase1_backward (same arguments, same outputs, same bits) with the deterministic-mode variant of the kernels at
848
+ the chunk sizes where it is faster."""
849
+ if not (_DET_MIN_C <= log_k.shape[1] <= _DET_MAX_C):
850
+ return phase1_backward(payload, log_k, log_q, rex, k_val, q_val, g, d_a, d_qk, d_k_eff, d_k_pd, d_q_w)
851
+ return tuple(_module().p1_backward_det(
852
+ payload, log_k, log_q, rex, k_val, q_val, g, d_a, d_qk, d_k_eff, d_k_pd, d_q_w, smem_cap(k_val.device)))