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,1802 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """mover_da2_cuda: the deterministic Phase-2 movers that walk Phase 1's sorted entries: DW (the forward's decay and
3
+ write) and GDS (the read-side backward).
4
+
5
+ DW computes, per slot group,
6
+
7
+ bank[slot] = fma(base, cpe_last, acc), acc = fma(sc_m, dv[tok_m], acc) over the group's members in
8
+ Phase-1 sorted order starting from 0, sc = __fmul_rn(f32(k_val), expf(rex)), base = the pre-write bank row,
9
+
10
+ the arithmetic of 2.0.0's Triton DW (phase2_kernels), its bitwise reference then. The walk keeps global latency off its
11
+ critical path:
12
+ * Slot rows (base) are prefetched into a per-warp shared-memory ring of RS rows in group order (per-lane cp.async;
13
+ TMA bulk copies for runs of a dense buffer): a batch start fills the ring, and every completed group issues the
14
+ next pending one. The walk only waits for copies that have long completed.
15
+ * Records (krec, Phase 1's record format: {spay element, absolute slot, f32 scale, f32 cpe}) are one 16-byte load
16
+ per lane per 32 positions, three batches ahead, broadcast by shuffles.
17
+ * Token rows: one bulk copy per token row slice into shared memory.
18
+ * The walk is unrolled over 32 positions and has no shared-memory stores and no memory clobbers, so the
19
+ compiler can hoist the token-row loads of later positions over the (uniform) FIRST / LAST branches.
20
+ Tiles: warp tile t of a chunk = [tb[t], tb[t+1]) with tb[t] the group FIRST nearest to t*n/TU (ties: back).
21
+ Every group lies in one tile and is summed in sorted order by one warp, so the output does not depend on the
22
+ tiling (TU, warps per CTA, CTAs per chunk, ring depth). The geometry comes from shapes only.
23
+
24
+ GDS (da2_gds2) computes the read entries' dots in token order (no atomics) and scatters the bank gradient in sorted
25
+ order, one single-rounding read-modify-write per (slot, element) and launch (a single writer per element). Both roles
26
+ run in one fused launch (8 warps per block) or in two launches.
27
+
28
+ The saving forward (dense compact snapshot) and the no-grad forward hand their DW to mover_fx_cuda's bank-direct kernel
29
+ when it supports the shape (see make_dw)."""
30
+
31
+ import functools
32
+
33
+ import torch
34
+
35
+ from ._build import load_cuda_extension
36
+ from ._device import backend, smem_cap
37
+
38
+ _CPP = r"""
39
+ void da2_krec(at::Tensor kpay, at::Tensor k_idx, at::Tensor k_val, at::Tensor rex, at::Tensor cpe,
40
+ at::Tensor krec);
41
+ void da2_tiles(at::Tensor kpay, at::Tensor tb);
42
+ void da2_dw(at::Tensor bank, at::Tensor dv, int64_t dv_step_rows, at::Tensor krec, at::Tensor tb,
43
+ int64_t ti, int64_t P, int64_t C, int64_t K, int64_t nw, int64_t rs,
44
+ at::Tensor snapc, at::Tensor segbuf, at::Tensor segb, int64_t sb_step, int64_t sf_full);
45
+ void da2_segb(at::Tensor kpay, at::Tensor tb, at::Tensor segb);
46
+ void da2_qrec(at::Tensor qpay, at::Tensor q_idx, at::Tensor q_val, at::Tensor logq, at::Tensor qrec);
47
+ void da2_dot_reduce(at::Tensor part, at::Tensor out, int64_t nsl);
48
+ void da2_gds2(at::Tensor bank, at::Tensor grad, at::Tensor gr, int64_t gr_step_rows, at::Tensor qrec, at::Tensor tb,
49
+ at::Tensor q_idx, at::Tensor dots, int64_t ti, int64_t valid, int64_t P, int64_t C, int64_t R, int64_t K,
50
+ int64_t nw, int64_t which, int64_t tpw);
51
+ """
52
+
53
+ _CUDA = r"""
54
+ #include <torch/extension.h>
55
+ #include <ATen/cuda/CUDAContext.h>
56
+ #include <c10/cuda/CUDAGuard.h>
57
+ #include <c10/cuda/CUDAException.h>
58
+ #include <cuda_bf16.h>
59
+ #include <algorithm>
60
+ #include <cstdint>
61
+ #include <map>
62
+ #include <mutex>
63
+ #include <utility>
64
+
65
+ namespace {
66
+
67
+ constexpr uint32_t kFirst = 0x80000000u;
68
+ constexpr uint32_t kLast = 0x40000000u;
69
+ constexpr uint32_t kInvalid = 0x20000u;
70
+ constexpr int kDistShift = 18;
71
+ constexpr uint32_t kDistMax = 4095u;
72
+ constexpr unsigned kFull = 0xFFFFFFFFu;
73
+
74
+ __device__ __forceinline__ float f_of(float v) { return v; }
75
+ __device__ __forceinline__ float f_of(__nv_bfloat16 v) { return __bfloat162float(v); }
76
+
77
+ __device__ __forceinline__ uint32_t smem_u32(const void* p) {
78
+ return static_cast<uint32_t>(__cvta_generic_to_shared(p));
79
+ }
80
+ // These kernels need sm_90+ (TMA bulk copies, mbarrier transaction counts, PDL); a JIT build for an arch list that
81
+ // also names older GPUs (8.0, mixed A100/H100) compiles these helpers to traps there, and the launchers never run on
82
+ // cc < 9 (supported() gates on the device's capability).
83
+ #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 900)
84
+ #define DA2_PRE_SM90 1
85
+ #define DA2_SM90_ONLY(...) __trap()
86
+ #else
87
+ #define DA2_SM90_ONLY(...) __VA_ARGS__
88
+ #endif
89
+ __device__ __forceinline__ void mbar_init(uint64_t* bar, uint32_t count) {
90
+ DA2_SM90_ONLY(asm volatile("mbarrier.init.shared::cta.b64 [%0], %1;\n" ::"r"(smem_u32(bar)), "r"(count)
91
+ : "memory"));
92
+ }
93
+ __device__ __forceinline__ void fence_mbar_init() {
94
+ DA2_SM90_ONLY(asm volatile("fence.mbarrier_init.release.cluster;\n" ::: "memory"));
95
+ }
96
+ __device__ __forceinline__ void fence_proxy_async() {
97
+ DA2_SM90_ONLY(asm volatile("fence.proxy.async.shared::cta;\n" ::: "memory"));
98
+ }
99
+ // One arrival that also announces `bytes` of transactions (count-1 barriers: the phase completes
100
+ // when this arrival happened AND every announced byte landed, in either order).
101
+ __device__ __forceinline__ void mbar_arrive_tx(uint64_t* bar, uint32_t bytes) {
102
+ DA2_SM90_ONLY(asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;\n" ::"r"(smem_u32(bar)),
103
+ "r"(bytes) : "memory"));
104
+ }
105
+ // TMA bulk copy global -> this CTA's shared memory, completion counted on `bar`.
106
+ __device__ __forceinline__ void bulk_g2s(void* dst, const void* src, uint32_t bytes, uint64_t* bar) {
107
+ DA2_SM90_ONLY(asm volatile(
108
+ "cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes [%0], [%1], %2, [%3];\n"
109
+ ::"r"(smem_u32(dst)), "l"(src), "r"(bytes), "r"(smem_u32(bar)) : "memory"));
110
+ }
111
+ #define DA2_SC_MINB 3 // min resident blocks per SM for the GDS scatter (registers <= 64K / (3 * 256))
112
+ #define DA2_DOTS_MINB 3 // min resident blocks per SM for the token-order dots (latency-bound gather)
113
+ constexpr int kRecPad = 40; // record batch slots per warp: 32 + prefetch overrun (sentinels)
114
+ template <int BY>
115
+ __device__ __forceinline__ void cp_async_lane(void* dst, const void* src, int src_bytes) {
116
+ if constexpr (BY == 16) {
117
+ asm volatile("cp.async.cg.shared.global [%0], [%1], 16, %2;\n" ::"r"(smem_u32(dst)), "l"(src),
118
+ "r"(src_bytes));
119
+ } else {
120
+ asm volatile("cp.async.ca.shared.global [%0], [%1], %2, %3;\n" ::"r"(smem_u32(dst)), "l"(src),
121
+ "n"(BY), "r"(src_bytes));
122
+ }
123
+ }
124
+ // Per-lane cp.async rings complete through commit groups (per thread, FIFO): a row's copy is one group; waiting for
125
+ // row q while `issued` rows were committed = wait until at most issued - 1 - q groups are pending (a form that
126
+ // compute-sanitizer's racecheck models).
127
+ __device__ __forceinline__ void cp_async_commit() {
128
+ DA2_SM90_ONLY(asm volatile("cp.async.commit_group;\n" ::: "memory"));
129
+ }
130
+ template <int N>
131
+ __device__ __forceinline__ void cp_async_wait_n() {
132
+ DA2_SM90_ONLY(asm volatile("cp.async.wait_group %0;\n" ::"n"(N) : "memory"));
133
+ }
134
+ __device__ __forceinline__ void cp_async_wait_upto(int n) { // 0 <= n; n >= 31 waits for at most 31 pending
135
+ switch (n) {
136
+ case 0: cp_async_wait_n<0>(); break; case 1: cp_async_wait_n<1>(); break;
137
+ case 2: cp_async_wait_n<2>(); break; case 3: cp_async_wait_n<3>(); break;
138
+ case 4: cp_async_wait_n<4>(); break; case 5: cp_async_wait_n<5>(); break;
139
+ case 6: cp_async_wait_n<6>(); break; case 7: cp_async_wait_n<7>(); break;
140
+ case 8: cp_async_wait_n<8>(); break; case 9: cp_async_wait_n<9>(); break;
141
+ case 10: cp_async_wait_n<10>(); break; case 11: cp_async_wait_n<11>(); break;
142
+ case 12: cp_async_wait_n<12>(); break; case 13: cp_async_wait_n<13>(); break;
143
+ case 14: cp_async_wait_n<14>(); break; case 15: cp_async_wait_n<15>(); break;
144
+ case 16: cp_async_wait_n<16>(); break; case 17: cp_async_wait_n<17>(); break;
145
+ case 18: cp_async_wait_n<18>(); break; case 19: cp_async_wait_n<19>(); break;
146
+ case 20: cp_async_wait_n<20>(); break; case 21: cp_async_wait_n<21>(); break;
147
+ case 22: cp_async_wait_n<22>(); break; case 23: cp_async_wait_n<23>(); break;
148
+ case 24: cp_async_wait_n<24>(); break; case 25: cp_async_wait_n<25>(); break;
149
+ case 26: cp_async_wait_n<26>(); break; case 27: cp_async_wait_n<27>(); break;
150
+ case 28: cp_async_wait_n<28>(); break; case 29: cp_async_wait_n<29>(); break;
151
+ case 30: cp_async_wait_n<30>(); break; default: cp_async_wait_n<31>(); break;
152
+ }
153
+ }
154
+ __device__ __forceinline__ void cp_async_mbar_arrive_noinc(uint64_t* bar) {
155
+ DA2_SM90_ONLY(asm volatile("cp.async.mbarrier.arrive.noinc.shared::cta.b64 [%0];\n" ::"r"(smem_u32(bar))));
156
+ }
157
+ // Spins until the phase of parity `parity` of `bar` completed; returns 0. Deliberately NO memory
158
+ // clobber: the caller adds the result to the address of every read that must follow the wait, so
159
+ // only those reads are ordered after it (acquire semantics of try_wait) and everything else stays
160
+ // free to move.
161
+ __device__ __forceinline__ uint32_t mbar_wait_dep(uint64_t* bar, uint32_t parity) {
162
+ #ifdef DA2_PRE_SM90
163
+ __trap();
164
+ return 0u;
165
+ #else
166
+ uint32_t ok;
167
+ asm volatile(
168
+ "{\n"
169
+ " .reg .pred p;\n"
170
+ " DA2_WAIT:\n"
171
+ " mbarrier.try_wait.parity.shared::cta.b64 p, [%1], %2;\n"
172
+ " @!p bra DA2_WAIT;\n"
173
+ " mov.u32 %0, 0;\n"
174
+ "}\n"
175
+ : "=r"(ok) : "r"(smem_u32(bar)), "r"(parity));
176
+ return ok;
177
+ #endif
178
+ }
179
+
180
+ template <typename E> struct Row4;
181
+ template <> struct Row4<__nv_bfloat16> { using T = uint2; }; // 4 bf16
182
+ template <> struct Row4<float> { using T = float4; }; // 4 f32
183
+
184
+ __device__ __forceinline__ float4 cvt4(uint2 v) {
185
+ return make_float4(__uint_as_float(v.x << 16), __uint_as_float(v.x & 0xFFFF0000u),
186
+ __uint_as_float(v.y << 16), __uint_as_float(v.y & 0xFFFF0000u));
187
+ }
188
+ __device__ __forceinline__ float4 cvt4(float4 v) { return v; }
189
+ __device__ __forceinline__ uint32_t pack_bf2(float a, float b) {
190
+ const __nv_bfloat162 h = __floats2bfloat162_rn(a, b);
191
+ return *reinterpret_cast<const uint32_t*>(&h);
192
+ }
193
+ __device__ __forceinline__ void st4(__nv_bfloat16* p, float4 v) {
194
+ *reinterpret_cast<uint2*>(p) = make_uint2(pack_bf2(v.x, v.y), pack_bf2(v.z, v.w));
195
+ }
196
+ __device__ __forceinline__ void st4(float* p, float4 v) { *reinterpret_cast<float4*>(p) = v; }
197
+ template <typename T> __device__ __forceinline__ T make_zero_row();
198
+ template <> __device__ __forceinline__ uint2 make_zero_row<uint2>() { return make_uint2(0u, 0u); }
199
+ template <> __device__ __forceinline__ float4 make_zero_row<float4>() {
200
+ return make_float4(0.f, 0.f, 0.f, 0.f);
201
+ }
202
+
203
+ __device__ __forceinline__ uint4 ld_rec(const uint4* __restrict__ kr, int pos, int e) {
204
+ if (pos < e) return __ldg(kr + pos);
205
+ return make_uint4(kFirst | kLast | kInvalid, 0u, 0u, 0x3F800000u); // padding: own group
206
+ }
207
+ // Valid group FIRSTs of a record batch (one bit per lane).
208
+ __device__ __forceinline__ uint32_t first_mask(const uint4& r) {
209
+ return __ballot_sync(kFull, (r.x & (kFirst | kInvalid)) == kFirst);
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------------------------
213
+ // Records (Phase-1 record contract): per sorted write position {spay_k element, absolute slot,
214
+ // __fmul_rn(f32(k_val), expf(rex)), f32(cpe)}; padding: slot 0, scale 0, cpe 1.
215
+ template <typename IdxT, typename ValT>
216
+ __global__ void __launch_bounds__(256) da2_krec_kernel(
217
+ const int32_t* __restrict__ kpay, const IdxT* __restrict__ k_idx, const ValT* __restrict__ kv,
218
+ const float* __restrict__ rex, const ValT* __restrict__ cpe, uint4* __restrict__ krec,
219
+ int64_t total, int n, int W) {
220
+ const int64_t i = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
221
+ if (i >= total) return;
222
+ const int64_t chunk = i / n;
223
+ const uint32_t pay = static_cast<uint32_t>(kpay[i]);
224
+ uint32_t slot = 0u, sc = 0u, c = 0x3F800000u;
225
+ if (!(pay & kInvalid)) {
226
+ const int64_t o = chunk * n + static_cast<int64_t>((pay >> 9) & 0xFF) * W + (pay & 0xFF);
227
+ slot = static_cast<uint32_t>(static_cast<int32_t>(k_idx[o]));
228
+ sc = __float_as_uint(__fmul_rn(f_of(kv[o]), expf(rex[o])));
229
+ c = __float_as_uint(f_of(cpe[o]));
230
+ }
231
+ krec[i] = make_uint4(pay, slot, sc, c);
232
+ }
233
+
234
+ // Tile bounds: tb[chunk][t] = the group FIRST nearest to floor(t * n / TU) (ties: the earlier one),
235
+ // t = 0..TU (tb[0] = 0, tb[TU] = n). Monotone in t, so tiles are well formed (possibly empty).
236
+ __global__ void __launch_bounds__(256) da2_tiles_kernel(
237
+ const int32_t* __restrict__ kpay, int32_t* __restrict__ tb, int64_t total, int n, int TU) {
238
+ const int64_t i = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
239
+ if (i >= total) return;
240
+ const int64_t chunk = i / (TU + 1);
241
+ const int t = static_cast<int>(i % (TU + 1));
242
+ const int j = static_cast<int>(static_cast<int64_t>(t) * n / TU);
243
+ const int32_t* kp = kpay + chunk * n;
244
+ int r;
245
+ if (j <= 0) {
246
+ r = 0;
247
+ } else if (j >= n) {
248
+ r = n;
249
+ } else {
250
+ const uint32_t pj = static_cast<uint32_t>(kp[j]);
251
+ if (pj & kFirst) {
252
+ r = j;
253
+ } else {
254
+ const uint32_t d = (pj >> kDistShift) & kDistMax;
255
+ int back;
256
+ if (d < kDistMax) {
257
+ back = j - static_cast<int>(d);
258
+ } else {
259
+ back = j - 1;
260
+ while (back > 0 && !(static_cast<uint32_t>(kp[back]) & kFirst)) --back;
261
+ }
262
+ int fwd = j + 1;
263
+ while (fwd < n && !(static_cast<uint32_t>(kp[fwd]) & kFirst)) ++fwd;
264
+ r = (fwd - j < j - back) ? fwd : back;
265
+ }
266
+ }
267
+ tb[i] = r;
268
+ }
269
+
270
+ // Segment ordinals (dense walk): segb[chunk][t] = valid group FIRSTs before tb[chunk][t], t = 0..TU
271
+ // (segb[TU] = the chunk's group count). One block per chunk.
272
+ __global__ void __launch_bounds__(256) da2_segb_kernel(
273
+ const int32_t* __restrict__ kpay, const int32_t* __restrict__ tb, int32_t* __restrict__ segb, int n, int TU) {
274
+ extern __shared__ int32_t cnt_s[];
275
+ const int64_t chunk = blockIdx.x;
276
+ const int32_t* kp = kpay + chunk * n;
277
+ const int32_t* tbc = tb + chunk * (TU + 1);
278
+ for (int t = threadIdx.x; t < TU; t += blockDim.x) {
279
+ const int s = tbc[t], e = tbc[t + 1];
280
+ int c = 0;
281
+ for (int j = s; j < e; ++j) c += (static_cast<uint32_t>(__ldg(kp + j)) & (kFirst | kInvalid)) == kFirst;
282
+ cnt_s[t] = c;
283
+ }
284
+ __syncthreads();
285
+ if (threadIdx.x == 0) {
286
+ int acc = 0;
287
+ for (int t = 0; t < TU; ++t) {
288
+ const int c = cnt_s[t];
289
+ cnt_s[t] = acc;
290
+ acc += c;
291
+ }
292
+ cnt_s[TU] = acc;
293
+ }
294
+ __syncthreads();
295
+ for (int t = threadIdx.x; t <= TU; t += blockDim.x) segb[chunk * (TU + 1) + t] = cnt_s[t];
296
+ }
297
+
298
+ // Read records (Phase-1 record contract): per sorted read position {spay_q element, absolute slot,
299
+ // __fmul_rn(f32(q_val), expf(log_q)), 0}; padding: slot 0, scale 0.
300
+ template <typename IdxT, typename ValT>
301
+ __global__ void __launch_bounds__(256) da2_qrec_kernel(
302
+ const int32_t* __restrict__ qpay, const IdxT* __restrict__ q_idx, const ValT* __restrict__ qv,
303
+ const float* __restrict__ logq, uint4* __restrict__ qrec, int64_t total, int n, int R) {
304
+ const int64_t i = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
305
+ if (i >= total) return;
306
+ const int64_t chunk = i / n;
307
+ const uint32_t pay = static_cast<uint32_t>(qpay[i]);
308
+ uint32_t slot = 0u, sc = 0u;
309
+ if (!(pay & kInvalid)) {
310
+ const int64_t o = chunk * n + static_cast<int64_t>((pay >> 9) & 0xFF) * R + (pay & 0xFF);
311
+ slot = static_cast<uint32_t>(static_cast<int32_t>(q_idx[o]));
312
+ sc = __float_as_uint(__fmul_rn(f_of(qv[o]), expf(logq[o])));
313
+ }
314
+ qrec[i] = make_uint4(pay, slot, sc, 0u);
315
+ }
316
+
317
+ // d_q_w[e] = ((part[0][e] + part[1][e]) + part[2][e]) + ...: the column slices of a wide head, in order.
318
+ __global__ void __launch_bounds__(256) da2_dot_reduce_kernel(
319
+ const float* __restrict__ part, float* __restrict__ out, int64_t total, int NSL) {
320
+ const int64_t i = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
321
+ if (i >= total) return;
322
+ float s = part[i];
323
+ for (int k = 1; k < NSL; ++k) s = __fadd_rn(s, part[static_cast<int64_t>(k) * total + i]);
324
+ out[i] = s;
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------------------------
328
+ // DW. Grid: P * NSL * K blocks (block b: cr = b % K, slice = b / K % NSL, p = rest); warp w of
329
+ // block cr walks tile cr * NW + w of its chunk. Shared memory: token rows [C][SW] f32 | rings
330
+ // [NW][RS][RB] | record batches [NW][32] uint4 | mbarriers [1 + NW * RS].
331
+ // Token rows [C, SW] of a slice into shared memory (row stride SW floats), all completing on tok_bar (its expected
332
+ // bytes are set here): ONE bulk copy when the slice is the whole row (SW == hd, the C rows are contiguous), else one
333
+ // copy per row. (Per-row copies issued by divergent threads serialize through an elect loop: avoid when possible.)
334
+ __device__ __forceinline__ void stage_tok_rows(float* tok_s, const float* rows, int C, int SW, int hd,
335
+ uint64_t* tok_bar, int nthreads) {
336
+ if (threadIdx.x == 0) mbar_arrive_tx(tok_bar, static_cast<uint32_t>(C * SW * 4));
337
+ if (SW == hd) {
338
+ if (threadIdx.x == 0) bulk_g2s(tok_s, rows, static_cast<uint32_t>(C * SW * 4), tok_bar);
339
+ } else {
340
+ for (int t = threadIdx.x; t < C; t += nthreads) {
341
+ bulk_g2s(tok_s + t * SW, rows + static_cast<int64_t>(t) * hd, static_cast<uint32_t>(SW * 4), tok_bar);
342
+ }
343
+ }
344
+ }
345
+
346
+ struct Geo {
347
+ int P, C, n, hd, NSL, SW, K, TU, tok_bytes;
348
+ int sf_full; // SF source layout: 0 slice-major dense buffer, 1 full rows chunk * n + g (dense snapc)
349
+ };
350
+
351
+ template <typename E, int NW, int RS, int MODE = 0>
352
+ __global__ void __launch_bounds__(NW * 32, (NW <= 8 ? 2 : 1)) da2_dw_kernel(
353
+ E* __restrict__ bank, const float* __restrict__ dv, int64_t dv_step_rows,
354
+ const uint4* __restrict__ krec, const int32_t* __restrict__ tb, int ti, Geo g,
355
+ long long* __restrict__ stats, E* __restrict__ snapc, const E* __restrict__ segbuf,
356
+ const int32_t* __restrict__ segb, int sb_step) {
357
+ // MODE & 32 (SF, the dense walk): the tile's base rows are rows sb_chunk * n + segb[tile] + q of the dense buffer
358
+ // segbuf (q = the group's index in the tile): issue order = group order, no records.
359
+ constexpr bool SF = (MODE & 32) != 0;
360
+ // The dense walk streams its rows in TMA bulk RUNS; the per-row ring (no SF) is cp.async.
361
+ // CPR: the cp.async per-row ring is the active ring.
362
+ constexpr bool CPR = !SF;
363
+ // snapc: compact snapshot rows chunk * n + j_FIRST <- the group's raw pre-write bank row, or null (the launchers
364
+ // pass null: no row is written).
365
+ using RowT = typename Row4<E>::T;
366
+ static_assert((RS & (RS - 1)) == 0 && RS <= 32, "RS: power of two <= 32");
367
+ extern __shared__ __align__(128) unsigned char smem[];
368
+ const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5;
369
+ int b = blockIdx.x;
370
+ const int cr = b % g.K;
371
+ b /= g.K;
372
+ const int sl = b % g.NSL;
373
+ const int p = b / g.NSL;
374
+ const int64_t chunk = static_cast<int64_t>(ti) * g.P + p;
375
+ const int SW = g.SW, C = g.C, hd = g.hd;
376
+ constexpr int BY = 4 * static_cast<int>(sizeof(E)); // bytes of a lane's 4 columns
377
+ const int RB = SW * static_cast<int>(sizeof(E)); // row slice bytes (16 | RB)
378
+ const int RBS = CPR ? 32 * BY : RB; // ring row stride
379
+ float* tok_s = reinterpret_cast<float*>(smem);
380
+ unsigned char* rings = smem + g.tok_bytes;
381
+ unsigned char* ring = rings + static_cast<size_t>(warp) * RS * RBS;
382
+ uint4* rec_s = reinterpret_cast<uint4*>(rings + static_cast<size_t>(NW) * RS * RBS) + warp * kRecPad;
383
+ uint64_t* bars = reinterpret_cast<uint64_t*>(rings + static_cast<size_t>(NW) * RS * RBS +
384
+ static_cast<size_t>(NW) * kRecPad * sizeof(uint4));
385
+ if (lane < kRecPad - 32) rec_s[32 + lane] = make_uint4(0u, 0u, 0u, 0u);
386
+ uint64_t* tok_bar = bars;
387
+ uint64_t* my_bars = bars + 1 + warp * RS;
388
+ if (threadIdx.x == 0) mbar_init(tok_bar, 1);
389
+ if (lane < RS) mbar_init(my_bars + lane, CPR ? 32u : 1u);
390
+ fence_mbar_init();
391
+ __syncthreads();
392
+
393
+ { // Token rows of the slice, all on tok_bar.
394
+ const float* rows = dv + (static_cast<int64_t>(ti) * dv_step_rows +
395
+ static_cast<int64_t>(p) * C) * hd + static_cast<int64_t>(sl) * SW;
396
+ stage_tok_rows(tok_s, rows, C, SW, hd, tok_bar, NW * 32);
397
+ }
398
+
399
+ const int32_t* tbc = tb + chunk * (g.TU + 1);
400
+ const int tile = cr * NW + warp;
401
+ const int s = __ldg(tbc + tile), e = __ldg(tbc + tile + 1);
402
+ const uint4* kr = krec + chunk * g.n;
403
+ const int c4 = 4 * lane;
404
+ const bool on = c4 < SW && sl * SW + c4 < hd;
405
+ const int coff = on ? c4 : 0; // safe column for loads
406
+ const int ring_off = CPR ? lane * BY : coff * static_cast<int>(sizeof(E));
407
+ E* const bank_col = bank + static_cast<int64_t>(sl) * SW + c4; // stores only when `on`
408
+ const E* const bank_src = bank + static_cast<int64_t>(sl) * SW; // slice start of a row
409
+
410
+ uint4 r0 = ld_rec(kr, s + lane, e);
411
+ uint4 r1 = ld_rec(kr, s + 32 + lane, e);
412
+ uint4 r2 = ld_rec(kr, s + 64 + lane, e);
413
+ uint4 r3 = ld_rec(kr, s + 96 + lane, e);
414
+ // Not-yet-issued valid FIRSTs of the batches held in r0, r1, r2 (issue order = segment order).
415
+ uint32_t rm0 = 0u, rm1 = 0u, rm2 = 0u;
416
+ if constexpr (!SF) {
417
+ rm0 = first_mask(r0);
418
+ rm1 = first_mask(r1);
419
+ rm2 = first_mask(r2);
420
+ }
421
+ int done = 0; // segments of the tile completed (emitted)
422
+ int issued = 0; // segments whose slot row was issued (always >= done)
423
+ int nseg = 0; // SF: the tile's group count
424
+ const E* sf_src = nullptr;
425
+ if constexpr (SF) {
426
+ const int64_t tix = chunk * (g.TU + 1) + tile;
427
+ const int r0_ = __ldg(segb + tix);
428
+ nseg = __ldg(segb + tix + 1) - r0_;
429
+ const int64_t sbc = static_cast<int64_t>(ti) * sb_step + p;
430
+ sf_src = g.sf_full ? segbuf + (sbc * g.n + r0_) * static_cast<int64_t>(hd) + static_cast<int64_t>(sl) * SW
431
+ : segbuf + ((sbc * g.NSL + sl) * g.n + r0_) * static_cast<int64_t>(SW);
432
+ }
433
+ const int sf_rs = g.sf_full ? hd : SW; // SF row stride (elements); runs need contiguous rows (sf_rs == SW)
434
+ // SF: rows go in RUNS (one bulk copy per contiguous run of groups, split at the ring end), one mbarrier per
435
+ // run (at its first slot). run_mask: slots where an issued, unwaited run starts; ph_mask: per-slot barrier
436
+ // phase; rows <= ready_until are present.
437
+ constexpr bool RUNS = SF;
438
+ uint32_t run_mask = 0u, ph_mask = 0u;
439
+ int ready_until = -1;
440
+ auto issue_runs = [&](int lim) { // warp-uniform: rows [issued, lim) (lim <= done + RS)
441
+ while (issued < lim) {
442
+ const int s0 = issued & (RS - 1);
443
+ const int k = sf_rs == SW ? min(lim - issued, RS - s0) : 1;
444
+ if (lane == 0) {
445
+ mbar_arrive_tx(my_bars + s0, static_cast<uint32_t>(k * RB));
446
+ bulk_g2s(ring + s0 * RBS, sf_src + static_cast<int64_t>(issued) * sf_rs, static_cast<uint32_t>(k * RB),
447
+ my_bars + s0);
448
+ }
449
+ run_mask |= 1u << s0;
450
+ issued += k;
451
+ }
452
+ };
453
+ // zrun: the sum of every run wait's dependency token so far (all 0 at run time). Ring reads of rows covered
454
+ // by waited runs add zrun to their address, so none can be scheduled before the wait of its run.
455
+ uint32_t zrun = 0u;
456
+ auto wait_run = [&](int q) -> uint32_t { // q: a run start > ready_until; returns the updated zrun
457
+ const int s0 = q & (RS - 1);
458
+ zrun += mbar_wait_dep(my_bars + s0, (ph_mask >> s0) & 1u);
459
+ ph_mask ^= 1u << s0;
460
+ run_mask &= ~(1u << s0);
461
+ const uint64_t m2 = static_cast<uint64_t>(run_mask) | (static_cast<uint64_t>(run_mask) << RS);
462
+ const uint32_t rot = static_cast<uint32_t>(m2 >> (s0 + 1)) & static_cast<uint32_t>((1ull << RS) - 1ull);
463
+ ready_until = (rot ? q + __ffs(rot) : issued) - 1;
464
+ return zrun;
465
+ };
466
+ auto sf_issue = [&](int q) { // SF: group q's base row (warp-uniform call; TMA: lane 0 issues)
467
+ const int ri = q & (RS - 1);
468
+ const E* row = sf_src + static_cast<int64_t>(q) * sf_rs;
469
+ if constexpr (CPR) {
470
+ __syncwarp(); // slot handoff: every lane's reads of this slot's previous row precede the refill
471
+ cp_async_lane<BY>(ring + ri * RBS + lane * BY, on ? row + c4 : row, on ? BY : 0);
472
+ cp_async_commit();
473
+ } else {
474
+ if (lane == 0) {
475
+ mbar_arrive_tx(my_bars + ri, static_cast<uint32_t>(RB));
476
+ bulk_g2s(ring + ri * RBS, row, static_cast<uint32_t>(RB), my_bars + ri);
477
+ }
478
+ }
479
+ };
480
+
481
+ // Source row of a group: bank[slot] (key = slot).
482
+ const E* const row_src = bank_src;
483
+ int bpos = s; // sorted position of r0's first lane (r1: + 32, r2: + 64)
484
+ auto issue_seg = [&](int q, int key) { // TMA: the calling lane issues group q's base row
485
+ const int ri = q & (RS - 1);
486
+ uint64_t* bar = my_bars + ri;
487
+ mbar_arrive_tx(bar, static_cast<uint32_t>(RB));
488
+ bulk_g2s(ring + ri * RBS, row_src + static_cast<int64_t>(key) * hd, static_cast<uint32_t>(RB), bar);
489
+ };
490
+ auto issue_all = [&](int q, int key) { // LDGSTS: every lane copies its 4 columns (warp-uniform key)
491
+ const int ri = q & (RS - 1);
492
+ const E* row = row_src + static_cast<int64_t>(key) * hd;
493
+ __syncwarp(); // slot handoff: every lane's reads of this slot's previous row precede the refill
494
+ cp_async_lane<BY>(ring + ri * RBS + lane * BY, on ? row + c4 : row, on ? BY : 0);
495
+ cp_async_commit();
496
+ };
497
+ // Issue the next FIRSTs of batch mask `m` (records r, first lane at sorted position p0) while the ring has room.
498
+ auto issue_from = [&](uint32_t& m, const uint4& r, int p0) {
499
+ const int avail = done + RS - issued;
500
+ if (avail <= 0 || m == 0u) return;
501
+ const uint32_t lt = (1u << lane) - 1u;
502
+ const bool mine = ((m >> lane) & 1u) && __popc(m & lt) < avail;
503
+ const uint32_t tm = __ballot_sync(kFull, mine);
504
+ if constexpr (CPR) {
505
+ uint32_t t = tm;
506
+ int q = issued;
507
+ while (t) {
508
+ const int li = __ffs(t) - 1;
509
+ t &= t - 1u;
510
+ issue_all(q++, __shfl_sync(kFull, static_cast<int32_t>(r.y), li));
511
+ }
512
+ } else {
513
+ if (mine) issue_seg(issued + __popc(tm & lt), static_cast<int32_t>(r.y));
514
+ }
515
+ issued += __popc(tm);
516
+ m &= ~tm;
517
+ };
518
+ // After a group completed (a ring slot freed): issue the next pending group, if its record is held.
519
+ auto pump = [&]() {
520
+ if (issued >= done + RS) return;
521
+ const uint32_t mm = rm0 ? rm0 : (rm1 ? rm1 : rm2);
522
+ if (mm == 0u) return;
523
+ const int jb = rm0 ? 0 : (rm1 ? 1 : 2);
524
+ const int li = __ffs(mm) - 1;
525
+ const int32_t sl_ = static_cast<int32_t>(jb == 0 ? r0.y : (jb == 1 ? r1.y : r2.y));
526
+ if constexpr (CPR) {
527
+ issue_all(issued, __shfl_sync(kFull, sl_, li));
528
+ } else {
529
+ fence_proxy_async();
530
+ __syncwarp();
531
+ if (lane == li) issue_seg(issued, sl_);
532
+ }
533
+ const uint32_t bit = 1u << li;
534
+ if (jb == 0) rm0 &= ~bit; else if (jb == 1) rm1 &= ~bit; else rm2 &= ~bit;
535
+ ++issued;
536
+ };
537
+ auto top_up = [&]() {
538
+ if constexpr (RUNS) {
539
+ issue_runs(min(done + RS, nseg));
540
+ } else if constexpr (SF) {
541
+ const int lim = min(done + RS, nseg);
542
+ while (issued < lim) sf_issue(issued++);
543
+ } else {
544
+ issue_from(rm0, r0, bpos);
545
+ if (rm0 == 0u) issue_from(rm1, r1, bpos + 32);
546
+ if (rm0 == 0u && rm1 == 0u) issue_from(rm2, r2, bpos + 64);
547
+ }
548
+ };
549
+ auto sf_pump = [&]() {
550
+ if constexpr (RUNS) {
551
+ const int lim = min(done + RS, nseg);
552
+ if (lim - issued >= RS / 2 || (lim == nseg && lim > issued)) {
553
+ fence_proxy_async();
554
+ __syncwarp();
555
+ issue_runs(lim);
556
+ }
557
+ } else if (issued < min(done + RS, nseg)) {
558
+ if constexpr (!CPR) {
559
+ fence_proxy_async();
560
+ __syncwarp();
561
+ }
562
+ sf_issue(issued++);
563
+ }
564
+ };
565
+ __syncwarp();
566
+ top_up();
567
+ const uint32_t ztok = mbar_wait_dep(tok_bar, 0u);
568
+ const float* tok = tok_s + ztok + coff;
569
+
570
+ float4 acc = make_float4(0.f, 0.f, 0.f, 0.f);
571
+ int jf = s; // position of the current group's FIRST
572
+ E* const snap_row0 = snapc == nullptr ? nullptr : snapc + chunk * static_cast<int64_t>(g.n) * hd +
573
+ static_cast<int64_t>(sl) * SW + c4;
574
+ const int nb = (e - s + 31) >> 5;
575
+ for (int bi = 0; bi < nb; ++bi) {
576
+ // The previous batch's walk is done with rec_s / ring. TMA ring: every lane orders its generic reads
577
+ // of ring slots before the async-proxy refill (fence, THEN the warp sync that hands slots over).
578
+ if constexpr (!CPR) fence_proxy_async();
579
+ __syncwarp();
580
+ if (bi > 0) {
581
+ bpos = s + 32 * bi;
582
+ r0 = r1;
583
+ r1 = r2;
584
+ r2 = r3;
585
+ r3 = ld_rec(kr, s + 32 * (bi + 3) + lane, e);
586
+ if constexpr (!SF) {
587
+ rm0 = rm1;
588
+ rm1 = rm2;
589
+ rm2 = first_mask(r2);
590
+ }
591
+ top_up();
592
+ }
593
+ // Per-batch precompute, vectorized over the lanes (lane i = position s + 32 * bi + i): the token row
594
+ // offset and the uniform FIRST / LAST masks, so the walk has no per-position decode and only uniform
595
+ // branches. rec_s[i] = {token row offset (floats), scale, slot, cpe}.
596
+ const uint32_t vfirst = __ballot_sync(kFull, (r0.x & (kFirst | kInvalid)) == kFirst);
597
+ const uint32_t vlast = __ballot_sync(kFull, (r0.x & (kLast | kInvalid)) == kLast);
598
+ // SF: no FIRST test. Groups (valid ones and the invalid entries' own groups) tile the walk contiguously,
599
+ // so the accumulator restarts right after EVERY LAST (vend), valid or not (an invalid entry's product,
600
+ // even NaN from a non-finite token row, never reaches the next group); only valid LASTs emit (vlast).
601
+ const uint32_t vend = SF ? __ballot_sync(kFull, (r0.x & kLast) != 0u) : 0u;
602
+ if constexpr (SF) {
603
+ rec_s[lane] = make_uint4(static_cast<uint32_t>(min(static_cast<int>((r0.x >> 9) & 0xFFu), C - 1) * SW),
604
+ r0.z, r0.y, r0.w);
605
+ } else {
606
+ rec_s[lane] = make_uint4(static_cast<uint32_t>(min(static_cast<int>((r0.x >> 9) & 0xFFu), C - 1) * SW),
607
+ r0.z, r0.y, r0.w);
608
+ }
609
+ // Rows of the groups this batch completes: [done, done + nlast); the ones already issued
610
+ // are waited for here, so the walk itself waits only for a group issued late (slow path).
611
+ const int nlast = __popc(vlast);
612
+ const int wend = min(done + nlast, issued);
613
+ uint32_t zr = 0u;
614
+ if constexpr (RUNS) {
615
+ for (int q = max(done, ready_until + 1); q < wend; q = ready_until + 1) (void)wait_run(q);
616
+ zr = zrun;
617
+ } else {
618
+ if constexpr (CPR) {
619
+ if (wend > done) cp_async_wait_upto(issued - wend); // rows [done, wend) landed (FIFO groups)
620
+ } else {
621
+ for (int q = done; q < wend; ++q) zr += mbar_wait_dep(my_bars + (q & (RS - 1)),
622
+ static_cast<uint32_t>(q / RS) & 1u);
623
+ }
624
+ }
625
+ __syncwarp();
626
+ const uint4* rq = rec_s + zr;
627
+ RowT nbase = make_zero_row<RowT>();
628
+ if (!RUNS && done < wend) {
629
+ nbase = *reinterpret_cast<const RowT*>(ring + (done & (RS - 1)) * RBS + zr + ring_off);
630
+ }
631
+ const int pos0 = s + 32 * bi;
632
+ const int kend = min(32, e - pos0); // positions of this batch inside the tile
633
+ #pragma unroll 1
634
+ for (int k8 = 0; k8 < kend; k8 += 8) {
635
+ // All 8 positions' records and token rows first (16 independent shared loads in flight).
636
+ uint2 rr[8];
637
+ float4 xx[8];
638
+ #pragma unroll
639
+ for (int j = 0; j < 8; ++j) rr[j] = *reinterpret_cast<const uint2*>(rq + k8 + j);
640
+ #pragma unroll
641
+ for (int j = 0; j < 8; ++j) {
642
+ xx[j] = *reinterpret_cast<const float4*>(tok + rr[j].x);
643
+ }
644
+ const uint32_t f8 = vfirst >> k8, l8 = vlast >> k8, e8 = vend >> k8;
645
+ // RUNS (SF): every row this sub-batch emits is made present ONCE here (issue + wait, one code site;
646
+ // normally already covered by the batch-start wait), so the 8 unrolled emits carry no slow path.
647
+ uint32_t zs = 0u;
648
+ if constexpr (RUNS) {
649
+ const int nl = __popc(l8 & 0xFFu);
650
+ if (nl > 0 && done + nl - 1 > ready_until) {
651
+ if (done + nl > issued) {
652
+ fence_proxy_async();
653
+ __syncwarp();
654
+ issue_runs(min(done + RS, nseg));
655
+ }
656
+ while (ready_until < done + nl - 1) (void)wait_run(ready_until + 1);
657
+ }
658
+ zs = zrun;
659
+ }
660
+ #pragma unroll
661
+ for (int j = 0; j < 8; ++j) {
662
+ if (!SF && (f8 & (1u << j))) {
663
+ acc = make_float4(0.f, 0.f, 0.f, 0.f);
664
+ jf = pos0 + k8 + j;
665
+ if (done >= issued) {
666
+ // Slow path (the ring was full at the batch start): issue this group's row now.
667
+ const int32_t key_f = static_cast<int32_t>(rq[k8 + j].z);
668
+ if constexpr (CPR) {
669
+ issue_all(done, key_f);
670
+ } else {
671
+ fence_proxy_async();
672
+ __syncwarp();
673
+ if (lane == 0) issue_seg(done, key_f);
674
+ }
675
+ issued = done + 1;
676
+ rm0 &= ~(1u << (k8 + j));
677
+ }
678
+ }
679
+ const float sc = __uint_as_float(rr[j].y);
680
+ acc.x = fmaf(sc, xx[j].x, acc.x);
681
+ acc.y = fmaf(sc, xx[j].y, acc.y);
682
+ acc.z = fmaf(sc, xx[j].z, acc.z);
683
+ acc.w = fmaf(sc, xx[j].w, acc.w);
684
+ if ((SF ? e8 : l8) & (1u << j)) {
685
+ if (!SF || (l8 & (1u << j))) {
686
+ const uint2 sw2 = *reinterpret_cast<const uint2*>(
687
+ reinterpret_cast<const unsigned char*>(rq + k8 + j) + 8); // {slot, cpe}
688
+ const int slot = static_cast<int>(sw2.x);
689
+ const float cpe = __uint_as_float(sw2.y);
690
+ RowT bv = nbase;
691
+ if constexpr (RUNS) {
692
+ bv = *reinterpret_cast<const RowT*>(ring + (done & (RS - 1)) * RBS + zs + ring_off);
693
+ } else if (done >= wend) { // issued in this batch (slow path): wait here
694
+ if constexpr (SF) {
695
+ if (done >= issued) {
696
+ if constexpr (!CPR) {
697
+ fence_proxy_async();
698
+ __syncwarp();
699
+ }
700
+ sf_issue(done);
701
+ issued = done + 1;
702
+ }
703
+ }
704
+ const int ri = done & (RS - 1);
705
+ uint32_t z = 0u;
706
+ if constexpr (CPR) {
707
+ cp_async_wait_upto(issued - done - 1);
708
+ } else {
709
+ z = mbar_wait_dep(my_bars + ri, static_cast<uint32_t>(done / RS) & 1u);
710
+ }
711
+ bv = *reinterpret_cast<const RowT*>(ring + ri * RBS + z + ring_off);
712
+ }
713
+ const float4 base = cvt4(bv);
714
+ float4 o;
715
+ o.x = fmaf(base.x, cpe, acc.x);
716
+ o.y = fmaf(base.y, cpe, acc.y);
717
+ o.z = fmaf(base.z, cpe, acc.z);
718
+ o.w = fmaf(base.w, cpe, acc.w);
719
+ if (on) st4(bank_col + static_cast<uint64_t>(static_cast<uint32_t>(slot)) * static_cast<uint32_t>(hd), o);
720
+ if (snap_row0 != nullptr && on) {
721
+ *reinterpret_cast<RowT*>(snap_row0 + static_cast<int64_t>(jf) * hd) = bv;
722
+ }
723
+ ++done;
724
+ if (!RUNS && done < wend) {
725
+ nbase = *reinterpret_cast<const RowT*>(ring + (done & (RS - 1)) * RBS + zr + ring_off);
726
+ }
727
+ if constexpr (!RUNS) {
728
+ if constexpr (SF) sf_pump(); else pump();
729
+ }
730
+ }
731
+ if constexpr (SF) { // after any LAST: the next group starts at the next position
732
+ acc = make_float4(0.f, 0.f, 0.f, 0.f);
733
+ jf = pos0 + k8 + j + 1;
734
+ }
735
+ }
736
+ }
737
+ if constexpr (RUNS) { // slots freed by this sub-batch's groups: refill
738
+ if (l8 & 0xFFu) sf_pump();
739
+ }
740
+ }
741
+ }
742
+ // Every thread observes the token rows before the CTA may exit (no copy left in flight).
743
+ (void)mbar_wait_dep(tok_bar, 0u);
744
+ }
745
+
746
+ // ---------------------------------------------------------------------------------------------
747
+ // GDS, split by data need: (1) dots in TOKEN order, no atomics: d_q_w[t, r] = <gr[t], bank[slot]> per slice with
748
+ // the per-lane chain fma(x.w, m.w, fma(x.z, m.z, fma(x.y, m.y, x.x * m.x))) and the xor-16/8/4/2/1 reduce-scatter;
749
+ // (2) the grad scatter in SORTED order with no slot-row reads. RMW (single rounding, no subnormal flush): at each
750
+ // valid LAST, grad[slot] = round(grad[slot] + acc) by a plain load + store; the walk is the row's only writer in the
751
+ // launch, so this is race-free and deterministic. The launchers instantiate RMW = true only.
752
+ template <typename G, int NW, bool RMW>
753
+ __device__ __forceinline__ void gscatter_role(
754
+ G* __restrict__ grad, const float* __restrict__ gr, int64_t gr_step_rows, const uint4* __restrict__ qrec,
755
+ const int32_t* __restrict__ tb, int ti, const Geo& g, int b, unsigned char* smem) {
756
+ using RowG = typename Row4<G>::T;
757
+ const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5;
758
+ const int cr = b % g.K;
759
+ b /= g.K;
760
+ const int sl = b % g.NSL;
761
+ const int p = b / g.NSL;
762
+ const int64_t chunk = static_cast<int64_t>(ti) * g.P + p;
763
+ const int SW = g.SW, C = g.C, hd = g.hd;
764
+ float* tok_s = reinterpret_cast<float*>(smem);
765
+ uint4* rec_s = reinterpret_cast<uint4*>(smem + g.tok_bytes) + warp * 32;
766
+ uint64_t* tok_bar = reinterpret_cast<uint64_t*>(smem + g.tok_bytes + NW * 32 * sizeof(uint4));
767
+ if (threadIdx.x == 0) mbar_init(tok_bar, 1);
768
+ fence_mbar_init();
769
+ __syncthreads();
770
+ {
771
+ const float* rows = gr + (static_cast<int64_t>(ti) * gr_step_rows + static_cast<int64_t>(p) * C) * hd +
772
+ static_cast<int64_t>(sl) * SW;
773
+ stage_tok_rows(tok_s, rows, C, SW, hd, tok_bar, NW * 32);
774
+ }
775
+ const int32_t* tbc = tb + chunk * (g.TU + 1);
776
+ const int tile = cr * NW + warp;
777
+ const int s = __ldg(tbc + tile), e = __ldg(tbc + tile + 1);
778
+ const uint4* kr = qrec + chunk * g.n;
779
+ const int c4 = 4 * lane;
780
+ const bool on = c4 < SW && sl * SW + c4 < hd;
781
+ const int coff = on ? c4 : 0;
782
+ G* const grad_col = grad + static_cast<int64_t>(sl) * SW + c4;
783
+ // Records 4 batches ahead (a batch walks in a fraction of one global-load latency).
784
+ uint4 r0 = ld_rec(kr, s + lane, e);
785
+ uint4 r1 = ld_rec(kr, s + 32 + lane, e);
786
+ uint4 r2 = ld_rec(kr, s + 64 + lane, e);
787
+ uint4 r3 = ld_rec(kr, s + 96 + lane, e);
788
+ const uint32_t ztok = mbar_wait_dep(tok_bar, 0u);
789
+ const float* tok = tok_s + ztok + coff;
790
+ float4 acc = make_float4(0.f, 0.f, 0.f, 0.f);
791
+ const int nb = (e - s + 31) >> 5;
792
+ for (int bi = 0; bi < nb; ++bi) {
793
+ __syncwarp();
794
+ if (bi > 0) {
795
+ r0 = r1;
796
+ r1 = r2;
797
+ r2 = r3;
798
+ r3 = ld_rec(kr, s + 32 * (bi + 3) + lane, e);
799
+ }
800
+ // No FIRST test (see the DW's dense walk): acc restarts after EVERY LAST (vend, valid or invalid group); only
801
+ // valid LASTs add to the gradient (vlast).
802
+ const uint32_t vlast = __ballot_sync(kFull, (r0.x & (kLast | kInvalid)) == kLast);
803
+ const uint32_t vend = __ballot_sync(kFull, (r0.x & kLast) != 0u);
804
+ rec_s[lane] = make_uint4(static_cast<uint32_t>(min(static_cast<int>((r0.x >> 9) & 0xFFu), C - 1) * SW),
805
+ r0.z, r0.y, 0u);
806
+ if constexpr (RMW) {
807
+ // The NEXT batch's group rows (records r1, one lane per position) to L2 a batch ahead of their
808
+ // read-modify-write (each (slot, row) has one group per launch: no other writer, so reading early is safe).
809
+ if ((r1.x & (kLast | kInvalid)) == kLast) {
810
+ const char* rp = reinterpret_cast<const char*>(grad + static_cast<uint64_t>(r1.y) * static_cast<uint32_t>(hd) +
811
+ static_cast<int64_t>(sl) * SW);
812
+ for (int off = 0; off < SW * static_cast<int>(sizeof(G)); off += 128) {
813
+ asm volatile("prefetch.global.L2 [%0];" ::"l"(rp + off));
814
+ }
815
+ }
816
+ if (bi == 0 && (vlast >> lane) & 1u) { // the first batch's rows (nothing prefetched them)
817
+ const char* rp = reinterpret_cast<const char*>(grad + static_cast<uint64_t>(r0.y) * static_cast<uint32_t>(hd) +
818
+ static_cast<int64_t>(sl) * SW);
819
+ for (int off = 0; off < SW * static_cast<int>(sizeof(G)); off += 128) {
820
+ asm volatile("prefetch.global.L2 [%0];" ::"l"(rp + off));
821
+ }
822
+ }
823
+ }
824
+ __syncwarp();
825
+ const int pos0 = s + 32 * bi;
826
+ const int kend = min(32, e - pos0);
827
+ // RMW: the grad rows of a sub-batch's valid LASTs are loaded into registers one sub-batch ahead.
828
+ RowG gc[RMW ? 8 : 1];
829
+ auto load_g = [&](RowG (&g)[RMW ? 8 : 1], int kb) {
830
+ if constexpr (RMW) {
831
+ const uint32_t lb = vlast >> kb;
832
+ #pragma unroll
833
+ for (int j = 0; j < 8; ++j) {
834
+ g[j] = make_zero_row<RowG>();
835
+ if (on && ((lb >> j) & 1u)) {
836
+ const int slot = static_cast<int>(rec_s[kb + j].z);
837
+ g[j] = *reinterpret_cast<const RowG*>(
838
+ grad_col + static_cast<uint64_t>(static_cast<uint32_t>(slot)) * static_cast<uint32_t>(hd));
839
+ }
840
+ }
841
+ }
842
+ };
843
+ if constexpr (RMW) load_g(gc, 0);
844
+ #pragma unroll 1
845
+ for (int k8 = 0; k8 < kend; k8 += 8) {
846
+ RowG gn[RMW ? 8 : 1];
847
+ if constexpr (RMW) {
848
+ if (k8 + 8 < kend) load_g(gn, k8 + 8);
849
+ }
850
+ uint2 rr[8];
851
+ float4 xx[8];
852
+ #pragma unroll
853
+ for (int j = 0; j < 8; ++j) rr[j] = *reinterpret_cast<const uint2*>(rec_s + k8 + j);
854
+ #pragma unroll
855
+ for (int j = 0; j < 8; ++j) xx[j] = *reinterpret_cast<const float4*>(tok + rr[j].x);
856
+ const uint32_t l8 = vlast >> k8, e8 = vend >> k8;
857
+ #pragma unroll
858
+ for (int j = 0; j < 8; ++j) {
859
+ const float sc = __uint_as_float(rr[j].y);
860
+ acc.x = fmaf(sc, xx[j].x, acc.x);
861
+ acc.y = fmaf(sc, xx[j].y, acc.y);
862
+ acc.z = fmaf(sc, xx[j].z, acc.z);
863
+ acc.w = fmaf(sc, xx[j].w, acc.w);
864
+ if (e8 & (1u << j)) {
865
+ if (l8 & (1u << j)) {
866
+ const int slot = static_cast<int>(rec_s[k8 + j].z);
867
+ G* const gp = grad_col + static_cast<uint64_t>(static_cast<uint32_t>(slot)) * static_cast<uint32_t>(hd);
868
+ if constexpr (RMW) {
869
+ if (on) {
870
+ const float4 gb = cvt4(gc[j]);
871
+ float4 o;
872
+ o.x = gb.x + acc.x;
873
+ o.y = gb.y + acc.y;
874
+ o.z = gb.z + acc.z;
875
+ o.w = gb.w + acc.w;
876
+ st4(gp, o);
877
+ }
878
+ }
879
+ }
880
+ acc = make_float4(0.f, 0.f, 0.f, 0.f);
881
+ }
882
+ }
883
+ if constexpr (RMW) {
884
+ if (k8 + 8 < kend) {
885
+ #pragma unroll
886
+ for (int j = 0; j < 8; ++j) gc[j] = gn[j];
887
+ }
888
+ }
889
+ }
890
+ }
891
+ (void)mbar_wait_dep(tok_bar, 0u);
892
+ }
893
+
894
+ template <typename G, int NW, bool RMW>
895
+ __global__ void __launch_bounds__(NW * 32, (NW <= 8 ? DA2_SC_MINB : 1)) da2_gscatter_kernel(
896
+ G* __restrict__ grad, const float* __restrict__ gr, int64_t gr_step_rows, const uint4* __restrict__ qrec,
897
+ const int32_t* __restrict__ tb, int ti, Geo g) {
898
+ extern __shared__ __align__(128) unsigned char smem[];
899
+ gscatter_role<G, NW, RMW>(grad, gr, gr_step_rows, qrec, tb, ti, g, blockIdx.x, smem);
900
+ // A no-op unless this grid is a programmatic dependent launch (the launchers never make one): the wait would let
901
+ // it complete only after the grid launched before it has completed.
902
+ #ifndef DA2_PRE_SM90
903
+ asm volatile("griddepcontrol.wait;" ::: "memory");
904
+ #endif
905
+ }
906
+
907
+ template <typename E, typename IdxT, int NW>
908
+ __device__ __forceinline__ void gdots_role(
909
+ const E* __restrict__ bank, const float* __restrict__ gr, int64_t gr_step_rows, const IdxT* __restrict__ q_idx,
910
+ float* __restrict__ dots, int64_t plane, int ti, int valid, int TG, const Geo& g, int R, int b, int TPW) {
911
+ using RowT = typename Row4<E>::T;
912
+ const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5;
913
+ const int tg = b % TG;
914
+ b /= TG;
915
+ const int sl = b % g.NSL;
916
+ const int p = b / g.NSL;
917
+ const int64_t chunk = static_cast<int64_t>(ti) * g.P + p;
918
+ const int SW = g.SW, C = g.C, hd = g.hd;
919
+ // TPW tokens per warp (a block covers NW * TPW consecutive tokens): fewer, longer dot blocks.
920
+ for (int it = 0; it < TPW; ++it) {
921
+ const int t = (tg * TPW + it) * NW + warp;
922
+ if (t >= C) break; // warp-uniform
923
+ const int c4 = 4 * lane;
924
+ const bool on = c4 < SW && sl * SW + c4 < hd;
925
+ float4 x = make_float4(0.f, 0.f, 0.f, 0.f);
926
+ if (on) {
927
+ x = *reinterpret_cast<const float4*>(
928
+ gr + (static_cast<int64_t>(ti) * gr_step_rows + static_cast<int64_t>(p) * C + t) * hd +
929
+ static_cast<int64_t>(sl) * SW + c4);
930
+ }
931
+ const IdxT* qi = q_idx + (chunk * C + t) * static_cast<int64_t>(R);
932
+ const E* bcol = bank + static_cast<int64_t>(sl) * SW + (on ? c4 : 0);
933
+ float* drow = dots + static_cast<int64_t>(sl) * plane + (chunk * C + t) * static_cast<int64_t>(R);
934
+ const bool tvalid = t < valid;
935
+ // Per 32 entries: one coalesced load of their slot ids (lane j holds entry r32 + j; absolute slots fit int32),
936
+ // then 4 sub-batches of 8 entries with the next sub-batch's bank rows loaded before this one is reduced.
937
+ auto load8 = [&](RowT (&m)[8], int myslot, int r32, int s8) {
938
+ #pragma unroll
939
+ for (int j = 0; j < 8; ++j) { // padding tokens (t >= valid) touch neither q_idx nor the bank
940
+ const int jj = s8 + j;
941
+ const int slot = __shfl_sync(kFull, myslot, jj & 31);
942
+ const bool ld = tvalid && r32 + jj < R && jj < 32;
943
+ m[j] = (on && ld) ? *reinterpret_cast<const RowT*>(bcol + static_cast<int64_t>(slot) * hd)
944
+ : make_zero_row<RowT>();
945
+ }
946
+ };
947
+ // One 8-entry sub-batch: per-lane chain, xor-16/8/4/2/1 reduce-scatter, store entry `base + (lane >> 2) & 7`.
948
+ auto dot8 = [&](const RowT (&m)[8], int base) {
949
+ float pd[8];
950
+ #pragma unroll
951
+ for (int j = 0; j < 8; ++j) {
952
+ const float4 mr = cvt4(m[j]);
953
+ pd[j] = on ? __fmaf_rn(x.w, mr.w, __fmaf_rn(x.z, mr.z, __fmaf_rn(x.y, mr.y, __fmul_rn(x.x, mr.x)))) : 0.f;
954
+ }
955
+ const bool h4 = (lane >> 4) & 1, h3 = (lane >> 3) & 1, h2 = (lane >> 2) & 1;
956
+ float a4[4];
957
+ #pragma unroll
958
+ for (int i = 0; i < 4; ++i) {
959
+ const float keep = h4 ? pd[i + 4] : pd[i];
960
+ const float send = h4 ? pd[i] : pd[i + 4];
961
+ a4[i] = keep + __shfl_xor_sync(kFull, send, 16);
962
+ }
963
+ float a2[2];
964
+ #pragma unroll
965
+ for (int i = 0; i < 2; ++i) {
966
+ const float keep = h3 ? a4[i + 2] : a4[i];
967
+ const float send = h3 ? a4[i] : a4[i + 2];
968
+ a2[i] = keep + __shfl_xor_sync(kFull, send, 8);
969
+ }
970
+ float a1;
971
+ {
972
+ const float keep = h2 ? a2[1] : a2[0];
973
+ const float send = h2 ? a2[0] : a2[1];
974
+ a1 = keep + __shfl_xor_sync(kFull, send, 4);
975
+ }
976
+ a1 += __shfl_xor_sync(kFull, a1, 2);
977
+ a1 += __shfl_xor_sync(kFull, a1, 1);
978
+ const int rr_ = base + ((lane >> 2) & 7);
979
+ if ((lane & 3) == 0 && rr_ < R) drow[rr_] = tvalid ? a1 : 0.f;
980
+ };
981
+ if (R <= 64) {
982
+ // Common case: all <= 64 slot ids up front (two coalesced loads), then ONE pipeline over the token's
983
+ // sub-batches (the next sub-batch's rows in flight while this one reduces), no stall between 32-groups.
984
+ const int myA = (tvalid && lane < R) ? static_cast<int>(__ldg(qi + lane)) : 0;
985
+ const int myB = (tvalid && 32 + lane < R) ? static_cast<int>(__ldg(qi + 32 + lane)) : 0;
986
+ RowT mc[8], mn[8];
987
+ load8(mc, myA, 0, 0);
988
+ #pragma unroll
989
+ for (int s8 = 0; s8 < 64; s8 += 8) {
990
+ if (s8 < R) {
991
+ if (s8 + 8 < R) {
992
+ if (s8 + 8 < 32) load8(mn, myA, 0, s8 + 8);
993
+ else load8(mn, myB, 32, s8 + 8 - 32);
994
+ }
995
+ dot8(mc, s8);
996
+ #pragma unroll
997
+ for (int j = 0; j < 8; ++j) mc[j] = mn[j];
998
+ }
999
+ }
1000
+ } else {
1001
+ for (int r32 = 0; r32 < R; r32 += 32) {
1002
+ const int rj = r32 + lane;
1003
+ const int myslot = (tvalid && rj < R) ? static_cast<int>(__ldg(qi + rj)) : 0;
1004
+ const int ng = min(32, R - r32);
1005
+ RowT mc[8], mn[8];
1006
+ load8(mc, myslot, r32, 0);
1007
+ #pragma unroll
1008
+ for (int s8 = 0; s8 < 32; s8 += 8) {
1009
+ if (s8 < ng) {
1010
+ if (s8 + 8 < ng) load8(mn, myslot, r32, s8 + 8);
1011
+ dot8(mc, r32 + s8);
1012
+ #pragma unroll
1013
+ for (int j = 0; j < 8; ++j) mc[j] = mn[j];
1014
+ }
1015
+ }
1016
+ }
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ template <typename E, typename IdxT, int NW>
1022
+ __global__ void __launch_bounds__(NW * 32, DA2_DOTS_MINB) da2_gdots_kernel(
1023
+ const E* __restrict__ bank, const float* __restrict__ gr, int64_t gr_step_rows, const IdxT* __restrict__ q_idx,
1024
+ float* __restrict__ dots, int64_t plane, int ti, int valid, int TG, Geo g, int R, int TPW) {
1025
+ // launch_dependents: a no-op unless the next launch is programmatic (the launchers never make one).
1026
+ #ifndef DA2_PRE_SM90
1027
+ asm volatile("griddepcontrol.launch_dependents;");
1028
+ #endif
1029
+ gdots_role<E, IdxT, NW>(bank, gr, gr_step_rows, q_idx, dots, plane, ti, valid, TG, g, R, blockIdx.x, TPW);
1030
+ }
1031
+
1032
+ // Both GDS roles in ONE launch (no per-step fork/join): blocks [0, sblocks) scatter (they use the
1033
+ // dynamic shared memory: token rows), the rest compute dots (no shared memory). 8 warps per block for both roles.
1034
+ template <typename E, typename G, typename IdxT, bool RMW>
1035
+ __global__ void __launch_bounds__(256, 2) da2_gds2f_kernel(
1036
+ const E* __restrict__ bank, G* __restrict__ grad, const float* __restrict__ gr, int64_t gr_step_rows,
1037
+ const uint4* __restrict__ qrec, const int32_t* __restrict__ tb, const IdxT* __restrict__ q_idx,
1038
+ float* __restrict__ dots, int64_t plane, int ti, int valid, int TG, Geo g, int R, int sblocks, int TPW) {
1039
+ extern __shared__ __align__(128) unsigned char smem[];
1040
+ // Roles interleaved over blockIdx in proportion (block b is a scatter block iff floor((b+1) S / T) > floor(b S / T),
1041
+ // S = sblocks, T = gridDim.x), so every wave mixes both roles instead of running all scatter blocks first.
1042
+ const int64_t b = blockIdx.x, T = gridDim.x, S = sblocks;
1043
+ const int64_t s0 = (b * S) / T, s1 = ((b + 1) * S) / T;
1044
+ if (s1 > s0) {
1045
+ gscatter_role<G, 8, RMW>(grad, gr, gr_step_rows, qrec, tb, ti, g, static_cast<int>(s0), smem);
1046
+ } else {
1047
+ gdots_role<E, IdxT, 8>(bank, gr, gr_step_rows, q_idx, dots, plane, ti, valid, TG, g, R,
1048
+ static_cast<int>(b - s0), TPW);
1049
+ }
1050
+ }
1051
+
1052
+ template <typename K>
1053
+ void set_smem(K kernel, size_t bytes) { // cudaFuncSetAttribute is per device: key on (kernel, device)
1054
+ static std::mutex mu;
1055
+ static std::map<std::pair<const void*, int>, size_t> done;
1056
+ int dev = 0;
1057
+ C10_CUDA_CHECK(cudaGetDevice(&dev));
1058
+ std::lock_guard<std::mutex> lk(mu);
1059
+ const auto key = std::make_pair(reinterpret_cast<const void*>(kernel), dev);
1060
+ auto it = done.find(key);
1061
+ if (it != done.end() && it->second >= bytes) return;
1062
+ C10_CUDA_CHECK(cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize,
1063
+ static_cast<int>(bytes)));
1064
+ done[key] = bytes;
1065
+ }
1066
+
1067
+ int slices_of(int hd) { return (hd + 127) / 128; }
1068
+
1069
+ } // namespace
1070
+
1071
+ void da2_krec(at::Tensor kpay, at::Tensor k_idx, at::Tensor k_val, at::Tensor rex, at::Tensor cpe,
1072
+ at::Tensor krec) {
1073
+ const at::cuda::CUDAGuard guard(kpay.device());
1074
+ TORCH_CHECK(kpay.dim() == 2 && kpay.is_contiguous() && kpay.scalar_type() == at::kInt,
1075
+ "da2_krec: kpay must be a contiguous int32 [N, C * W]");
1076
+ TORCH_CHECK(k_idx.dim() == 3 && k_idx.is_contiguous() && k_val.is_contiguous() &&
1077
+ rex.is_contiguous() && cpe.is_contiguous(), "da2_krec: contiguous [N, C, W] inputs");
1078
+ TORCH_CHECK(rex.scalar_type() == at::kFloat, "da2_krec: rex must be float32");
1079
+ TORCH_CHECK(krec.is_contiguous() && krec.scalar_type() == at::kInt && krec.numel() == kpay.numel() * 4 &&
1080
+ reinterpret_cast<uintptr_t>(krec.data_ptr()) % 16 == 0,
1081
+ "da2_krec: krec must be a contiguous, 16-byte aligned int32 [N, C * W, 4]");
1082
+ const int64_t N = kpay.size(0), n = kpay.size(1);
1083
+ const int W = static_cast<int>(k_idx.size(2));
1084
+ TORCH_CHECK(k_idx.size(0) == N && k_idx.size(1) * W == n, "da2_krec: shape mismatch");
1085
+ TORCH_CHECK(cpe.scalar_type() == k_val.scalar_type(), "da2_krec: cpe dtype");
1086
+ const int64_t total = N * n;
1087
+ if (total == 0) return;
1088
+ auto stream = at::cuda::getCurrentCUDAStream();
1089
+ const int blocks = static_cast<int>((total + 255) / 256);
1090
+ uint4* out = reinterpret_cast<uint4*>(krec.data_ptr<int32_t>());
1091
+ #define DA2_REC(IDX, VAL) \
1092
+ da2_krec_kernel<IDX, VAL><<<blocks, 256, 0, stream>>>( \
1093
+ kpay.data_ptr<int32_t>(), k_idx.data_ptr<IDX>(), \
1094
+ reinterpret_cast<const VAL*>(k_val.data_ptr()), rex.data_ptr<float>(), \
1095
+ reinterpret_cast<const VAL*>(cpe.data_ptr()), out, total, static_cast<int>(n), W)
1096
+ const bool i64 = k_idx.scalar_type() == at::kLong;
1097
+ const bool bf = k_val.scalar_type() == at::kBFloat16;
1098
+ TORCH_CHECK(bf || k_val.scalar_type() == at::kFloat, "da2_krec: k_val bf16 or f32");
1099
+ TORCH_CHECK(i64 || k_idx.scalar_type() == at::kInt, "da2_krec: k_idx int32 or int64");
1100
+ if (i64 && bf) { DA2_REC(int64_t, __nv_bfloat16); }
1101
+ else if (i64) { DA2_REC(int64_t, float); }
1102
+ else if (bf) { DA2_REC(int32_t, __nv_bfloat16); }
1103
+ else { DA2_REC(int32_t, float); }
1104
+ #undef DA2_REC
1105
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1106
+ }
1107
+
1108
+ void da2_tiles(at::Tensor kpay, at::Tensor tb) {
1109
+ const at::cuda::CUDAGuard guard(kpay.device());
1110
+ TORCH_CHECK(kpay.dim() == 2 && kpay.is_contiguous() && kpay.scalar_type() == at::kInt,
1111
+ "da2_tiles: kpay contiguous int32 [N, n]");
1112
+ TORCH_CHECK(tb.dim() == 2 && tb.is_contiguous() && tb.scalar_type() == at::kInt &&
1113
+ tb.size(0) == kpay.size(0) && tb.size(1) >= 2, "da2_tiles: tb int32 [N, TU + 1]");
1114
+ const int64_t N = kpay.size(0);
1115
+ const int n = static_cast<int>(kpay.size(1));
1116
+ const int TU = static_cast<int>(tb.size(1)) - 1;
1117
+ const int64_t total = N * (TU + 1);
1118
+ if (total == 0) return;
1119
+ auto stream = at::cuda::getCurrentCUDAStream();
1120
+ da2_tiles_kernel<<<static_cast<int>((total + 255) / 256), 256, 0, stream>>>(
1121
+ kpay.data_ptr<int32_t>(), tb.data_ptr<int32_t>(), total, n, TU);
1122
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1123
+ }
1124
+
1125
+ void da2_segb(at::Tensor kpay, at::Tensor tb, at::Tensor segb) {
1126
+ const at::cuda::CUDAGuard guard(kpay.device());
1127
+ TORCH_CHECK(kpay.dim() == 2 && kpay.is_contiguous() && kpay.scalar_type() == at::kInt, "da2_segb: kpay");
1128
+ TORCH_CHECK(tb.dim() == 2 && tb.is_contiguous() && tb.scalar_type() == at::kInt && tb.size(0) == kpay.size(0),
1129
+ "da2_segb: tb");
1130
+ TORCH_CHECK(segb.sizes() == tb.sizes() && segb.scalar_type() == at::kInt && segb.is_contiguous(), "da2_segb: segb");
1131
+ const int64_t N = kpay.size(0);
1132
+ const int TU = static_cast<int>(tb.size(1)) - 1;
1133
+ if (N == 0) return;
1134
+ auto stream = at::cuda::getCurrentCUDAStream();
1135
+ da2_segb_kernel<<<static_cast<int>(N), 256, (TU + 1) * sizeof(int32_t), stream>>>(
1136
+ kpay.data_ptr<int32_t>(), tb.data_ptr<int32_t>(), segb.data_ptr<int32_t>(),
1137
+ static_cast<int>(kpay.size(1)), TU);
1138
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1139
+ }
1140
+
1141
+ void da2_dw(at::Tensor bank, at::Tensor dv, int64_t dv_step_rows, at::Tensor krec, at::Tensor tb,
1142
+ int64_t ti, int64_t P, int64_t C, int64_t K, int64_t nw, int64_t rs,
1143
+ at::Tensor snapc, at::Tensor segbuf, at::Tensor segb, int64_t sb_step, int64_t sf_full) {
1144
+ const at::cuda::CUDAGuard guard(bank.device());
1145
+ TORCH_CHECK(bank.dim() == 2 && bank.is_contiguous(), "da2_dw: bank [slots, hd] contiguous");
1146
+ const int hd = static_cast<int>(bank.size(1));
1147
+ const int NSL = slices_of(hd);
1148
+ TORCH_CHECK(hd % NSL == 0, "da2_dw: head_dim ", hd, " does not split into ", NSL, " slices");
1149
+ const int SW = hd / NSL;
1150
+ const bool bf = bank.scalar_type() == at::kBFloat16;
1151
+ TORCH_CHECK(bf || bank.scalar_type() == at::kFloat, "da2_dw: bank must be bf16 or f32");
1152
+ const int es = bf ? 2 : 4;
1153
+ TORCH_CHECK((SW * es) % 16 == 0 && (hd * es) % 16 == 0 && SW % 4 == 0,
1154
+ "da2_dw: slice rows must be 16-byte multiples (hd ", hd, ")");
1155
+ TORCH_CHECK(reinterpret_cast<uintptr_t>(bank.data_ptr()) % 16 == 0 &&
1156
+ reinterpret_cast<uintptr_t>(dv.data_ptr()) % 16 == 0, "da2_dw: 16-byte alignment");
1157
+ TORCH_CHECK(dv.scalar_type() == at::kFloat && dv.is_contiguous(), "da2_dw: dv f32 contiguous");
1158
+ TORCH_CHECK(C >= 1 && C <= 256, "da2_dw: C <= 256");
1159
+ TORCH_CHECK(krec.dim() == 3 && krec.size(2) == 4 && krec.is_contiguous() && krec.scalar_type() == at::kInt &&
1160
+ reinterpret_cast<uintptr_t>(krec.data_ptr()) % 16 == 0, "da2_dw: krec [N, n, 4] int32, 16-byte aligned");
1161
+ const int n = static_cast<int>(krec.size(1));
1162
+ const int TU = static_cast<int>(tb.size(1)) - 1;
1163
+ TORCH_CHECK(TU == K * nw, "da2_dw: tile table holds ", TU, " tiles, the grid needs ", K * nw);
1164
+ Geo g;
1165
+ g.P = static_cast<int>(P);
1166
+ g.C = static_cast<int>(C);
1167
+ g.n = n;
1168
+ g.hd = hd;
1169
+ g.NSL = NSL;
1170
+ g.SW = SW;
1171
+ g.K = static_cast<int>(K);
1172
+ g.TU = TU;
1173
+ g.tok_bytes = ((static_cast<int>(C) * SW * 4 + 64) + 127) / 128 * 128;
1174
+ g.sf_full = 0;
1175
+ g.sf_full = static_cast<int>(sf_full);
1176
+ const bool sf_launch = segb.numel() > 0; // SF: TMA bulk-run rows (SW * es)
1177
+ const size_t ring_row = !sf_launch ? 32 * 4 * es : SW * es;
1178
+ const size_t smem = static_cast<size_t>(g.tok_bytes) + static_cast<size_t>(nw) * rs * ring_row +
1179
+ static_cast<size_t>(nw) * kRecPad * 16 + 8 * (1 + static_cast<size_t>(nw) * rs);
1180
+ TORCH_CHECK(smem <= 227 * 1024, "da2_dw: ", smem, " bytes of shared memory per CTA");
1181
+ const int blocks = static_cast<int>(P) * NSL * static_cast<int>(K);
1182
+ if (blocks == 0) return;
1183
+ auto stream = at::cuda::getCurrentCUDAStream();
1184
+ const uint4* kr = reinterpret_cast<const uint4*>(krec.data_ptr<int32_t>());
1185
+ long long* stats_ptr = nullptr;
1186
+ void* snapc_ptr = nullptr;
1187
+ if (snapc.numel() > 0) {
1188
+ TORCH_CHECK(snapc.is_contiguous() && snapc.scalar_type() == bank.scalar_type() &&
1189
+ snapc.numel() == krec.size(0) * static_cast<int64_t>(n) * hd,
1190
+ "da2_dw: snapc must be the full per-entry snapshot [N * C * W, hd] in the bank dtype");
1191
+ snapc_ptr = snapc.data_ptr();
1192
+ }
1193
+ const bool sf = segb.numel() > 0;
1194
+ const void* segbuf_ptr = nullptr;
1195
+ const int32_t* segb_ptr = nullptr;
1196
+ if (sf) {
1197
+ TORCH_CHECK(bf, "da2_dw: the S/F split is compiled for bf16 banks only");
1198
+ TORCH_CHECK(segb.scalar_type() == at::kInt && segb.is_contiguous() && segb.numel() == tb.numel(),
1199
+ "da2_dw: segb int32 shaped like tb");
1200
+ TORCH_CHECK(segbuf.scalar_type() == bank.scalar_type() && segbuf.is_contiguous() &&
1201
+ segbuf.numel() >= (sb_step == 0 ? P : krec.size(0)) * static_cast<int64_t>(n) * hd &&
1202
+ reinterpret_cast<uintptr_t>(segbuf.data_ptr()) % 16 == 0, "da2_dw: segbuf");
1203
+ segbuf_ptr = segbuf.data_ptr();
1204
+ segb_ptr = segb.data_ptr<int32_t>();
1205
+ }
1206
+ #define DA2_DWM(E, NW_, RS_, M_) \
1207
+ { \
1208
+ auto kf = da2_dw_kernel<E, NW_, RS_, M_>; \
1209
+ set_smem(kf, smem); \
1210
+ kf<<<blocks, NW_ * 32, smem, stream>>>(reinterpret_cast<E*>(bank.data_ptr()), \
1211
+ dv.data_ptr<float>(), dv_step_rows, kr, \
1212
+ tb.data_ptr<int32_t>(), static_cast<int>(ti), g, \
1213
+ stats_ptr, reinterpret_cast<E*>(snapc_ptr), \
1214
+ reinterpret_cast<const E*>(segbuf_ptr), segb_ptr, \
1215
+ static_cast<int>(sb_step)); \
1216
+ }
1217
+ #define DA2_DW(E, NW_, RS_) DA2_DWM(E, NW_, RS_, 0)
1218
+ #define DA2_DWSF(NW_, RS_) DA2_DWM(__nv_bfloat16, NW_, RS_, 32)
1219
+ #define DA2_DW_E(E) \
1220
+ if (nw == 8 && rs == 16) DA2_DW(E, 8, 16) \
1221
+ else if (nw == 8 && rs == 32) DA2_DW(E, 8, 32) \
1222
+ else if (nw == 16 && rs == 16) DA2_DW(E, 16, 16) \
1223
+ else if (nw == 16 && rs == 32) DA2_DW(E, 16, 32) \
1224
+ else TORCH_CHECK(false, "da2_dw: unsupported (nw, rs) = (", nw, ", ", rs, ")");
1225
+ if (sf) {
1226
+ if (nw == 8 && rs == 16) DA2_DWSF(8, 16)
1227
+ else if (nw == 8 && rs == 32) DA2_DWSF(8, 32)
1228
+ else if (nw == 16 && rs == 16) DA2_DWSF(16, 16)
1229
+ else if (nw == 16 && rs == 32) DA2_DWSF(16, 32)
1230
+ else TORCH_CHECK(false, "da2_dw: unsupported (nw, rs) = (", nw, ", ", rs, ")");
1231
+ }
1232
+ else if (bf) { DA2_DW_E(__nv_bfloat16) }
1233
+ else { DA2_DW_E(float) }
1234
+ #undef DA2_DW_E
1235
+ #undef DA2_DWSF
1236
+ #undef DA2_DW
1237
+ #undef DA2_DWM
1238
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1239
+ }
1240
+
1241
+ void da2_qrec(at::Tensor qpay, at::Tensor q_idx, at::Tensor q_val, at::Tensor logq, at::Tensor qrec) {
1242
+ const at::cuda::CUDAGuard guard(qpay.device());
1243
+ TORCH_CHECK(qpay.dim() == 2 && qpay.is_contiguous() && qpay.scalar_type() == at::kInt,
1244
+ "da2_qrec: qpay contiguous int32 [N, C * R]");
1245
+ TORCH_CHECK(q_idx.dim() == 3 && q_idx.is_contiguous() && q_val.is_contiguous() && logq.is_contiguous(),
1246
+ "da2_qrec: contiguous [N, C, R] inputs");
1247
+ TORCH_CHECK(logq.scalar_type() == at::kFloat, "da2_qrec: log_q float32");
1248
+ TORCH_CHECK(qrec.is_contiguous() && qrec.scalar_type() == at::kInt && qrec.numel() == qpay.numel() * 4 &&
1249
+ reinterpret_cast<uintptr_t>(qrec.data_ptr()) % 16 == 0,
1250
+ "da2_qrec: qrec contiguous, 16-byte aligned int32 [N, C * R, 4]");
1251
+ const int64_t N = qpay.size(0), n = qpay.size(1);
1252
+ const int R = static_cast<int>(q_idx.size(2));
1253
+ TORCH_CHECK(q_idx.size(0) == N && q_idx.size(1) * R == n, "da2_qrec: shape mismatch");
1254
+ const int64_t total = N * n;
1255
+ if (total == 0) return;
1256
+ auto stream = at::cuda::getCurrentCUDAStream();
1257
+ const int blocks = static_cast<int>((total + 255) / 256);
1258
+ uint4* out = reinterpret_cast<uint4*>(qrec.data_ptr<int32_t>());
1259
+ #define DA2_QREC(IDX, VAL) \
1260
+ da2_qrec_kernel<IDX, VAL><<<blocks, 256, 0, stream>>>( \
1261
+ qpay.data_ptr<int32_t>(), q_idx.data_ptr<IDX>(), \
1262
+ reinterpret_cast<const VAL*>(q_val.data_ptr()), logq.data_ptr<float>(), out, total, \
1263
+ static_cast<int>(n), R)
1264
+ const bool i64 = q_idx.scalar_type() == at::kLong;
1265
+ const bool bf = q_val.scalar_type() == at::kBFloat16;
1266
+ TORCH_CHECK(bf || q_val.scalar_type() == at::kFloat, "da2_qrec: q_val bf16 or f32");
1267
+ TORCH_CHECK(i64 || q_idx.scalar_type() == at::kInt, "da2_qrec: q_idx int32 or int64");
1268
+ if (i64 && bf) { DA2_QREC(int64_t, __nv_bfloat16); }
1269
+ else if (i64) { DA2_QREC(int64_t, float); }
1270
+ else if (bf) { DA2_QREC(int32_t, __nv_bfloat16); }
1271
+ else { DA2_QREC(int32_t, float); }
1272
+ #undef DA2_QREC
1273
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1274
+ }
1275
+
1276
+ void da2_gds2(at::Tensor bank, at::Tensor grad, at::Tensor gr, int64_t gr_step_rows, at::Tensor qrec, at::Tensor tb,
1277
+ at::Tensor q_idx, at::Tensor dots, int64_t ti, int64_t valid, int64_t P, int64_t C, int64_t R, int64_t K,
1278
+ int64_t nw, int64_t which, int64_t tpw) {
1279
+ // which: 3 = both roles (two launches), 4 = both in ONE launch.
1280
+ const at::cuda::CUDAGuard guard(bank.device());
1281
+ TORCH_CHECK(which == 3 || which == 4, "da2_gds2: which must be 3 (two launches) or 4 (one fused launch)");
1282
+ TORCH_CHECK(bank.dim() == 2 && bank.is_contiguous() && grad.is_contiguous() && grad.sizes() == bank.sizes() &&
1283
+ (grad.scalar_type() == bank.scalar_type() ||
1284
+ (bank.scalar_type() == at::kBFloat16 && grad.scalar_type() == at::kFloat)),
1285
+ "da2_gds2: bank / grad [slots, hd]; grad dtype = bank dtype, or f32 with a bf16 bank");
1286
+ const int hd = static_cast<int>(bank.size(1));
1287
+ const int NSL = slices_of(hd);
1288
+ TORCH_CHECK(hd % NSL == 0, "da2_gds2: head_dim split");
1289
+ const int SW = hd / NSL;
1290
+ const bool bf = bank.scalar_type() == at::kBFloat16;
1291
+ const bool g32 = grad.scalar_type() == at::kFloat;
1292
+ TORCH_CHECK(gr.scalar_type() == at::kFloat && gr.is_contiguous(), "da2_gds2: grad_readings f32");
1293
+ TORCH_CHECK(reinterpret_cast<uintptr_t>(bank.data_ptr()) % 16 == 0 &&
1294
+ reinterpret_cast<uintptr_t>(grad.data_ptr()) % 16 == 0 &&
1295
+ reinterpret_cast<uintptr_t>(gr.data_ptr()) % 16 == 0 &&
1296
+ reinterpret_cast<uintptr_t>(dots.data_ptr()) % 4 == 0 &&
1297
+ reinterpret_cast<uintptr_t>(qrec.data_ptr()) % 16 == 0 && qrec.is_contiguous() &&
1298
+ qrec.scalar_type() == at::kInt, "da2_gds2: 16-byte alignment (bank, grad, readings, qrec)");
1299
+ TORCH_CHECK((hd % 4) == 0 && (SW % 4) == 0, "da2_gds2: head_dim / slice width multiples of 4");
1300
+ TORCH_CHECK(q_idx.is_contiguous() && (q_idx.scalar_type() == at::kInt || q_idx.scalar_type() == at::kLong),
1301
+ "da2_gds2: q_idx int32 / int64 contiguous");
1302
+ const int n = static_cast<int>(qrec.size(1));
1303
+ const int64_t plane = qrec.size(0) * static_cast<int64_t>(n);
1304
+ TORCH_CHECK(dots.numel() == plane * NSL, "da2_gds2: dots planes");
1305
+ const int TU = static_cast<int>(tb.size(1)) - 1;
1306
+ TORCH_CHECK(TU == K * nw, "da2_gds2: tiles");
1307
+ Geo g;
1308
+ g.P = static_cast<int>(P);
1309
+ g.C = static_cast<int>(C);
1310
+ g.n = n;
1311
+ g.hd = hd;
1312
+ g.NSL = NSL;
1313
+ g.SW = SW;
1314
+ g.K = static_cast<int>(K);
1315
+ g.TU = TU;
1316
+ g.tok_bytes = ((static_cast<int>(C) * SW * 4 + 64) + 127) / 128 * 128;
1317
+ g.sf_full = 0;
1318
+ auto stream = at::cuda::getCurrentCUDAStream();
1319
+ const uint4* qr = reinterpret_cast<const uint4*>(qrec.data_ptr<int32_t>());
1320
+ // (2) scatter, sorted order
1321
+ const size_t smem = static_cast<size_t>(g.tok_bytes) + static_cast<size_t>(nw) * 32 * 16 + 16;
1322
+ const int sblocks = static_cast<int>(P) * NSL * static_cast<int>(K);
1323
+ if (which == 4) {
1324
+ TORCH_CHECK(nw == 8, "da2_gds2: the fused launch uses 8 warps per block");
1325
+ // Dot blocks inherit the scatter's shared-memory reservation in the fused launch: make them few and long
1326
+ // (TPW tokens per warp).
1327
+ const int TPWf = static_cast<int>(std::max<int64_t>(1, std::min<int64_t>(tpw, 16)));
1328
+ const int TGf = static_cast<int>((C + 8 * TPWf - 1) / (8 * TPWf));
1329
+ const int total = sblocks + static_cast<int>(P) * NSL * TGf;
1330
+ if (total == 0) return;
1331
+ const bool i64 = q_idx.scalar_type() == at::kLong;
1332
+ #define DA2_GF(E_, G_, I_) \
1333
+ { \
1334
+ auto kf = da2_gds2f_kernel<E_, G_, I_, true>; \
1335
+ set_smem(kf, smem); \
1336
+ kf<<<total, 256, smem, stream>>>(reinterpret_cast<const E_*>(bank.data_ptr()), \
1337
+ reinterpret_cast<G_*>(grad.data_ptr()), gr.data_ptr<float>(), gr_step_rows, qr, \
1338
+ tb.data_ptr<int32_t>(), q_idx.data_ptr<I_>(), dots.data_ptr<float>(), plane, \
1339
+ static_cast<int>(ti), static_cast<int>(valid), TGf, g, static_cast<int>(R), sblocks, TPWf); \
1340
+ }
1341
+ if (bf && g32) { if (i64) DA2_GF(__nv_bfloat16, float, int64_t) else DA2_GF(__nv_bfloat16, float, int32_t) }
1342
+ else if (bf) { if (i64) DA2_GF(__nv_bfloat16, __nv_bfloat16, int64_t) else DA2_GF(__nv_bfloat16, __nv_bfloat16, int32_t) }
1343
+ else { if (i64) DA2_GF(float, float, int64_t) else DA2_GF(float, float, int32_t) }
1344
+ #undef DA2_GF
1345
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1346
+ return;
1347
+ }
1348
+ #define DA2_GSC(G_, NW_) \
1349
+ { \
1350
+ auto kf = da2_gscatter_kernel<G_, NW_, true>; \
1351
+ set_smem(kf, smem); \
1352
+ kf<<<sblocks, NW_ * 32, smem, stream>>>(reinterpret_cast<G_*>(grad.data_ptr()), gr.data_ptr<float>(), \
1353
+ gr_step_rows, qr, tb.data_ptr<int32_t>(), static_cast<int>(ti), g); \
1354
+ }
1355
+ if (sblocks > 0 && (which & 1)) {
1356
+ if (g32) { if (nw == 16) DA2_GSC(float, 16) else DA2_GSC(float, 8) }
1357
+ else { if (nw == 16) DA2_GSC(__nv_bfloat16, 16) else DA2_GSC(__nv_bfloat16, 8) }
1358
+ }
1359
+ #undef DA2_GSC
1360
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1361
+ // (1) dots, token order: one warp per token, 8 warps per CTA
1362
+ const int TG = static_cast<int>((C + 7) / 8);
1363
+ const int dblocks = static_cast<int>(P) * NSL * TG;
1364
+ #define DA2_GD(E_, I_) \
1365
+ da2_gdots_kernel<E_, I_, 8><<<dblocks, 256, 0, stream>>>( \
1366
+ reinterpret_cast<const E_*>(bank.data_ptr()), gr.data_ptr<float>(), gr_step_rows, \
1367
+ q_idx.data_ptr<I_>(), dots.data_ptr<float>(), plane, static_cast<int>(ti), static_cast<int>(valid), TG, g, \
1368
+ static_cast<int>(R), 1);
1369
+ if (dblocks > 0 && (which & 2)) {
1370
+ const bool i64 = q_idx.scalar_type() == at::kLong;
1371
+ if (bf) { if (i64) { DA2_GD(__nv_bfloat16, int64_t) } else { DA2_GD(__nv_bfloat16, int32_t) } }
1372
+ else { if (i64) { DA2_GD(float, int64_t) } else { DA2_GD(float, int32_t) } }
1373
+ }
1374
+ #undef DA2_GD
1375
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1376
+ }
1377
+
1378
+ void da2_dot_reduce(at::Tensor part, at::Tensor out, int64_t nsl) {
1379
+ const at::cuda::CUDAGuard guard(part.device());
1380
+ const int64_t total = out.numel();
1381
+ TORCH_CHECK(part.is_contiguous() && out.is_contiguous() && part.numel() == total * nsl &&
1382
+ part.scalar_type() == at::kFloat && out.scalar_type() == at::kFloat, "da2_dot_reduce");
1383
+ if (total == 0) return;
1384
+ auto stream = at::cuda::getCurrentCUDAStream();
1385
+ da2_dot_reduce_kernel<<<static_cast<int>((total + 255) / 256), 256, 0, stream>>>(
1386
+ part.data_ptr<float>(), out.data_ptr<float>(), total, static_cast<int>(nsl));
1387
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
1388
+ }
1389
+ """
1390
+
1391
+
1392
+ @functools.cache
1393
+ def _module():
1394
+ return load_cuda_extension(
1395
+ "sdm_mover_da2",
1396
+ cpp_sources=_CPP,
1397
+ cuda_sources=_CUDA,
1398
+ functions=("da2_krec", "da2_tiles", "da2_dw", "da2_qrec", "da2_dot_reduce", "da2_segb", "da2_gds2"),
1399
+ extra_cuda_cflags=("-O3", "-lineinfo"),
1400
+ )
1401
+
1402
+
1403
+ # ------------------------------------------------------------------ geometry (shapes only)
1404
+
1405
+ _GEOMS = {(8, 16), (8, 32), (16, 16), (16, 32)}
1406
+
1407
+
1408
+ _SMEM_MAX = 227 * 1024 # GB300 / H100 opt-in per block; fits() narrows it to the device's own limit
1409
+
1410
+
1411
+ def _smem_limit(device) -> int:
1412
+ """The device class's opt-in shared memory per block (never the grid or the geometry)."""
1413
+ return smem_cap(device)
1414
+
1415
+
1416
+ def _smem_bytes(chunk_size, head_dim, row_es, nw, rs):
1417
+ """Shared memory of a DW (row_es = (bank es,)) or GDS (row_es = (bank es, grad es)) CTA."""
1418
+ nsl = -(-head_dim // 128)
1419
+ sw = head_dim // nsl
1420
+ tok = (chunk_size * sw * 4 + 64 + 127) // 128 * 128
1421
+ slot = sum(32 * 4 * es for es in row_es)
1422
+ return tok + nw * rs * slot + nw * 40 * 16 + 8 * (1 + nw * rs)
1423
+
1424
+
1425
+ def _fit(pairs, want, chunk_size, head_dim, row_es, limit=_SMEM_MAX):
1426
+ """`want` if its shared memory fits, else the first compiled pair (largest first) that fits."""
1427
+ if _smem_bytes(chunk_size, head_dim, row_es, *want) <= limit:
1428
+ return want
1429
+ for nw, rs in sorted(pairs, key=lambda x: -(x[0] * x[1])):
1430
+ if _smem_bytes(chunk_size, head_dim, row_es, nw, rs) <= limit:
1431
+ return nw, rs
1432
+ raise ValueError(f"no mover geometry fits shared memory (C={chunk_size}, hd={head_dim})")
1433
+
1434
+
1435
+ def dw_geometry(chunk_size: int, width: int, head_dim: int, bank_dtype: torch.dtype, limit: int = _SMEM_MAX):
1436
+ """(nw, rs, ppw): warps per CTA, ring rows per warp, nominal positions per warp tile: (8, 16, 128), or the
1437
+ largest compiled (nw, rs) that fits shared memory. From the shapes only (never the SM count); the output does not
1438
+ depend on any of them (see the module docstring)."""
1439
+ es = bank_dtype.itemsize
1440
+ nw, rs = _fit(_GEOMS, (8, 16), chunk_size, head_dim, (es,), limit)
1441
+ return nw, rs, 128
1442
+
1443
+
1444
+ def fits(bank: torch.Tensor, chunk_size: int, kind: str, grad_dtype=None) -> bool:
1445
+ """Whether some compiled geometry of the `kind` ("dw" | "gds") mover fits shared memory."""
1446
+ es = bank.element_size()
1447
+ if kind == "dw":
1448
+ pairs, row_es = _GEOMS, (es,)
1449
+ else:
1450
+ esg = grad_dtype.itemsize if grad_dtype is not None else es
1451
+ pairs, row_es = _GDS_GEOMS, (es, esg)
1452
+ try:
1453
+ _fit(pairs, min(pairs), chunk_size, bank.shape[-1], row_es, _smem_limit(bank.device))
1454
+ return True
1455
+ except ValueError:
1456
+ return False
1457
+
1458
+
1459
+ def supported(bank: torch.Tensor, chunk_size: int) -> bool:
1460
+ """Shapes, dtype and architecture these movers handle (the modern backend: TMA bulk copies and mbarrier
1461
+ transaction counts); anything else takes phase2_kernels' launchers."""
1462
+ if not bank.is_cuda or backend(bank.device) != "modern":
1463
+ return False
1464
+ return _shape_supported(bank, chunk_size)
1465
+
1466
+
1467
+ def _shape_supported(bank: torch.Tensor, chunk_size: int) -> bool:
1468
+ # Slot ids travel as signed int32 in the records and ring addressing: banks of >= 2^31 rows take phase2_kernels' path.
1469
+ if bank.dim() != 2 or bank.shape[0] >= 2 ** 31:
1470
+ return False
1471
+ return _shape_supported_inner(bank, chunk_size)
1472
+
1473
+
1474
+ def _shape_supported_inner(bank: torch.Tensor, chunk_size: int) -> bool:
1475
+ hd = bank.shape[-1]
1476
+ nsl = -(-hd // 128)
1477
+ if hd % nsl:
1478
+ return False
1479
+ sw = hd // nsl
1480
+ es = bank.element_size()
1481
+ return (bank.dtype in (torch.bfloat16, torch.float32) and sw % 4 == 0 and (sw * es) % 16 == 0
1482
+ and (hd * es) % 16 == 0 and chunk_size <= 256)
1483
+
1484
+
1485
+ def build_krec(spay_k, k_idx, k_val, rex, cpe):
1486
+ """krec [N, C * W, 4] int32 per sorted write position (the Phase-1 record contract)."""
1487
+ n = spay_k.shape[-1]
1488
+ spay_k = spay_k.reshape(-1, n)
1489
+ krec = torch.empty(spay_k.shape[0], n, 4, device=spay_k.device, dtype=torch.int32)
1490
+ _module().da2_krec(spay_k, k_idx.contiguous(), k_val.contiguous(), rex.contiguous(),
1491
+ cpe.contiguous(), krec)
1492
+ return krec
1493
+
1494
+
1495
+ def build_tiles(spay_k, tiles: int):
1496
+ n = spay_k.shape[-1]
1497
+ spay_k = spay_k.reshape(-1, n)
1498
+ tb = torch.empty(spay_k.shape[0], tiles + 1, device=spay_k.device, dtype=torch.int32)
1499
+ _module().da2_tiles(spay_k, tb)
1500
+ return tb
1501
+
1502
+
1503
+
1504
+ def _tiles_or_build(spay, tiles, TU, fill_last=False):
1505
+ """Tile bounds [N, TU + 1] int32: Phase 1's FIRST-snapped tiles (tiles[t] = first FIRST >= t * L, last
1506
+ entry n; or a per-tile count prefix with fill_last) padded to TU + 1 with their last entry, when given and not
1507
+ longer than TU + 1; else None (the caller builds its own). Any monotone FIRST-snapped bounds give the same bits."""
1508
+ if tiles is None or tiles.numel() == 0:
1509
+ return None
1510
+ t2 = tiles.reshape(-1, tiles.shape[-1])
1511
+ if t2.shape[0] != spay.reshape(-1, spay.shape[-1]).shape[0] or t2.shape[1] > TU + 1 or t2.dtype != torch.int32:
1512
+ return None
1513
+ if t2.shape[1] == TU + 1:
1514
+ return t2.contiguous()
1515
+ pad = t2[:, -1:].expand(-1, TU + 1 - t2.shape[1])
1516
+ return torch.cat([t2, pad], dim=1).contiguous()
1517
+
1518
+ def build_qrec(spay_q, q_idx, q_val, log_q):
1519
+ """qrec [N, C * R, 4] int32 per sorted read position {spay_q element, abs slot, scale, 0}."""
1520
+ n = spay_q.shape[-1]
1521
+ spay_q = spay_q.reshape(-1, n)
1522
+ qrec = torch.empty(spay_q.shape[0], n, 4, device=spay_q.device, dtype=torch.int32)
1523
+ _module().da2_qrec(spay_q, q_idx.contiguous(), q_val.contiguous(), log_q.contiguous(), qrec)
1524
+ return qrec
1525
+
1526
+
1527
+ _GDS_GEOMS = {(8, 16), (8, 8), (16, 16), (16, 8)}
1528
+
1529
+
1530
+ def gds_geometry(chunk_size: int, width: int, head_dim: int, bank_dtype: torch.dtype, grad_dtype=None,
1531
+ limit: int = _SMEM_MAX):
1532
+ """(nw, rs, ppw) of the GDS mover: (8, 8, 128), or the largest compiled (nw, rs) that fits shared memory; shapes
1533
+ only."""
1534
+ es = torch.tensor([], dtype=bank_dtype).element_size()
1535
+ esg = torch.tensor([], dtype=grad_dtype or bank_dtype).element_size()
1536
+ nw, rs = _fit(_GDS_GEOMS, (8, 8), chunk_size, head_dim, (es, esg), limit)
1537
+ return nw, rs, 128
1538
+
1539
+
1540
+ def make_gds(bank, grad_bank, grad_readings, spay_q, qrec, d_q_w, *, chunk_size, num_parallel,
1541
+ steps, q_idx=None, valid_tokens=None, tiles_q=None):
1542
+ """Step launcher of the GDS mover. The backward walks the steps in reverse, so the slice
1543
+ partials of a wide head's dots are summed (in slice order) when step 0 runs."""
1544
+ hd = bank.shape[-1]
1545
+ sc = num_parallel * chunk_size
1546
+ if grad_readings.numel() == steps * sc * hd:
1547
+ gr_rows = sc
1548
+ elif grad_readings.numel() == sc * hd:
1549
+ gr_rows = 0
1550
+ else:
1551
+ raise ValueError("grad_readings: unexpected size")
1552
+ n = spay_q.shape[-1]
1553
+ r = n // chunk_size
1554
+ nsl = -(-hd // 128)
1555
+ nw, rs, ppw = gds_geometry(chunk_size, r, hd, bank.dtype, grad_bank.dtype, _smem_limit(bank.device))
1556
+ K = max(1, -(-n // (nw * ppw)))
1557
+ tb = _tiles_or_build(spay_q, tiles_q, K * nw) if ppw == 128 else None # Phase 1's tiles use L = 128
1558
+ if tb is None:
1559
+ tb = build_tiles(spay_q, K * nw)
1560
+ dqw = d_q_w.view(-1)
1561
+ part = torch.empty(nsl * dqw.numel(), device=bank.device, dtype=torch.float32) if nsl > 1 else dqw
1562
+ # Non-f32 readings (direct callers; the package's backward stages f32): converted at every launch into one f32
1563
+ # buffer, so a caller (or the aligned-scratch refresh) updating the readings between steps is always seen.
1564
+ gr_conv = grad_readings.dtype != torch.float32
1565
+ gr = grad_readings if not gr_conv else torch.empty(grad_readings.shape, device=grad_readings.device,
1566
+ dtype=torch.float32)
1567
+ mod = _module()
1568
+
1569
+ # Dots in token order + the grad scatter in sorted order with the single-writer RMW update (da2_gds2: one
1570
+ # rounding per element); one fused launch with 8 warps per block, else two launches.
1571
+ if q_idx is None or valid_tokens is None:
1572
+ raise ValueError("make_gds: q_idx [N, C, R] and valid_tokens are required (the token-order dots read them)")
1573
+ qix = q_idx.reshape(-1, chunk_size, r).contiguous()
1574
+ fused = nw == 8
1575
+
1576
+ def launch(ti: int) -> None:
1577
+ if gr_conv:
1578
+ gr.copy_(grad_readings)
1579
+ valid = max(0, min(chunk_size, valid_tokens - ti * chunk_size))
1580
+ mod.da2_gds2(bank, grad_bank, gr, gr_rows, qrec, tb, qix, part, ti, valid, num_parallel, chunk_size, r, K, nw,
1581
+ 4 if fused else 3, 1)
1582
+ if nsl > 1 and ti == 0:
1583
+ mod.da2_dot_reduce(part, dqw, nsl)
1584
+
1585
+ launch.geom = dict(nw=nw, rs=rs, ppw=ppw, K=K, nsl=nsl)
1586
+ return launch
1587
+
1588
+
1589
+ def make_dw(bank, delta_v, spay_k, krec, *, chunk_size, num_parallel, steps, snapshot=None, tiles_k=None,
1590
+ segb_k=None, snapshot_quant="none", payload=None, scale=None, valid_tokens=None):
1591
+ """Step launcher of the DW mover. delta_v: f32 rows of one step or of every step. A snapshot tagged by the forward
1592
+ (snapshot._dc_snapc_layout) selects the base-row source: layout 2 (saving forward: the dense compact rows, row
1593
+ chunk * n + g of each valid group) or layout 4 (no-grad forward: the bank itself, mover_fx_cuda's bank-direct
1594
+ kernel only); otherwise the walk reads bank[slot] and the gather writes the per-entry stash. snapshot_quant fp8 /
1595
+ int4: mover_fx_cuda's quantized bank-direct DW writes the compact rows (layout 2, by the DW); a quantized call
1596
+ without them raises (2.0.0: memory_ops.refuse_non_native refuses it up front). The launcher announces what it reads
1597
+ and writes (reads_snapc, writes_snapc)."""
1598
+ hd = bank.shape[-1]
1599
+ sc = num_parallel * chunk_size
1600
+ if delta_v.numel() == steps * sc * hd:
1601
+ dv_rows = sc
1602
+ elif delta_v.numel() == sc * hd:
1603
+ dv_rows = 0
1604
+ else:
1605
+ raise ValueError(f"delta_v: expected {sc} or {steps * sc} rows of {hd}")
1606
+ n = spay_k.shape[-1]
1607
+ # This module's DW kernels run on the modern backend only; elsewhere (a100 / rtx) mover_fx_cuda's portable walk takes
1608
+ # every call (movers.dw_admits / _fx_layout) with Phase 1's tile set (8 warps, L = 128) as the default tiles.
1609
+ modern = backend(bank.device) == "modern"
1610
+ tail = not _shape_supported(bank, chunk_size) # tail: only mover_fx_cuda's DW runs it (checked below)
1611
+ if modern:
1612
+ try:
1613
+ nw, rs, ppw = dw_geometry(chunk_size, n // chunk_size, hd, bank.dtype, _smem_limit(bank.device))
1614
+ except ValueError:
1615
+ if not tail:
1616
+ raise
1617
+ nw, rs, ppw = 8, 16, 128 # a tail never runs this module's DW: nominal tiles for mover_fx
1618
+ else:
1619
+ nw, rs, ppw = 8, 16, 128
1620
+ K = max(1, -(-n // (nw * ppw)))
1621
+ tb = _tiles_or_build(spay_k, tiles_k, K * nw) if ppw == 128 else None # Phase 1's tiles use L = 128
1622
+ segb_dp = _tiles_or_build(spay_k, segb_k, K * nw) if tb is not None else None
1623
+ if tb is None:
1624
+ tb = build_tiles(spay_k, K * nw)
1625
+ mod = _module()
1626
+
1627
+ snapc = torch.empty(0, device=bank.device, dtype=bank.dtype)
1628
+ # snapshot_quant bf16 on a float32 bank (the forward passes "none" here): the every-step compact rows are bf16, tagged
1629
+ # layout 2 and written by the DW (by_dw); only mover_fx_cuda's bank-direct WR walk writes them (bf16 stores, RN).
1630
+ s16 = (snapshot is not None and bank.dtype == torch.float32 and snapshot.dtype == torch.bfloat16
1631
+ and snapshot_quant == "none" and getattr(snapshot, "_dc_snapc_layout", 0) == 2
1632
+ and bool(getattr(snapshot, "_dc_snapc_by_dw", False)))
1633
+ full_snap = (snapshot is not None and (snapshot.dtype == bank.dtype or s16) and snapshot.is_contiguous()
1634
+ and snapshot.data_ptr() % 16 == 0 and snapshot_quant == "none"
1635
+ and snapshot.numel() == steps * sc * (n // chunk_size) * hd)
1636
+ # float32 banks: mover_fx_cuda's float32 instantiations (the one-slice bank-direct walk, 8 / 16 warps) serve layouts
1637
+ # 4 and 2 when they take the call (fx32: the predicate of the launcher loop below with nw_fx); every other float32
1638
+ # call keeps this module's DW and the gather's per-entry stash (layout 0), as before.
1639
+ fx32 = False
1640
+ if bank.dtype == torch.float32 and snapshot is not None and snapshot_quant == "none":
1641
+ from . import mover_fx_cuda as _fx
1642
+ nw_fx32, _ = _fx.tile_geometry(num_parallel, hd, n, chunk_size, 8, bank.device)
1643
+ fx32 = _fx.supported(bank, chunk_size, nw_fx32, True)
1644
+ if (tail or s16) and not fx32: # tails, bf16 rows: any bank-direct warp count that fits (s16_supported)
1645
+ fx32 = any(_fx.supported(bank, chunk_size, w, True) for w in (8, 16))
1646
+ fx_dtype = bank.dtype == torch.bfloat16 or fx32
1647
+ # Layout 4 (no-grad forward, bank-direct), tagged by the writer: the forward writes no compact rows; mover_fx_cuda's
1648
+ # DW reads each valid group's base row from bank[slot] (the pre-step row). Only that kernel reads the bank this way:
1649
+ # when it does not run, the launcher announces 0 (this module's DW and the gather's per-step stash).
1650
+ step_bank = (fx_dtype and snapshot is not None
1651
+ and getattr(snapshot, "_dc_snapc_layout", 0) == 4 and snapshot.dtype == bank.dtype
1652
+ and snapshot.is_contiguous() and snapshot.data_ptr() % 16 == 0 and snapshot_quant == "none"
1653
+ and snapshot.numel() == sc * (n // chunk_size) * hd)
1654
+ # Dense compact snapshot (saving forward, writer-tagged layout 2: row chunk * n + g of each valid group, g its
1655
+ # ordinal): the dense walk reads its base rows there (group order); DW writes no snapc.
1656
+ dense = (fx_dtype and full_snap and not step_bank
1657
+ and getattr(snapshot, "_dc_snapc_layout", 0) == 2)
1658
+ sf = dense
1659
+ noint = torch.empty(0, device=bank.device, dtype=torch.int32)
1660
+ sb_step, sf_full = 0, 0
1661
+ if dense:
1662
+ segb = segb_dp if segb_dp is not None else build_segb(spay_k, tb)
1663
+ segbuf, sb_step, sf_full = snapshot, num_parallel, 1
1664
+ snapc = snapshot[:0] # no DW-side snapc writes
1665
+ else:
1666
+ segb, segbuf = noint, snapc[:0]
1667
+ # snapshot_quant fp8 / int4 (training forward only): the DW updates the bank exactly as without quantization
1668
+ # (the no-grad forward never quantizes, so both forwards run the same DW arithmetic).
1669
+ if snapshot_quant == "bf16": # bf16 rows: the forward passes "none" with its bf16 compact buffer (no per-entry route)
1670
+ raise RuntimeError("make_dw: snapshot_quant bf16 has no per-entry route (the forward passes 'none')")
1671
+ qmode = {"none": 0, "fp8": 1, "int4": 2}[snapshot_quant]
1672
+ if qmode and (snapshot is None or payload is None or scale is None or valid_tokens is None):
1673
+ raise ValueError("make_dw: snapshot_quant needs snapshot, payload, scale and valid_tokens")
1674
+ # fp8 / int4 compact rows (saving forward, writer-tagged layout 2 + by-DW, eager and captured): mover_fx_cuda's
1675
+ # quantized bank-direct DW writes each valid group's pre-write row quantized to payload / scale (row chunk * n + g).
1676
+ # It is the only quantized writer (2.0.0, A4: the per-entry quantizer da2_quant is gone): any other quantized call
1677
+ # raises below; the op refuses such calls before any work (memory_ops.refuse_non_native: forward._fxq_rows).
1678
+ qdense = False
1679
+ qform = 0 # the kernels' form: 1 / 2 cluster, 5 / 6 all slices (quant_choice)
1680
+ if (qmode and getattr(snapshot, "_dc_snapc_layout", 0) == 2 and getattr(snapshot, "_dc_snapc_by_dw", False)
1681
+ and snapshot.dtype == bank.dtype and snapshot.is_contiguous() and snapshot.data_ptr() % 16 == 0):
1682
+ from . import mover_fx_cuda as _fx
1683
+ qform = _fx.quant_choice(bank, chunk_size, qmode)
1684
+ qdense = qform != 0
1685
+ if qmode and not qdense:
1686
+ raise RuntimeError("make_dw: a quantized call needs mover_fx_cuda's quantized compact rows (snapshot layout 2 "
1687
+ "written by the DW, quant_choice != 0); memory_ops.refuse_non_native refuses the others")
1688
+ fxq = dict(quant=qform, payload=payload, scale=scale) if qdense else {}
1689
+
1690
+ # mover_fx_cuda's DW (8-row base-row units, 2D tensor-map boxes for column-sliced heads) when it supports the
1691
+ # shape: bitwise this module's dense walk, with the same records, tiles and segb.
1692
+ fx_launch = None
1693
+ # Bank-direct DW for layout 4 and for the dense layout (the dense rows the backward needs are still written; the
1694
+ # DW reads the same values from the bank).
1695
+ # Off the modern backend no other CUDA DW exists: a call without one of mover_fx_cuda's layouts (layout 0: the gather
1696
+ # writes the per-entry stash) runs its bank-direct walk too (the no-grad forward's DW, hence the same bank update).
1697
+ stash0 = not modern and not (step_bank or dense or qdense)
1698
+ bankb = step_bank or dense or qdense or stash0
1699
+ # mover_fx_cuda's snapc argument: the dense rows (layout 2), or for layout 4 the one-step snapshot (shape-checked
1700
+ # and tensor-map anchor only: the bank-direct DW reads no snapshot rows). This module's own arguments (segbuf /
1701
+ # sb_step) are left untouched.
1702
+ fx_snap, fx_sb = (snapshot, 0) if step_bank else (segbuf if dense else None, sb_step)
1703
+ if qdense: # the one-step snapshot anchors the maps; rows of every step
1704
+ fx_snap, fx_sb = snapshot, num_parallel
1705
+ if stash0: # shape-checked only: the bank-direct walk reads no snapshot rows
1706
+ fx_snap, fx_sb = snapshot, 0
1707
+ # The bank-direct DW also writes the dense compact rows of the saving forward (layout 2) when the forward asked for
1708
+ # it on the snapshot tensor (snapshot._dc_snapc_by_dw, set by the writer only) and the kernel runs; the launcher
1709
+ # announces it (writes_snapc = 2) and the forward's gather then writes no compact rows.
1710
+ snapw = dense and bankb and bool(getattr(snapshot, "_dc_snapc_by_dw", False))
1711
+ if dense or step_bank or qdense or stash0:
1712
+ from . import mover_fx_cuda as _fx
1713
+ # mover_fx_cuda's tile geometry: shorter warp tiles (ppw 64 / 32) when a step has few warps, 16-warp CTAs
1714
+ # when the token rows are large. Speed only: every tile is cut at FIRST boundaries, so each group stays whole in
1715
+ # one warp and is summed in sorted order (the same bits for any tiles). Phase 1's L = 128 tiles serve ppw 128
1716
+ # (padded); other tile lengths are built here (tiles + segb, once per factory call).
1717
+ nw_fx, ppw_fx = _fx.tile_geometry(num_parallel, bank.shape[-1], n, chunk_size,
1718
+ nw if bank.dtype == torch.bfloat16 else 8, bank.device,
1719
+ quant=qform)
1720
+ # Two column slices per warp (fx_dw2_kernel; even slice counts; bf16 banks) when its geometry fits: tiles of
1721
+ # ppw2 positions, built here (FIRST-snapped like every tile set: the same bits for any tiles).
1722
+ g2 = (_fx.tile_geometry_w2(num_parallel, bank.shape[-1], n, chunk_size, _fx._smem_limit(bank.device),
1723
+ bank.device, quant=qform)
1724
+ if bankb and bank.dtype == torch.bfloat16 and _fx.rows16(bank) # tails: one slice per warp
1725
+ and _fx.supported(bank, chunk_size, 8, True) else None)
1726
+ if g2 is not None:
1727
+ nw_c, ppw_c = g2
1728
+ K_fx = max(1, -(-n // (nw_c * ppw_c)))
1729
+ if qdense: # quantized: one wave of co-resident clusters (speed only)
1730
+ K_fx = _fx.quant_k(bank, chunk_size, qform, True, nw_c, num_parallel, n, K_fx)
1731
+ tb_fx = build_tiles(spay_k, K_fx * nw_c)
1732
+ segb_fx = build_segb(spay_k, tb_fx) if snapw or qdense else tb_fx
1733
+ fx_launch = _fx.make_dw_fx(bank, delta_v, krec, tb_fx, segb_fx, fx_snap, chunk_size=chunk_size,
1734
+ num_parallel=num_parallel, dv_rows=dv_rows, sb_step=fx_sb, nw=nw_c, K=K_fx,
1735
+ bankb=bankb, snapw=snapw, w2=True, **fxq)
1736
+ fx_launch.geom = dict(fx_launch.geom, ppw=ppw_c)
1737
+ for nw_c in (() if g2 is not None else dict.fromkeys((nw_fx, nw) + ((8, 16) if qdense or tail or s16 else ()))):
1738
+ if not _fx.supported(bank, chunk_size, nw_c, bankb) or (qdense and not _fx.quant_geometry(bank, chunk_size, nw_c, qform)):
1739
+ continue
1740
+ K_fx = max(1, -(-n // (nw_c * ppw_fx)))
1741
+ if qdense:
1742
+ K_fx = _fx.quant_k(bank, chunk_size, qform, False, nw_c, num_parallel, n, K_fx)
1743
+ no_segb = bankb and not snapw and not qdense # the bank-direct DW never reads segb (shape-checked only); writing the dense rows uses its ordinals
1744
+ if K_fx * nw_c == K * nw and ppw_fx == ppw:
1745
+ tb_fx = tb
1746
+ segb_fx = tb if no_segb else (segb if dense else build_segb(spay_k, tb))
1747
+ else:
1748
+ tb_fx = _tiles_or_build(spay_k, tiles_k, K_fx * nw_c) if ppw_fx == 128 else None
1749
+ segb_fx = (_tiles_or_build(spay_k, segb_k, K_fx * nw_c) if tb_fx is not None and not no_segb
1750
+ else None)
1751
+ if tb_fx is None:
1752
+ tb_fx = build_tiles(spay_k, K_fx * nw_c)
1753
+ if no_segb:
1754
+ segb_fx = tb_fx
1755
+ elif segb_fx is None:
1756
+ segb_fx = build_segb(spay_k, tb_fx)
1757
+ fx_launch = _fx.make_dw_fx(bank, delta_v, krec, tb_fx, segb_fx, fx_snap, chunk_size=chunk_size,
1758
+ num_parallel=num_parallel, dv_rows=dv_rows, sb_step=fx_sb, nw=nw_c, K=K_fx,
1759
+ bankb=bankb, snapw=snapw, **fxq)
1760
+ fx_launch.geom = dict(fx_launch.geom, ppw=ppw_fx)
1761
+ break
1762
+ if fx32 and (dense or step_bank) and fx_launch is None: # the loop's own predicate admitted it: unreachable
1763
+ raise RuntimeError("make_dw: mover_fx_cuda declined a float32 call its admission accepted")
1764
+ if s16 and (fx_launch is None or not snapw): # bf16 compact rows: only that walk writes them (s16_supported admitted it)
1765
+ raise RuntimeError("make_dw: bf16 compact snapshot rows need mover_fx_cuda's bank-direct WR walk, which declined")
1766
+ if qdense and fx_launch is None: # quant_supported admitted it (16 warps fit): unreachable
1767
+ raise RuntimeError("make_dw: mover_fx_cuda declined a quantized call its admission accepted")
1768
+ if not modern and fx_launch is None: # mover_fx_cuda.admits accepted it, and no other CUDA DW exists there
1769
+ raise RuntimeError("make_dw: mover_fx_cuda declined a call on the a100 / rtx backend that its admission accepted")
1770
+ if tail and fx_launch is None: # tail shape: this module's DW cannot run it
1771
+ raise RuntimeError("make_dw: a tail shape (non-16-byte slices) needs mover_fx_cuda's bank-direct DW")
1772
+
1773
+ def launch(ti: int) -> None:
1774
+ if fx_launch is not None:
1775
+ fx_launch(ti)
1776
+ return
1777
+ mod.da2_dw(bank, delta_v, dv_rows, krec, tb, ti, num_parallel, chunk_size, K, nw, rs,
1778
+ snapc, segbuf, segb, sb_step, sf_full)
1779
+
1780
+ launch.geom = dict(nw=nw, rs=rs, ppw=ppw, K=K, sf=sf, dense=dense, step_bank=step_bank,
1781
+ fx=fx_launch.geom if fx_launch is not None else None)
1782
+ # Handshake with the forward (it writes compact rows only when the DW launcher announces it reads them, in the
1783
+ # announced layout): 2 = dense group ordinals, 4 = the bank-direct DW reads the bank, 0 = the DW reads the bank and
1784
+ # the gather writes the per-entry stash as usual.
1785
+ launch.reads_snapc = 2 if dense or (qdense and fx_launch is not None) else (4 if step_bank and fx_launch is not None else 0)
1786
+ # Programmatic dependent launches of mover_fx_cuda's DW steps (the caller asserts the launch-order rule, see
1787
+ # mover_fx_cuda.make_dw_fx); this module's kernels never launch that way.
1788
+ launch.set_pdl = fx_launch.set_pdl if fx_launch is not None else (lambda on: None)
1789
+ launch.fx = fx_launch # mover_fx_cuda's DW launcher (its plan feeds the fused deterministic step loop), or None
1790
+ # 2 = this launcher writes the dense compact rows (layout 2) of every step itself, raw or quantized (fx quant)
1791
+ # (the forward's gather must not).
1792
+ launch.writes_snapc = 2 if fx_launch is not None and (fx_launch.geom.get("snapw") or fx_launch.geom.get("quant")) else 0
1793
+ return launch
1794
+
1795
+
1796
+ def build_segb(spay, tb):
1797
+ """segb [N, TU + 1] int32: valid group FIRSTs before each tile bound (the dense walk's ordinals)."""
1798
+ n = spay.shape[-1]
1799
+ spay = spay.reshape(-1, n)
1800
+ segb = torch.empty_like(tb)
1801
+ _module().da2_segb(spay, tb, segb)
1802
+ return segb