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,1297 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """gx_gds_cuda: the read-side backward mover (GDS) of the deterministic backward as ONE sorted walk per tile, bitwise
3
+ equal to mover_da2_cuda's GDS.
4
+
5
+ What it computes, per chunk of one step, over Phase 1's sorted read records (qrec: {spay_q element, absolute slot,
6
+ scale = __fmul_rn(f32(q_val), expf(log_q)), 0}; one slot group = consecutive positions FIRST..LAST):
7
+
8
+ grad[slot] = round(gb + acc), acc = fmaf(sc_m, dy[t_m], acc) over the group's members in sorted order from 0,
9
+ gb = the grad row before this launch (one rounding: mover_da2_cuda's read-modify-write update);
10
+ d_q_w[t, r] = <dy[t], bank[slot]>: per lane the chain __fmaf_rn(x.w, m.w, __fmaf_rn(x.z, m.z, __fmaf_rn(x.y, m.y,
11
+ __fmul_rn(x.x, m.x)))) over its 4 columns, summed over the lanes in the xor-16, 8, 4, 2, 1 order
12
+ (mover_da2_cuda's 8-entry reduce-scatter); column slices of a wide head go to planes summed in slice
13
+ order at step 0.
14
+
15
+ These are mover_da2_cuda's formulas, so the outputs are bitwise its GDS's. What changes is the row supply; the walk
16
+ has no shared-memory rings and no waits:
17
+ * at tile start every valid group's bank row (needed from its FIRST) and grad row (needed at its LAST) of the first
18
+ two record batches is prefetched to L2 (prefetch.global.L2 per 128-byte line, the LSU path: no TMA engine
19
+ serialization), then the rows of batch bi + 1 at batch bi;
20
+ * the rows enter registers 8 positions ahead: position j of a sub-batch is refilled with position j of the next
21
+ sub-batch right after it is consumed (16 row registers per lane);
22
+ * per-entry dots are computed from the group's bank row held in registers (one bank row load per GROUP instead of
23
+ one per read entry), so there is one role and one launch per step.
24
+ Tiles: Phase 1's tiles_q (L = 128, forward-snapped) or mover_da2_cuda's build_tiles; every group lies in one tile, so
25
+ outputs do not depend on the tiling, the warps per CTA or the prefetch distance. Records a provider built at factory time
26
+ are read as tensor records (the step's GDS may then launch with PDL in captured backwards too).
27
+ Scope: bf16 bank with bf16 or f32 grad rows, f32 bank with f32 grad rows; every row type but bf16 / bf16, and bf16 banks
28
+ of >= 2^32 elements, take the 64-bit-offset instantiations. Shapes outside admits() take mover_da2_cuda's make_gds on the
29
+ modern backend; elsewhere the caller routes them (movers.gds_admits). On a100 / rtx (the portable side of sdm_copy.cuh)
30
+ the token rows are staged with cooperative 16-byte cp.async copies and a CTA barrier, without programmatic launch;
31
+ everything else is the same code, so the outputs are the modern kernel's, bit for bit."""
32
+
33
+ import functools
34
+
35
+ import torch
36
+
37
+ from ._build import load_cuda_extension
38
+ from ._device import headers, num_sms, prev_pow2
39
+ # device-class reads memoized per device ordinal and forced row (values = _device's)
40
+ from ._devmemo import backend, capability, cuda_backend, pdl_ok as device_pdl_ok
41
+ from . import mover_da2_cuda as da2
42
+
43
+ # Device helpers of the movers' row types (Row4, cvt4, st4 / st_raw, zero rows, row_off / rec_val / rec_row), shared by
44
+ # gx_gds_cuda, gx_pa2_cuda and mover_fx_cuda (inserted into each source).
45
+ ROW_HELPERS = r"""
46
+ template <typename E> struct Row4;
47
+ template <> struct Row4<__nv_bfloat16> { using T = uint2; }; // 4 bf16
48
+ template <> struct Row4<float> { using T = float4; }; // 4 f32
49
+ __device__ __forceinline__ float4 cvt4(uint2 v) {
50
+ return make_float4(__uint_as_float(v.x << 16), __uint_as_float(v.x & 0xFFFF0000u),
51
+ __uint_as_float(v.y << 16), __uint_as_float(v.y & 0xFFFF0000u));
52
+ }
53
+ __device__ __forceinline__ float4 cvt4(float4 v) { return v; }
54
+ __device__ __forceinline__ uint32_t pack_bf2(float a, float b) {
55
+ const __nv_bfloat162 h = __floats2bfloat162_rn(a, b);
56
+ return *reinterpret_cast<const uint32_t*>(&h);
57
+ }
58
+ __device__ __forceinline__ void st4(__nv_bfloat16* p, float4 v) {
59
+ *reinterpret_cast<uint2*>(p) = make_uint2(pack_bf2(v.x, v.y), pack_bf2(v.z, v.w));
60
+ }
61
+ __device__ __forceinline__ void st4(float* p, float4 v) { *reinterpret_cast<float4*>(p) = v; }
62
+ template <typename E> __device__ __forceinline__ void st_raw(E* p, typename Row4<E>::T v) {
63
+ *reinterpret_cast<typename Row4<E>::T*>(p) = v;
64
+ }
65
+ template <typename T> __device__ __forceinline__ T make_zero_row();
66
+ template <> __device__ __forceinline__ uint2 make_zero_row<uint2>() { return make_uint2(0u, 0u); }
67
+ template <> __device__ __forceinline__ float4 make_zero_row<float4>() { return make_float4(0.f, 0.f, 0.f, 0.f); }
68
+ template <bool W64> __device__ __forceinline__ auto row_off(uint32_t row, int hd) {
69
+ if constexpr (W64) return static_cast<int64_t>(row) * hd; else return row * static_cast<uint32_t>(hd);
70
+ }
71
+ template <bool W64> __device__ __forceinline__ uint32_t rec_val(uint32_t slot, int hd) {
72
+ if constexpr (W64) return slot; else return slot * static_cast<uint32_t>(hd);
73
+ }
74
+ template <bool W64> __device__ __forceinline__ uint64_t rec_row(uint32_t v, int hd) {
75
+ if constexpr (W64) return static_cast<uint64_t>(v) * static_cast<uint32_t>(hd); else return v;
76
+ }
77
+ // Quantized snapshot rows: QUANT 1 = fp8 e4m3 bytes, 2 = int4 signed nibbles (element 2i in the high
78
+ // nibble of byte i), one f32 scale per row; decoded x' = E(f32(q) * f32(E(scale))) (phase2_kernels._snapshot_tile).
79
+ // Integer decode only (no fp8 intrinsics): the same code on every architecture.
80
+ __device__ __forceinline__ float e4m3_f32(uint32_t b) {
81
+ const uint32_t e = (b >> 3) & 0xFu, m = b & 7u;
82
+ float v = e ? __uint_as_float(((e + 120u) << 23) | (m << 20)) : static_cast<float>(m) * 0x1p-9f;
83
+ if ((b & 0x7Fu) == 0x7Fu) v = __uint_as_float(0x7FC00000u); // S.1111.111: NaN (e4m3 has no infinity)
84
+ return (b & 0x80u) ? -v : v;
85
+ }
86
+ __device__ __forceinline__ float nib_f32(uint32_t n) { return static_cast<float>(static_cast<int>(n ^ 8u) - 8); }
87
+ // A lane's 4 columns of a quantized row: w = the payload word copied for it (fp8: its 4 bytes; int4: the 4-byte word
88
+ // of its lane pair, its 2 bytes at byte 2 * (lane & 1)); raw = x' in the bank dtype (what the backward restores).
89
+ template <int QUANT, typename E>
90
+ __device__ __forceinline__ float4 dec4(uint32_t w, int lane, float scale, typename Row4<E>::T& raw) {
91
+ float4 q;
92
+ if constexpr (QUANT == 1) {
93
+ q = make_float4(e4m3_f32(w & 0xFFu), e4m3_f32((w >> 8) & 0xFFu), e4m3_f32((w >> 16) & 0xFFu), e4m3_f32(w >> 24));
94
+ } else {
95
+ const uint32_t h = (lane & 1) ? (w >> 16) : (w & 0xFFFFu);
96
+ q = make_float4(nib_f32((h >> 4) & 0xFu), nib_f32(h & 0xFu), nib_f32((h >> 12) & 0xFu), nib_f32((h >> 8) & 0xFu));
97
+ }
98
+ if constexpr (std::is_same<E, __nv_bfloat16>::value) {
99
+ const float s = __bfloat162float(__float2bfloat16_rn(scale));
100
+ raw = make_uint2(pack_bf2(__fmul_rn(q.x, s), __fmul_rn(q.y, s)), pack_bf2(__fmul_rn(q.z, s), __fmul_rn(q.w, s)));
101
+ return cvt4(raw);
102
+ } else {
103
+ raw = make_float4(__fmul_rn(q.x, scale), __fmul_rn(q.y, scale), __fmul_rn(q.z, scale), __fmul_rn(q.w, scale));
104
+ return raw;
105
+ }
106
+ }
107
+ """
108
+
109
+ # fp8 / int4 snapshot rows (briefs/w3_interface.md), shared by the producer (mover_fx_cuda's quantized DW) and the
110
+ # consumer's decode (needs <cuda_fp8.h>). One row = the hd columns of one pre-write bank row, in f32; the arithmetic of
111
+ # 2.0.0's per-entry quantizers (da2_quant_kernel, phase2_kernels' _store_quantized): amax = fmaxf over |x| from +0 (NaN ignored, inf kept), scale = f32(bf16(fmaxf(amax * f32(1 / qmax), 1e-12))),
112
+ # q = bf16(x / scale) (IEEE division), then e4m3 RN satfinite (Q 1) or nibble = clamp(rint(q), -8, 7) & 0xF with element
113
+ # 2i in the high nibble of byte i (Q 2).
114
+ QUANT_HELPERS = r"""
115
+ __device__ __forceinline__ float q_amax4(float4 x) {
116
+ return fmaxf(fmaxf(fmaxf(fmaxf(0.f, fabsf(x.x)), fabsf(x.y)), fabsf(x.z)), fabsf(x.w));
117
+ }
118
+ // = q_amax4(cvt4(v)) for 4 bf16 values, on packed pairs (__hmax2: a NaN operand yields the other; both NaN: NaN, which
119
+ // the final fmaxf from +0 ignores)
120
+ __device__ __forceinline__ float q_amax_bf4(uint2 v) {
121
+ const __nv_bfloat162 m = __hmax2(__habs2(*reinterpret_cast<const __nv_bfloat162*>(&v.x)),
122
+ __habs2(*reinterpret_cast<const __nv_bfloat162*>(&v.y)));
123
+ return fmaxf(fmaxf(0.f, __low2float(m)), __high2float(m));
124
+ }
125
+ // max over the warp of non-negative, non-NaN floats (their bits order as unsigned integers)
126
+ __device__ __forceinline__ float q_warp_max(float a) {
127
+ return __uint_as_float(__reduce_max_sync(0xFFFFFFFFu, __float_as_uint(a)));
128
+ }
129
+ template <int Q> __device__ __forceinline__ float q_scale(float amax) {
130
+ const float qinv = (Q == 1) ? static_cast<float>(1.0 / 448.0) : static_cast<float>(1.0 / 7.0);
131
+ return __bfloat162float(__float2bfloat16_rn(fmaxf(amax * qinv, 1e-12f)));
132
+ }
133
+ // q = bf16(x / sc). EXACT: __fdiv_rn. Else (x a bf16 value of a row whose scale is sc; y = __frcp_rn(sc)): the product
134
+ // with one FMA correction, signed zeros kept; equal through the final byte / nibble to the EXACT form for every such
135
+ // (x, sc) (exhaustive proof: tests/test_quant_fx.py::test_quant_division_exhaustive).
136
+ template <bool EXACT> __device__ __forceinline__ float q_div(float x, float sc, float y) {
137
+ float q;
138
+ if constexpr (EXACT) {
139
+ q = __fdiv_rn(x, sc);
140
+ } else {
141
+ q = x * y;
142
+ if (q != 0.f) q = fmaf(fmaf(-sc, q, x), y, q);
143
+ }
144
+ return __bfloat162float(__float2bfloat16_rn(q));
145
+ }
146
+ __device__ __forceinline__ uint32_t q_nib(float q) {
147
+ return static_cast<uint32_t>(static_cast<int>(fmaxf(fminf(rintf(q), 7.f), -8.f))) & 0xFu;
148
+ }
149
+ // Columns col .. col + 3 (col % 4 == 0) of one quantized row qrow (hd bytes for fp8, hd / 2 for int4).
150
+ template <int Q, bool EXACT> __device__ __forceinline__ void q_store4(uint8_t* qrow, int64_t col, float4 x, float sc,
151
+ float y) {
152
+ const float a = q_div<EXACT>(x.x, sc, y), b = q_div<EXACT>(x.y, sc, y);
153
+ const float c = q_div<EXACT>(x.z, sc, y), d = q_div<EXACT>(x.w, sc, y);
154
+ if constexpr (Q == 1) { // e4m3 pairs: the low byte holds the first column
155
+ *reinterpret_cast<uint32_t*>(qrow + col) =
156
+ static_cast<uint32_t>(__nv_cvt_float2_to_fp8x2(make_float2(a, b), __NV_SATFINITE, __NV_E4M3)) |
157
+ (static_cast<uint32_t>(__nv_cvt_float2_to_fp8x2(make_float2(c, d), __NV_SATFINITE, __NV_E4M3)) << 16);
158
+ } else {
159
+ *reinterpret_cast<uint16_t*>(qrow + col / 2) =
160
+ static_cast<uint16_t>((q_nib(a) << 4) | q_nib(b) | (q_nib(c) << 12) | (q_nib(d) << 8));
161
+ }
162
+ }
163
+ """
164
+
165
+ _CPP = r"""
166
+ void gx_gds(at::Tensor bank, at::Tensor grad, at::Tensor gr, int64_t gr_step_rows, at::Tensor rec, at::Tensor spay,
167
+ at::Tensor tb, at::Tensor dots, int64_t ti, int64_t P, int64_t C, int64_t R, int64_t K, int64_t nw,
168
+ int64_t pdl);
169
+ void gx_gds_bf(at::Tensor bank, at::Tensor grad, at::Tensor gr, int64_t gr_step_rows, at::Tensor rec, at::Tensor spay,
170
+ at::Tensor tb, at::Tensor dots, int64_t ti, int64_t P, int64_t C, int64_t R, int64_t K, int64_t nw,
171
+ int64_t pdl);
172
+ at::Tensor gx_gds_plan(at::Tensor bank, at::Tensor grad, at::Tensor gr, int64_t gr_step_rows, at::Tensor rec,
173
+ at::Tensor spay, at::Tensor tb, at::Tensor dots, int64_t ti_max, int64_t P, int64_t C, int64_t R,
174
+ int64_t K, int64_t nw, bool kbf);
175
+ int64_t gx_gds_launch_addr();
176
+ """
177
+
178
+ _CUDA = r"""
179
+ #include <torch/extension.h>
180
+ #include <ATen/cuda/CUDAContext.h>
181
+ #include <c10/cuda/CUDAGuard.h>
182
+ #include <c10/cuda/CUDAException.h>
183
+ #include <cuda_bf16.h>
184
+ #include <cstdint>
185
+ #include <map>
186
+ #include <mutex>
187
+ #include <utility>
188
+ #include "sdm_copy.cuh"
189
+ #include "sdm_plancap.cuh"
190
+
191
+ namespace {
192
+
193
+ constexpr uint32_t kFirst = 0x80000000u;
194
+ constexpr uint32_t kLast = 0x40000000u;
195
+ constexpr uint32_t kInvalid = 0x20000u;
196
+ constexpr unsigned kFull = 0xFFFFFFFFu;
197
+
198
+ #define GX_MINB 2 // launch-bounds min resident CTAs per SM
199
+ // Row loads of data the previous kernel writes (bank / grad rows): coherent L2 loads (ld.global.cg), so
200
+ // nothing depends on the non-coherent read-only path across the programmatic edge.
201
+ #define GX_LD(T, p) __ldcg(reinterpret_cast<const T*>(p))
202
+
203
+ __device__ __forceinline__ uint32_t smem_u32(const void* p) {
204
+ return static_cast<uint32_t>(__cvta_generic_to_shared(p));
205
+ }
206
+ // The portable side (SDM_PORTABLE_ARCH, sdm_copy.cuh: sm_80 / 86 / 89 / 120 ...) stages the token rows with sdmcp's
207
+ // cooperative cp.async copies and publishes them with a CTA barrier (stage_tok_rows_spread, mbar_wait_dep): no barrier
208
+ // object, no griddepcontrol (GX_SM90_OR_NONE: nothing; the host never launches this kernel programmatically there) and
209
+ // no bulk copy (GX_SM90_ONLY traps there: never reached).
210
+ #ifdef SDM_PORTABLE_ARCH
211
+ #define GX_PRE_SM90 1
212
+ #define GX_SM90_ONLY(...) __trap()
213
+ #define GX_SM90_OR_NONE(...)
214
+ #else
215
+ #define GX_SM90_ONLY(...) __VA_ARGS__
216
+ #define GX_SM90_OR_NONE(...) __VA_ARGS__
217
+ #endif
218
+ __device__ __forceinline__ void mbar_init(uint64_t* bar, uint32_t count) {
219
+ GX_SM90_OR_NONE(asm volatile("mbarrier.init.shared::cta.b64 [%0], %1;\n" ::"r"(smem_u32(bar)), "r"(count)
220
+ : "memory"));
221
+ }
222
+ __device__ __forceinline__ void fence_mbar_init() {
223
+ GX_SM90_OR_NONE(asm volatile("fence.mbarrier_init.release.cluster;\n" ::: "memory"));
224
+ }
225
+ __device__ __forceinline__ void mbar_arrive_tx(uint64_t* bar, uint32_t bytes) {
226
+ GX_SM90_OR_NONE(asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;\n" ::"r"(smem_u32(bar)),
227
+ "r"(bytes) : "memory"));
228
+ }
229
+ __device__ __forceinline__ void bulk_g2s(void* dst, const void* src, uint32_t bytes, uint64_t* bar) {
230
+ GX_SM90_ONLY(asm volatile(
231
+ "cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes [%0], [%1], %2, [%3];\n"
232
+ ::"r"(smem_u32(dst)), "l"(src), "r"(bytes), "r"(smem_u32(bar)) : "memory"));
233
+ }
234
+ // Spins until the phase of parity `parity` of `bar` completed; returns 0. No memory clobber: the caller adds the
235
+ // result to the address of every read that must follow the wait (as in mover_da2_cuda). Portable side: every thread
236
+ // waits for its own token-row copies, then the CTA barrier publishes them (every thread of the CTA reaches both calls).
237
+ __device__ __forceinline__ uint32_t mbar_wait_dep(uint64_t* bar, uint32_t parity) {
238
+ #ifdef GX_PRE_SM90
239
+ (void)bar;
240
+ (void)parity;
241
+ sdmcp::publish_cta();
242
+ return 0u;
243
+ #else
244
+ uint32_t ok;
245
+ asm volatile(
246
+ "{\n"
247
+ " .reg .pred p;\n"
248
+ " GX_WAIT:\n"
249
+ " mbarrier.try_wait.parity.shared::cta.b64 p, [%1], %2;\n"
250
+ " @!p bra GX_WAIT;\n"
251
+ " mov.u32 %0, 0;\n"
252
+ "}\n"
253
+ : "=r"(ok) : "r"(smem_u32(bar)), "r"(parity));
254
+ return ok;
255
+ #endif
256
+ }
257
+ // L2 prefetch of [p, p + bytes): one prefetch per 128-byte line (a performance hint only, no semantic effect).
258
+ __device__ __forceinline__ void prefetch_l2(const void* p, int bytes) {
259
+ const uintptr_t a = reinterpret_cast<uintptr_t>(p);
260
+ const uintptr_t end = a + static_cast<uintptr_t>(bytes);
261
+ for (uintptr_t q = a & ~static_cast<uintptr_t>(127); q < end; q += 128) {
262
+ asm volatile("prefetch.global.L2 [%0];" ::"l"(q));
263
+ }
264
+ }
265
+
266
+ @ROW_HELPERS@
267
+
268
+ // Read records, two layouts with the same content (Phase 1's records or px_qrec_cuda's per-step builder):
269
+ // REC 0: qrec uint4 {spay_q element, absolute slot, scale bits, 0} per sorted read position;
270
+ // REC 1: rec8 uint2 {absolute slot, scale bits} + the spay_q element (int32) of the same position.
271
+ // Both are returned as {pay, slot, scale, 0}; past the tile end: a padding group of its own.
272
+ template <int REC> struct RecSrc;
273
+ template <> struct RecSrc<0> {
274
+ const uint4* r;
275
+ const int32_t* pay; // unused
276
+ __device__ __forceinline__ uint4 ld(int pos, int e) const {
277
+ if (pos < e) return __ldg(r + pos);
278
+ return make_uint4(kFirst | kLast | kInvalid, 0u, 0u, 0u);
279
+ }
280
+ };
281
+ template <> struct RecSrc<1> {
282
+ const uint2* r;
283
+ const int32_t* pay;
284
+ __device__ __forceinline__ uint4 ld(int pos, int e) const {
285
+ if (pos < e) {
286
+ const uint2 v = __ldg(r + pos);
287
+ return make_uint4(static_cast<uint32_t>(__ldg(pay + pos)), v.x, v.y, 0u);
288
+ }
289
+ return make_uint4(kFirst | kLast | kInvalid, 0u, 0u, 0u);
290
+ }
291
+ };
292
+
293
+ // The per-row copies dealt round-robin over the warps (row t by warp t % NW): every warp issues ~C / NW of them
294
+ // (each bulk copy is a uniform-datapath instruction, serialized over the active lanes of a warp).
295
+ // Portable side: every thread of the CTA copies its share of the C rows (SW floats each, row stride hd: 16-byte
296
+ // pieces, aligned by the host checks) with cp.async and commits its group; mbar_wait_dep publishes them.
297
+ __device__ __forceinline__ void stage_tok_rows_spread(float* tok_s, const float* rows, int C, int SW, int hd,
298
+ uint64_t* tok_bar, int warp, int lane, int nw) {
299
+ #ifdef GX_PRE_SM90
300
+ (void)tok_bar;
301
+ (void)warp;
302
+ (void)lane;
303
+ sdmcp::copy_rows16(tok_s, SW * 4, rows, static_cast<int64_t>(hd) * 4, C, SW * 4, static_cast<int>(threadIdx.x),
304
+ nw * 32);
305
+ sdmcp::commit();
306
+ #else
307
+ if (threadIdx.x == 0) mbar_arrive_tx(tok_bar, static_cast<uint32_t>(C * SW * 4));
308
+ if (SW == hd) {
309
+ if (threadIdx.x == 0) bulk_g2s(tok_s, rows, static_cast<uint32_t>(C * SW * 4), tok_bar);
310
+ } else {
311
+ for (int t = warp + nw * lane; t < C; t += nw * 32) {
312
+ bulk_g2s(tok_s + t * SW, rows + static_cast<int64_t>(t) * hd, static_cast<uint32_t>(SW * 4), tok_bar);
313
+ }
314
+ }
315
+ #endif
316
+ }
317
+
318
+ // Tails (dc_pa3's slices, the last one narrower): the slice's wl valid columns of each token row (wl % 4 == 0),
319
+ // into rows of SW floats (the columns past wl are never read: lanes past the slice read lane 0's columns).
320
+ // Portable side: sdmcp's cooperative 16-byte cp.async copies of the wl valid columns (wl % 4 == 0: 16-byte pieces at
321
+ // 16-byte aligned row starts, hd % 4 == 0), committed per thread and published by mbar_wait_dep, as the regular staging.
322
+ __device__ __forceinline__ void stage_tok_rows_tail(float* tok_s, const float* rows, int C, int SW, int wl, int hd,
323
+ uint64_t* tok_bar, int warp, int lane, int nw) {
324
+ #ifdef GX_PRE_SM90
325
+ (void)tok_bar;
326
+ (void)warp;
327
+ (void)lane;
328
+ sdmcp::copy_rows16(tok_s, SW * 4, rows, static_cast<int64_t>(hd) * 4, C, wl * 4, static_cast<int>(threadIdx.x),
329
+ nw * 32);
330
+ sdmcp::commit();
331
+ #else
332
+ if (threadIdx.x == 0) mbar_arrive_tx(tok_bar, static_cast<uint32_t>(C * wl * 4));
333
+ for (int t = warp + nw * lane; t < C; t += nw * 32) {
334
+ bulk_g2s(tok_s + t * SW, rows + static_cast<int64_t>(t) * hd, static_cast<uint32_t>(wl * 4), tok_bar);
335
+ }
336
+ #endif
337
+ }
338
+
339
+ // Tails with hd % 4 != 0 (MODE bit 16; rows aligned to 4 bytes only): the slice's wl valid columns of each token row
340
+ // by plain loads, the rest of the row's SW columns zeroed (a lane's last column group may be partial), one arrival.
341
+ __device__ __forceinline__ void stage_tok_rows_elem(float* tok_s, const float* rows, int C, int SW, int wl, int hd,
342
+ uint64_t* tok_bar) {
343
+ for (int i = threadIdx.x; i < C * SW; i += blockDim.x) {
344
+ const int t = i / SW, c = i - t * SW;
345
+ tok_s[i] = c < wl ? __ldg(rows + static_cast<int64_t>(t) * hd + c) : 0.f;
346
+ }
347
+ __syncthreads();
348
+ if (threadIdx.x == 0) mbar_arrive_tx(tok_bar, 0u);
349
+ }
350
+ // MODE bit 16: a lane's 4 row columns as elements, the first nv of them (zeros past them); stores of the first nv.
351
+ __device__ __forceinline__ uint2 ld_elems_row(const __nv_bfloat16* p, int nv) {
352
+ const unsigned short* q = reinterpret_cast<const unsigned short*>(p);
353
+ uint32_t w0 = 0u, w1 = 0u;
354
+ if (nv > 0) w0 = static_cast<uint32_t>(__ldcg(q));
355
+ if (nv > 1) w0 |= static_cast<uint32_t>(__ldcg(q + 1)) << 16;
356
+ if (nv > 2) w1 = static_cast<uint32_t>(__ldcg(q + 2));
357
+ if (nv > 3) w1 |= static_cast<uint32_t>(__ldcg(q + 3)) << 16;
358
+ return make_uint2(w0, w1);
359
+ }
360
+ __device__ __forceinline__ float4 ld_elems_row(const float* p, int nv) {
361
+ return make_float4(nv > 0 ? __ldcg(p) : 0.f, nv > 1 ? __ldcg(p + 1) : 0.f, nv > 2 ? __ldcg(p + 2) : 0.f,
362
+ nv > 3 ? __ldcg(p + 3) : 0.f);
363
+ }
364
+ __device__ __forceinline__ void st_elems_row(__nv_bfloat16* p, float4 v, int nv) { // st4's rounding per element
365
+ if (nv > 0) p[0] = __float2bfloat16_rn(v.x);
366
+ if (nv > 1) p[1] = __float2bfloat16_rn(v.y);
367
+ if (nv > 2) p[2] = __float2bfloat16_rn(v.z);
368
+ if (nv > 3) p[3] = __float2bfloat16_rn(v.w);
369
+ }
370
+ __device__ __forceinline__ void st_elems_row(float* p, float4 v, int nv) {
371
+ if (nv > 0) p[0] = v.x;
372
+ if (nv > 1) p[1] = v.y;
373
+ if (nv > 2) p[2] = v.z;
374
+ if (nv > 3) p[3] = v.w;
375
+ }
376
+
377
+ struct Geo {
378
+ int P, C, n, hd, NSL, SW, K, TU, tok_bytes;
379
+ };
380
+
381
+ // Grid: P * NSL * K CTAs of NW warps (CTA b: cr = b % K, slice = b / K % NSL, p = rest); warp w walks tile cr * NW + w
382
+ // of chunk ti * P + p. Shared memory: token rows [C][SW] f32 | per-warp record batches [NW][2][32] uint4 | tok_bar.
383
+ // Min resident CTAs per SM for the launch bounds: GX_MINB CTAs of 8 warps, i.e. 8 * GX_MINB warps per SM whatever NW.
384
+ // Clamped to 1 .. 32 / nw CTAs (<= 1024 threads per SM keeps >= 64 registers per thread: the row pipeline's 32 alone).
385
+ constexpr int gx_minb(int nw) {
386
+ return (8 * GX_MINB) / nw < 1 ? 1 : ((8 * GX_MINB) / nw > 32 / nw ? 32 / nw : (8 * GX_MINB) / nw);
387
+ }
388
+
389
+ // rec: the records (REC layout) of chunks rec_chunk0 .. (all chunks: 0; one step's P chunks: ti * P), spay: spay_q of
390
+ // all chunks (REC 1 only).
391
+ // MODE = role | 4 * W64 | 8 * TAIL. Role 0 = the full walk (grad RMW + dots), the only role compiled; kDot / kRmw keep
392
+ // the two roles' code apart. W64 (MODE bit 4): 64-bit row element offsets (banks of >= 2^32 elements, and every
393
+ // float32 row type): the record batch keeps the slot and each row access forms uint64(slot) * uint32(hd),
394
+ // mover_da2_cuda's GDS arithmetic; without it (bf16 bank + bf16 grad) the batch keeps the 32-bit slot * hd. TAIL
395
+ // (MODE bit 8): dc_pa3's slices of SW = 4 ceil(ceil(hd / 4) / NSL) columns, the last one narrower (uneven
396
+ // slices); the same arithmetic per column and lane. ELEM (MODE bit 16, with 8; hd % 4 != 0): rows aligned to their
397
+ // element only, so token rows, row loads and grad stores go by elements (zeros past the head's last column).
398
+ template <typename E, typename G, int NW, int REC, int MODE>
399
+ __global__ void __launch_bounds__(NW * 32, gx_minb(NW)) gx_gds_kernel(
400
+ const E* __restrict__ bank, G* __restrict__ grad, const float* __restrict__ gr, int64_t gr_step_rows,
401
+ const void* __restrict__ rec, const int32_t* __restrict__ spay, int64_t rec_chunk0,
402
+ const int32_t* __restrict__ tb, float* __restrict__ dots, int64_t plane, int ti, Geo g, int R) {
403
+ static_assert(MODE >= 0 && (MODE & ~24) <= 6 && (MODE & 3) <= 2 && ((MODE & 16) == 0 || (MODE & 8) != 0),
404
+ "MODE: role (0 full, 1 grad RMW only, 2 dots only) | 4 (64-bit row offsets) | 8 (tail slices) | 16 "
405
+ "(element pieces, with 8)");
406
+ constexpr bool kW64 = (MODE & 4) != 0;
407
+ constexpr bool kTail = (MODE & 8) != 0;
408
+ constexpr bool kElem = (MODE & 16) != 0;
409
+ static_assert(kW64 || (std::is_same<E, __nv_bfloat16>::value && std::is_same<G, __nv_bfloat16>::value),
410
+ "float32 rows use 64-bit row offsets");
411
+ constexpr bool kDot = (MODE & 3) != 1; // bank rows + d_q_w dots
412
+ constexpr bool kRmw = (MODE & 3) != 2; // grad rows + the single-writer read-modify-write
413
+ using RowT = typename Row4<E>::T;
414
+ using RowG = typename Row4<G>::T;
415
+ extern __shared__ __align__(128) unsigned char smem[];
416
+ const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5;
417
+ unsigned ub = blockIdx.x; // unsigned index math: blocks < 2^31 and K, NSL >= 1 (host checks), same values
418
+ const int cr = static_cast<int>(ub % static_cast<unsigned>(g.K));
419
+ ub /= static_cast<unsigned>(g.K);
420
+ const int sl = static_cast<int>(ub % static_cast<unsigned>(g.NSL));
421
+ const int p = static_cast<int>(ub / static_cast<unsigned>(g.NSL));
422
+ const int64_t chunk = static_cast<int64_t>(ti) * g.P + p;
423
+ const int SW = g.SW, C = g.C, hd = g.hd;
424
+ float* tok_s = reinterpret_cast<float*>(smem);
425
+ uint4* rec_s = reinterpret_cast<uint4*>(smem + g.tok_bytes) + warp * 64; // [2][32]: current | next batch
426
+ uint64_t* tok_bar = reinterpret_cast<uint64_t*>(smem + g.tok_bytes + NW * 64 * sizeof(uint4));
427
+ const int32_t* tbc = tb + chunk * (g.TU + 1);
428
+ const int tile = cr * NW + warp;
429
+ const int s = __ldg(tbc + tile), e = __ldg(tbc + tile + 1); // read-only input: in flight across the barrier
430
+ if (threadIdx.x == 0) mbar_init(tok_bar, 1);
431
+ fence_mbar_init();
432
+ __syncthreads();
433
+ if constexpr (kElem) {
434
+ stage_tok_rows_elem(tok_s, gr + (static_cast<int64_t>(ti) * gr_step_rows + static_cast<int64_t>(p) * C) * hd +
435
+ static_cast<int64_t>(sl) * SW, C, SW, min(SW, hd - sl * SW), hd, tok_bar);
436
+ } else if constexpr (kTail) {
437
+ stage_tok_rows_tail(tok_s, gr + (static_cast<int64_t>(ti) * gr_step_rows + static_cast<int64_t>(p) * C) * hd +
438
+ static_cast<int64_t>(sl) * SW, C, SW, min(SW, hd - sl * SW), hd, tok_bar, warp,
439
+ lane, NW);
440
+ } else {
441
+ stage_tok_rows_spread(tok_s, gr + (static_cast<int64_t>(ti) * gr_step_rows + static_cast<int64_t>(p) * C) * hd +
442
+ static_cast<int64_t>(sl) * SW, C, SW, hd, tok_bar, warp, lane, NW);
443
+ }
444
+ RecSrc<REC> kr;
445
+ if constexpr (REC == 0) {
446
+ kr.r = reinterpret_cast<const uint4*>(rec) + (chunk - rec_chunk0) * g.n;
447
+ kr.pay = nullptr;
448
+ } else {
449
+ kr.r = reinterpret_cast<const uint2*>(rec) + (chunk - rec_chunk0) * g.n;
450
+ kr.pay = spay + chunk * g.n;
451
+ }
452
+ const int c4 = 4 * lane;
453
+ // Lanes past the slice's columns (hd 120 / 960: lanes 30, 31; hd 64: lanes 16-31) mirror lane 0's columns for
454
+ // their row LOADS (same addresses as lane 0's loads in the same instruction: unguarded, no extra traffic); their
455
+ // grad stores stay predicated off by `on` (one writer per address) and their dot partials are zeroed.
456
+ const bool on = c4 < SW && sl * SW + c4 < hd;
457
+ const int coff = on ? c4 : 0;
458
+ // ELEM: valid columns at the lane's address (off lanes mirror lane 0's; the head's last group may be partial)
459
+ const int nvl = kElem ? min(4, hd - (sl * SW + coff)) : 4;
460
+ const E* const bank_col = bank + static_cast<int64_t>(sl) * SW + coff;
461
+ G* const grad_col = grad + static_cast<int64_t>(sl) * SW + coff;
462
+ float* const dots_sl = dots + static_cast<int64_t>(sl) * plane + chunk * (static_cast<int64_t>(C) * R);
463
+
464
+ // Records: batch bi + 1 .. bi + 3 in registers (lane = position), batch bi and bi + 1 staged in rec_s.
465
+ uint4 r0 = kr.ld(s + lane, e);
466
+ uint4 r1 = kr.ld(s + 32 + lane, e);
467
+ uint4 r2 = kr.ld(s + 64 + lane, e);
468
+ uint4 r3 = kr.ld(s + 96 + lane, e);
469
+ // The lane holding a valid FIRST prefetches its group's bank row slice, the lane holding a valid LAST its grad row
470
+ // slice (each slot has one group per chunk and one writer per launch, so reading it early is safe). TAIL: the slice's
471
+ // valid columns only (the last slice is narrower; a row slice never reaches past its row).
472
+ int pfw = SW;
473
+ if constexpr (kTail) pfw = min(SW, hd - sl * SW);
474
+ auto pf_b = [&](const uint4& r) {
475
+ if (kDot && (r.x & (kFirst | kInvalid)) == kFirst) {
476
+ prefetch_l2(bank + static_cast<uint64_t>(r.y) * static_cast<uint32_t>(hd) + static_cast<int64_t>(sl) * SW,
477
+ pfw * static_cast<int>(sizeof(E)));
478
+ }
479
+ };
480
+ auto pf_g = [&](const uint4& r) {
481
+ if (kRmw && (r.x & (kLast | kInvalid)) == kLast) {
482
+ prefetch_l2(grad + static_cast<uint64_t>(r.y) * static_cast<uint32_t>(hd) + static_cast<int64_t>(sl) * SW,
483
+ pfw * static_cast<int>(sizeof(G)));
484
+ }
485
+ };
486
+ // rec_s entry: {token row byte offset in the slice's token rows, scale bits, row element offset slot * hd (bank and
487
+ // grad rows share it; < 2^32, checked on the host) or with W64 the slot, d_q_w index (token * R + lane; bit 31 =
488
+ // invalid)}.
489
+ auto stage = [&](int buf, const uint4& r) {
490
+ const uint32_t px = r.x;
491
+ const uint32_t tk = (px >> 9) & 0xFFu;
492
+ const uint32_t oidx = (tk * static_cast<uint32_t>(R) + (px & 0xFFu)) | ((px & kInvalid) ? 0x80000000u : 0u);
493
+ rec_s[buf * 32 + lane] = make_uint4(static_cast<uint32_t>(min(static_cast<int>(tk), C - 1) * SW) * 4u, r.z,
494
+ rec_val<kW64>(r.y, hd), oidx);
495
+ };
496
+ // Uniform masks of a batch: valid FIRSTs (bank row loads), any FIRST (group starts: acc and row reset; FIRST(j) is
497
+ // exactly LAST(j - 1) or the tile start, so this equals mover_da2_cuda's reset after every LAST), valid LASTs (grad
498
+ // updates).
499
+ auto mask_vf = [&](const uint4& r) { return __ballot_sync(kFull, (r.x & (kFirst | kInvalid)) == kFirst); };
500
+ auto mask_af = [&](const uint4& r) { return __ballot_sync(kFull, (r.x & kFirst) != 0u); };
501
+ auto mask_vl = [&](const uint4& r) { return __ballot_sync(kFull, (r.x & (kLast | kInvalid)) == kLast); };
502
+ // valid LASTs whose grad row is loaded (= vl; kept separate from vl for the refill masks)
503
+ auto mask_vg = [&](const uint4& r) { return __ballot_sync(kFull, (r.x & (kLast | kInvalid)) == kLast); };
504
+
505
+ stage(0, r0);
506
+ uint32_t vf0 = mask_vf(r0), af0 = mask_af(r0), vl0 = mask_vl(r0), vg0 = mask_vg(r0);
507
+ __syncwarp();
508
+ // Everything above reads data written two or more launches back (tile bounds, records, grad_readings' token rows:
509
+ // inputs of the backward; the host launches with PDL only for tensor records and f32 readings). L2 hints for the
510
+ // first batches may run ahead of the previous kernel (prefetch.global.L2 is a hint; L2 is the point of coherence).
511
+ {
512
+ pf_b(r0);
513
+ pf_g(r0);
514
+ pf_b(r1);
515
+ pf_g(r1);
516
+ }
517
+ GX_SM90_OR_NONE(asm volatile("griddepcontrol.wait;" ::: "memory")); // previous kernel complete + visible
518
+ GX_SM90_OR_NONE(asm volatile("griddepcontrol.launch_dependents;" ::: "memory")); // only after this kernel's own wait
519
+ // Row registers of the current sub-batch's positions: mb[j] = bank row if position j is a valid FIRST, gb[j] = grad
520
+ // row if it is a valid LAST (zeros otherwise). Filled here for the tile's first sub-batch.
521
+ RowT mb[8];
522
+ RowG gb[8];
523
+ #pragma unroll
524
+ for (int j = 0; j < 8; ++j) {
525
+ const uint64_t ro = rec_row<kW64>(rec_s[j].z, hd);
526
+ if constexpr (kElem) {
527
+ if constexpr (kDot) mb[j] = ((vf0 >> j) & 1u) ? ld_elems_row(bank_col + ro, nvl) : make_zero_row<RowT>();
528
+ if constexpr (kRmw) gb[j] = ((vg0 >> j) & 1u) ? ld_elems_row(grad_col + ro, nvl) : make_zero_row<RowG>();
529
+ } else {
530
+ if constexpr (kDot) mb[j] = ((vf0 >> j) & 1u) ? GX_LD(RowT, bank_col + ro) : make_zero_row<RowT>();
531
+ if constexpr (kRmw) gb[j] = ((vg0 >> j) & 1u) ? GX_LD(RowG, grad_col + ro) : make_zero_row<RowG>();
532
+ }
533
+ }
534
+ const uint32_t ztok = mbar_wait_dep(tok_bar, 0u);
535
+ const unsigned char* tok = reinterpret_cast<const unsigned char*>(tok_s + ztok + coff);
536
+ float4 acc = make_float4(0.f, 0.f, 0.f, 0.f);
537
+ float4 mrow = make_float4(0.f, 0.f, 0.f, 0.f); // the current group's bank row
538
+ const int nb = (e - s + 31) >> 5;
539
+ for (int bi = 0; bi < nb; ++bi) {
540
+ if (bi > 0) {
541
+ r1 = r2;
542
+ r2 = r3;
543
+ r3 = kr.ld(s + 32 * (bi + 3) + lane, e);
544
+ { // batch bi + 1
545
+ pf_b(r1);
546
+ pf_g(r1);
547
+ }
548
+ }
549
+ __syncwarp(); // every lane is done with buffer (bi + 1) & 1 (batch bi - 1)
550
+ stage((bi + 1) & 1, r1);
551
+ const uint32_t vf1 = mask_vf(r1), af1 = mask_af(r1), vl1 = mask_vl(r1), vg1 = mask_vg(r1);
552
+ __syncwarp();
553
+ const uint4* rc = rec_s + (bi & 1) * 32;
554
+ const uint4* rn = rec_s + ((bi + 1) & 1) * 32;
555
+ const int pos0 = s + 32 * bi;
556
+ const int kend = min(32, e - pos0);
557
+ #pragma unroll 1
558
+ for (int k8 = 0; k8 < kend; k8 += 8) {
559
+ // The next sub-batch: the rest of this batch, else the next batch's first 8 positions, else none.
560
+ const uint4* rq_n = rc;
561
+ uint32_t fn8 = 0u, ln8 = 0u;
562
+ if (k8 + 8 < kend) { // ln8: grad rows to load (valid LASTs)
563
+ rq_n = rc + k8 + 8;
564
+ fn8 = vf0 >> (k8 + 8);
565
+ ln8 = vg0 >> (k8 + 8);
566
+ } else if (bi + 1 < nb) {
567
+ rq_n = rn;
568
+ fn8 = vf1;
569
+ ln8 = vg1;
570
+ }
571
+ const uint4* rq = rc + k8;
572
+ const uint32_t a8 = af0 >> k8, l8 = vl0 >> k8;
573
+ float pd[8];
574
+ // Refill position j with the next sub-batch's rows (8 positions ahead): predicated loads, no branch.
575
+ // (A macro, not a lambda: mb / gb must stay register arrays indexed by unrolled constants.)
576
+ #define GX_REFILL(j) \
577
+ { \
578
+ const uint64_t no = rec_row<kW64>(rq_n[j].z, hd); \
579
+ if constexpr (kElem) { \
580
+ if constexpr (kDot) mb[j] = ((fn8 >> (j)) & 1u) ? ld_elems_row(bank_col + no, nvl) : make_zero_row<RowT>(); \
581
+ if constexpr (kRmw) gb[j] = ((ln8 >> (j)) & 1u) ? ld_elems_row(grad_col + no, nvl) : make_zero_row<RowG>(); \
582
+ } else { \
583
+ if constexpr (kDot) mb[j] = ((fn8 >> (j)) & 1u) ? GX_LD(RowT, bank_col + no) : make_zero_row<RowT>(); \
584
+ if constexpr (kRmw) gb[j] = ((ln8 >> (j)) & 1u) ? GX_LD(RowG, grad_col + no) : make_zero_row<RowG>(); \
585
+ } \
586
+ }
587
+ if ((a8 & l8 & 0xFFu) == 0xFFu) {
588
+ // Every position starts a group (any FIRST) and ends it as a valid LAST: exactly the general body
589
+ // below with both branches taken (mrow = cvt4(mb[j]), acc = +0, then the same fmaf / dot / sum /
590
+ // store), written without branches; acc and mrow end as position 7's, as there.
591
+ #pragma unroll
592
+ for (int j = 0; j < 8; ++j) {
593
+ const uint4 rv = rq[j];
594
+ const float4 x = *reinterpret_cast<const float4*>(tok + rv.x); // rv.x: byte offset
595
+ const float4 m = cvt4(mb[j]);
596
+ const float sc = __uint_as_float(rv.y);
597
+ float4 a;
598
+ a.x = fmaf(sc, x.x, 0.f);
599
+ a.y = fmaf(sc, x.y, 0.f);
600
+ a.z = fmaf(sc, x.z, 0.f);
601
+ a.w = fmaf(sc, x.w, 0.f);
602
+ pd[j] = on ? __fmaf_rn(x.w, m.w, __fmaf_rn(x.z, m.z, __fmaf_rn(x.y, m.y, __fmul_rn(x.x, m.x))))
603
+ : 0.f;
604
+ const float4 gv = cvt4(gb[j]);
605
+ float4 o;
606
+ o.x = gv.x + a.x;
607
+ o.y = gv.y + a.y;
608
+ o.z = gv.z + a.z;
609
+ o.w = gv.w + a.w;
610
+ if constexpr (kElem) {
611
+ if (on) st_elems_row(grad_col + rec_row<kW64>(rv.z, hd), o, nvl);
612
+ } else {
613
+ if (on) st4(grad_col + rec_row<kW64>(rv.z, hd), o); // off lanes: no store (they mirror lane 0's address)
614
+ }
615
+ GX_REFILL(j)
616
+ if (j == 7) {
617
+ mrow = m;
618
+ acc = a;
619
+ }
620
+ }
621
+ } else
622
+ {
623
+ #pragma unroll
624
+ for (int j = 0; j < 8; ++j) {
625
+ const uint4 rr = rq[j]; // {x, y, z, w} in one read
626
+ const float4 x = *reinterpret_cast<const float4*>(tok + rr.x); // rr.x: byte offset
627
+ if ((a8 >> j) & 1u) {
628
+ mrow = cvt4(mb[j]);
629
+ acc = make_float4(0.f, 0.f, 0.f, 0.f);
630
+ }
631
+ const float sc = __uint_as_float(rr.y);
632
+ acc.x = fmaf(sc, x.x, acc.x);
633
+ acc.y = fmaf(sc, x.y, acc.y);
634
+ acc.z = fmaf(sc, x.z, acc.z);
635
+ acc.w = fmaf(sc, x.w, acc.w);
636
+ pd[j] = on ? __fmaf_rn(x.w, mrow.w, __fmaf_rn(x.z, mrow.z, __fmaf_rn(x.y, mrow.y,
637
+ __fmul_rn(x.x, mrow.x))))
638
+ : 0.f;
639
+ if ((l8 >> j) & 1u) {
640
+ const float4 gv = cvt4(gb[j]);
641
+ float4 o;
642
+ o.x = gv.x + acc.x;
643
+ o.y = gv.y + acc.y;
644
+ o.z = gv.z + acc.z;
645
+ o.w = gv.w + acc.w;
646
+ if constexpr (kElem) {
647
+ if (on) st_elems_row(grad_col + rec_row<kW64>(rr.z, hd), o, nvl);
648
+ } else {
649
+ if (on) st4(grad_col + rec_row<kW64>(rr.z, hd), o); // off lanes: no store (they mirror lane 0's address)
650
+ }
651
+ }
652
+ GX_REFILL(j)
653
+ }
654
+ }
655
+ #undef GX_REFILL
656
+ // Reduce-scatter of the 8 partial dots: lane L ends with entry (L >> 2) & 7 summed over all lanes, in the
657
+ // xor-16, 8, 4, 2, 1 order of a butterfly (mover_da2_cuda's tree, so d_q_w matches it bit for bit).
658
+ if constexpr (kDot) {
659
+ const bool h4 = (lane >> 4) & 1, h3 = (lane >> 3) & 1, h2 = (lane >> 2) & 1;
660
+ float a4[4];
661
+ #pragma unroll
662
+ for (int i = 0; i < 4; ++i) {
663
+ const float keep = h4 ? pd[i + 4] : pd[i];
664
+ const float send = h4 ? pd[i] : pd[i + 4];
665
+ a4[i] = keep + __shfl_xor_sync(kFull, send, 16);
666
+ }
667
+ float a2[2];
668
+ #pragma unroll
669
+ for (int i = 0; i < 2; ++i) {
670
+ const float keep = h3 ? a4[i + 2] : a4[i];
671
+ const float send = h3 ? a4[i] : a4[i + 2];
672
+ a2[i] = keep + __shfl_xor_sync(kFull, send, 8);
673
+ }
674
+ float a1;
675
+ {
676
+ const float keep = h2 ? a2[1] : a2[0];
677
+ const float send = h2 ? a2[0] : a2[1];
678
+ a1 = keep + __shfl_xor_sync(kFull, send, 4);
679
+ }
680
+ a1 += __shfl_xor_sync(kFull, a1, 2);
681
+ a1 += __shfl_xor_sync(kFull, a1, 1);
682
+ const int kk = k8 + ((lane >> 2) & 7);
683
+ if ((lane & 3) == 0 && pos0 + kk < e) {
684
+ const uint32_t w = rc[kk].w;
685
+ dots_sl[w & 0x7FFFFFFFu] = (w & 0x80000000u) ? 0.f : a1;
686
+ }
687
+ }
688
+ }
689
+ vf0 = vf1;
690
+ af0 = af1;
691
+ vl0 = vl1;
692
+ vg0 = vg1;
693
+ }
694
+ (void)mbar_wait_dep(tok_bar, 0u); // no CTA exits with its token-row copy in flight
695
+ }
696
+
697
+ template <typename K>
698
+ void set_smem(K kernel, size_t bytes) { // cudaFuncSetAttribute is per device: key on (kernel, device)
699
+ static std::mutex mu;
700
+ static std::map<std::pair<const void*, int>, size_t> done;
701
+ int dev = 0;
702
+ C10_CUDA_CHECK(cudaGetDevice(&dev));
703
+ std::lock_guard<std::mutex> lk(mu);
704
+ const auto key = std::make_pair(reinterpret_cast<const void*>(kernel), dev);
705
+ auto it = done.find(key);
706
+ if (it != done.end() && it->second >= bytes) return;
707
+ C10_CUDA_CHECK(cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize,
708
+ static_cast<int>(bytes)));
709
+ done[key] = bytes;
710
+ }
711
+
712
+ int slices_of(int hd) { return 1 + (hd - 1) / 128; }
713
+
714
+ // A plain launch, or (pdl) one with programmatic stream serialization (captured as a programmatic graph edge): the
715
+ // kernel may start while the previous kernel in the stream runs; its griddepcontrol.wait orders it.
716
+ template <typename K, typename... Args>
717
+ void gx_launch(K kernel, int blocks, int threads, size_t smem, cudaStream_t st, bool pdl, Args... args) {
718
+ if (sdm_plancap::take(kernel, dim3(blocks), dim3(threads), smem, args...)) return; // plan capture
719
+ if (pdl) {
720
+ cudaLaunchConfig_t cfg = {};
721
+ cfg.gridDim = dim3(blocks);
722
+ cfg.blockDim = dim3(threads);
723
+ cfg.dynamicSmemBytes = smem;
724
+ cfg.stream = st;
725
+ cudaLaunchAttribute attr[1];
726
+ attr[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
727
+ attr[0].val.programmaticStreamSerializationAllowed = 1;
728
+ cfg.attrs = attr;
729
+ cfg.numAttrs = 1;
730
+ C10_CUDA_CHECK(cudaLaunchKernelEx(&cfg, kernel, args...));
731
+ return;
732
+ }
733
+ kernel<<<blocks, threads, smem, st>>>(args...);
734
+ }
735
+
736
+ // KBF: the bf16 entry (gx_gds_bf, regular module only), picked once per factory call (make_gds) for bf16 bank + bf16
737
+ // grad below 2^32 elements: the row type and offset width are compile-time, so each step runs the plain host path (no
738
+ // row-type or offset-width selection per step). !KBF (gx_gds): every row type of the module. The same kernels,
739
+ // geometry and arguments for the rows both take.
740
+ template <bool KBF>
741
+ void gx_gds_run(const at::Tensor& bank, const at::Tensor& grad, const at::Tensor& gr, int64_t gr_step_rows,
742
+ const at::Tensor& rec, const at::Tensor& spay, const at::Tensor& tb, const at::Tensor& dots, int64_t ti,
743
+ int64_t P, int64_t C, int64_t R, int64_t K, int64_t nw, int64_t pdl) {
744
+ const at::cuda::CUDAGuard guard(bank.device());
745
+ // Row types: bf16 bank + bf16 grad (32-bit row offsets below 2^32 elements), bf16 bank + f32 grad (the
746
+ // grad_bank_fp32 option), f32 bank + f32 grad; every instantiation other than bf16 / bf16 / 32-bit offsets is a
747
+ // MODE 4 one (64-bit row offsets) and reads 8-byte records (rec8 + spay_q).
748
+ bool bf_bank = true, g32 = false, w64 = false, elem = false;
749
+ int hd = 0, NSL = 0, SW = 0;
750
+ if constexpr (KBF) {
751
+ // The plain validation sequence of bf16 / bf16 rows in equal 16-byte slices (no tail rows reach this
752
+ // entry: make_gds picks it only for the regular module's rows). Bank rows < 2^30 follow from < 2^32 elements
753
+ // and hd % 4 == 0 (the records' 31-bit slots).
754
+ TORCH_CHECK(bank.dim() == 2 && bank.is_contiguous() && grad.is_contiguous() && grad.sizes() == bank.sizes() &&
755
+ bank.scalar_type() == at::kBFloat16 && grad.scalar_type() == at::kBFloat16,
756
+ "gx_gds (bf16 entry): bf16 bank and bf16 grad [slots, hd], contiguous");
757
+ TORCH_CHECK(bank.numel() < (int64_t(1) << 32), "gx_gds (bf16 entry): bank of 2^32 elements or more");
758
+ hd = static_cast<int>(bank.size(1)); // < 2^32 elements: a head of >= 2^31 narrows to <= 0 (refused next)
759
+ TORCH_CHECK(hd > 0 && bank.size(0) > 0, "gx_gds: empty bank or head_dim >= 2^31");
760
+ NSL = slices_of(hd);
761
+ TORCH_CHECK(hd % NSL == 0, "gx_gds: head_dim split");
762
+ SW = hd / NSL;
763
+ TORCH_CHECK((hd % 4) == 0 && (SW % 4) == 0, "gx_gds: head_dim / slice width multiples of 4");
764
+ } else {
765
+ bf_bank = bank.scalar_type() == at::kBFloat16;
766
+ g32 = grad.scalar_type() == at::kFloat;
767
+ TORCH_CHECK(bank.dim() == 2 && bank.is_contiguous() && grad.is_contiguous() && grad.sizes() == bank.sizes() &&
768
+ (bf_bank || bank.scalar_type() == at::kFloat) && (g32 || grad.scalar_type() == at::kBFloat16) &&
769
+ (bf_bank || g32),
770
+ "gx_gds: bf16 bank + bf16 / f32 grad, or f32 bank + f32 grad, [slots, hd], contiguous");
771
+ TORCH_CHECK(bank.size(1) > 0 && bank.size(1) < (int64_t(1) << 31) && bank.size(0) > 0,
772
+ "gx_gds: empty bank or head_dim >= 2^31"); // checked before narrowing to int
773
+ #if defined(GX_GDS_TAILS_ONLY)
774
+ TORCH_CHECK(bank.size(1) <= (int64_t(1) << 20), "gx_gds (tail module): head_dim <= 2^20 (the tail-module bound)");
775
+ #endif
776
+ hd = static_cast<int>(bank.size(1));
777
+ TORCH_CHECK(bank.size(0) <= (int64_t(1) << 31) - 1, "gx_gds: bank rows must fit the records' 31-bit slots");
778
+ w64 = !bf_bank || g32 || bank.numel() >= (int64_t(1) << 32);
779
+ NSL = slices_of(hd);
780
+ // Equal slices of hd / NSL columns, or (tails: hd % NSL != 0 or a slice width that is not a multiple of 4)
781
+ // dc_pa3's slices of SW = 4 ceil(ceil(hd / 4) / NSL) columns, the last one narrower (MODE bit 8, 8 warps per
782
+ // CTA; hd % 4 != 0, always a tail: element pieces, MODE bit 16). Tails run in the tail module
783
+ // (sdm_gx_gds_tail).
784
+ const bool tail = hd % NSL != 0 || (hd / NSL) % 4 != 0;
785
+ elem = hd % 4 != 0;
786
+ SW = tail ? 4 * ((1 + (hd - 1) / 4 + NSL - 1) / NSL) : hd / NSL; // ceil(hd / 4) without int overflow
787
+ TORCH_CHECK((SW % 4) == 0, "gx_gds: slice width a multiple of 4");
788
+ TORCH_CHECK(!tail || nw == 8, "gx_gds: tail slices take 8 warps per CTA");
789
+ #if defined(GX_GDS_TAILS_ONLY)
790
+ TORCH_CHECK(tail, "gx_gds (tail module): tail slices only");
791
+ #else
792
+ TORCH_CHECK(!tail, "gx_gds: tail slices run in the tail module (gx_gds_cuda._tail_module)");
793
+ #endif
794
+ }
795
+ TORCH_CHECK(gr.scalar_type() == at::kFloat && gr.is_contiguous(), "gx_gds: grad_readings f32 contiguous");
796
+ TORCH_CHECK(reinterpret_cast<uintptr_t>(bank.data_ptr()) % 16 == 0 &&
797
+ reinterpret_cast<uintptr_t>(grad.data_ptr()) % 16 == 0 &&
798
+ reinterpret_cast<uintptr_t>(gr.data_ptr()) % 16 == 0 &&
799
+ reinterpret_cast<uintptr_t>(dots.data_ptr()) % 4 == 0, "gx_gds: 16-byte alignment (bank, grad, readings)");
800
+ TORCH_CHECK(dots.is_contiguous() && dots.scalar_type() == at::kFloat, "gx_gds: dots f32 contiguous");
801
+ TORCH_CHECK(tb.is_contiguous() && tb.scalar_type() == at::kInt && tb.dim() == 2, "gx_gds: tiles int32 [N, TU + 1]");
802
+ TORCH_CHECK(nw == 4 || nw == 8 || nw == 16, "gx_gds: 4, 8 or 16 warps per CTA");
803
+ TORCH_CHECK(C >= 1 && C <= 256 && R >= 1 && R <= 256, "gx_gds: chunk / read width");
804
+ const int64_t N = tb.size(0);
805
+ const int n = static_cast<int>(C * R);
806
+ // Records: int32 [Nr, n, 4] (qrec) or [Nr, n, 2] (rec8, + spay_q [N, n]); Nr = N (every chunk) or P (this step).
807
+ TORCH_CHECK(rec.scalar_type() == at::kInt && rec.is_contiguous() && rec.dim() == 3 && rec.size(1) == n &&
808
+ (rec.size(2) == 4 || rec.size(2) == 2) && (rec.size(0) == N || rec.size(0) == P),
809
+ "gx_gds: records int32 [N or P, C * R, 4 (qrec) or 2 (rec8)]");
810
+ const int REC = rec.size(2) == 4 ? 0 : 1;
811
+ TORCH_CHECK(reinterpret_cast<uintptr_t>(rec.data_ptr()) % (REC == 0 ? 16 : 8) == 0, "gx_gds: record alignment");
812
+ if constexpr (!KBF) {
813
+ TORCH_CHECK(!w64 || REC == 1, "gx_gds: the float32-row / 64-bit-offset instantiations read 8-byte records "
814
+ "(rec8 + spay_q)");
815
+ }
816
+ TORCH_CHECK(REC == 0 || (spay.scalar_type() == at::kInt && spay.is_contiguous() && spay.numel() == N * n &&
817
+ reinterpret_cast<uintptr_t>(spay.data_ptr()) % 4 == 0),
818
+ "gx_gds: rec8 needs spay_q int32 [N, C * R]");
819
+ const int64_t rec_chunk0 = rec.size(0) == N ? 0 : ti * P;
820
+ const int64_t plane = N * static_cast<int64_t>(n);
821
+ TORCH_CHECK(dots.numel() == plane * NSL, "gx_gds: dots planes");
822
+ const int TU = static_cast<int>(tb.size(1)) - 1;
823
+ TORCH_CHECK(TU == K * nw, "gx_gds: tiles");
824
+ TORCH_CHECK(ti >= 0 && P >= 1 && (ti + 1) * P <= N && N < (int64_t(1) << 31), "gx_gds: step out of range");
825
+ TORCH_CHECK(gr_step_rows >= 0 && gr.numel() >= (ti * gr_step_rows + P * C) * hd, "gx_gds: grad_readings too small");
826
+ for (const at::Tensor* t : {&grad, &gr, &rec, &tb, &dots})
827
+ TORCH_CHECK(t->device() == bank.device(), "gx_gds: all operands on the bank's device");
828
+ TORCH_CHECK(REC == 0 || spay.device() == bank.device(), "gx_gds: spay_q on the bank's device");
829
+ Geo g;
830
+ g.P = static_cast<int>(P);
831
+ g.C = static_cast<int>(C);
832
+ g.n = n;
833
+ g.hd = hd;
834
+ g.NSL = NSL;
835
+ g.SW = SW;
836
+ g.K = static_cast<int>(K);
837
+ g.TU = TU;
838
+ g.tok_bytes = ((static_cast<int>(C) * SW * 4 + 64) + 127) / 128 * 128;
839
+ const size_t smem = static_cast<size_t>(g.tok_bytes) + static_cast<size_t>(nw) * 64 * sizeof(uint4) + 16;
840
+ TORCH_CHECK(K >= 1 && K <= (int64_t(1) << 20), "gx_gds: tiles per chunk");
841
+ const int64_t blocks64 = P * static_cast<int64_t>(NSL) * K; // full product in 64 bits before narrowing
842
+ TORCH_CHECK(blocks64 <= static_cast<int64_t>(0x7FFFFFFF), "gx_gds: grid of ", blocks64, " CTAs exceeds 2^31 - 1");
843
+ const int blocks = static_cast<int>(blocks64);
844
+ if (blocks == 0) return;
845
+ cudaStream_t stream = static_cast<cudaStream_t>(at::cuda::getCurrentCUDAStream());
846
+ const int32_t* sp = REC == 1 ? spay.data_ptr<int32_t>() : nullptr;
847
+ #define GX_L(E_, G_, NW_, REC_, MODE_) \
848
+ { \
849
+ auto kf = gx_gds_kernel<E_, G_, NW_, REC_, MODE_>; \
850
+ set_smem(kf, smem); \
851
+ gx_launch(kf, blocks, NW_ * 32, smem, stream, pdl != 0, \
852
+ reinterpret_cast<const E_*>(bank.data_ptr()), \
853
+ reinterpret_cast<G_*>(grad.data_ptr()), static_cast<const float*>(gr.data_ptr<float>()), \
854
+ gr_step_rows, static_cast<const void*>(rec.data_ptr()), sp, rec_chunk0, \
855
+ static_cast<const int32_t*>(tb.data_ptr<int32_t>()), dots.data_ptr<float>(), plane, \
856
+ static_cast<int>(ti), g, static_cast<int>(R)); \
857
+ }
858
+ #define GX_LN(E_, G_, REC_, MODE_) { if (nw == 4) GX_L(E_, G_, 4, REC_, MODE_) else if (nw == 16) GX_L(E_, G_, 16, REC_, MODE_) else GX_L(E_, G_, 8, REC_, MODE_) }
859
+ #if defined(GX_GDS_TAILS_ONLY)
860
+ // Tail slices, 8 warps: MODE 8 (| 4 W64) for hd % 4 == 0, MODE 24 (| 4) element pieces for hd % 4 != 0. Their
861
+ // own module: the regular module's instantiations stay exactly the regular set.
862
+ static_assert(!KBF, "gx_gds: the bf16 entry is the regular module's");
863
+ if (!elem) {
864
+ if (!w64) {
865
+ if (REC == 0) GX_L(__nv_bfloat16, __nv_bfloat16, 8, 0, 8) else GX_L(__nv_bfloat16, __nv_bfloat16, 8, 1, 8)
866
+ } else if (!bf_bank) {
867
+ GX_L(float, float, 8, 1, 12)
868
+ } else if (g32) {
869
+ GX_L(__nv_bfloat16, float, 8, 1, 12)
870
+ } else {
871
+ GX_L(__nv_bfloat16, __nv_bfloat16, 8, 1, 12)
872
+ }
873
+ } else if (!w64) {
874
+ if (REC == 0) GX_L(__nv_bfloat16, __nv_bfloat16, 8, 0, 24) else GX_L(__nv_bfloat16, __nv_bfloat16, 8, 1, 24)
875
+ } else if (!bf_bank) {
876
+ GX_L(float, float, 8, 1, 28)
877
+ } else if (g32) {
878
+ GX_L(__nv_bfloat16, float, 8, 1, 28)
879
+ } else {
880
+ GX_L(__nv_bfloat16, __nv_bfloat16, 8, 1, 28)
881
+ }
882
+ #else
883
+ (void)elem;
884
+ if (KBF || !w64) {
885
+ if (REC == 0) GX_LN(__nv_bfloat16, __nv_bfloat16, 0, 0) else GX_LN(__nv_bfloat16, __nv_bfloat16, 1, 0)
886
+ } else if constexpr (!KBF) {
887
+ if (!bf_bank) {
888
+ GX_LN(float, float, 1, 4)
889
+ } else if (g32) {
890
+ GX_LN(__nv_bfloat16, float, 1, 4)
891
+ } else {
892
+ GX_LN(__nv_bfloat16, __nv_bfloat16, 1, 4)
893
+ }
894
+ }
895
+ #endif
896
+ #undef GX_LN
897
+ #undef GX_L
898
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
899
+ }
900
+ } // namespace
901
+
902
+ void gx_gds(at::Tensor bank, at::Tensor grad, at::Tensor gr, int64_t gr_step_rows, at::Tensor rec, at::Tensor spay,
903
+ at::Tensor tb, at::Tensor dots, int64_t ti, int64_t P, int64_t C, int64_t R, int64_t K, int64_t nw,
904
+ int64_t pdl) {
905
+ gx_gds_run<false>(bank, grad, gr, gr_step_rows, rec, spay, tb, dots, ti, P, C, R, K, nw, pdl);
906
+ }
907
+
908
+ #if !defined(GX_GDS_TAILS_ONLY)
909
+ // The bf16 entry: the same kernels, geometry and arguments as gx_gds for bf16 / bf16 rows below 2^32 elements.
910
+ void gx_gds_bf(at::Tensor bank, at::Tensor grad, at::Tensor gr, int64_t gr_step_rows, at::Tensor rec, at::Tensor spay,
911
+ at::Tensor tb, at::Tensor dots, int64_t ti, int64_t P, int64_t C, int64_t R, int64_t K, int64_t nw,
912
+ int64_t pdl) {
913
+ gx_gds_run<true>(bank, grad, gr, gr_step_rows, rec, spay, tb, dots, ti, P, C, R, K, nw, pdl);
914
+ }
915
+ #endif
916
+
917
+ // ---- the backward's C++ step loop (bwd_steps_cuda): gx_gds as a per-backward plan -------------------------------------
918
+ // gx_gds_plan runs the per-step entry's own host path (gx_gds_run<kbf>: gx_gds, or the regular module's bf16 entry
919
+ // gx_gds_bf) with pdl 0 ONCE under plan capture at step ti_max: every check runs (its step bounds cover every
920
+ // ti <= ti_max) and the launch is recorded. gx_gds_plan_launch (a plain C++ entry point; address from gx_gds_launch_addr)
921
+ // sets the step parameters (ti; rec_chunk0 = ti * P for per-step records, else 0, as gx_gds computes it) and issues the
922
+ // recorded launch on the given stream (the caller's current stream, as gx_gds uses): with programmatic stream
923
+ // serialization when pdl != 0, else plain -- gx_launch's choice for the same pdl.
924
+ namespace {
925
+ constexpr uint64_t kGdsPlanMagic = 0x314e414c50534447ull;
926
+ constexpr int kGdsRecChunk0Param = 6; // (bank, grad, gr, gr_step_rows, rec, spay, rec_chunk0, tb, dots, plane, ti, g, R)
927
+ constexpr int kGdsTiParam = 10;
928
+ struct GdsPlan {
929
+ uint64_t magic;
930
+ int32_t device;
931
+ int32_t rec_per_step;
932
+ int32_t kbf;
933
+ int32_t pad;
934
+ int64_t ti_max, P;
935
+ sdm_plancap::Launch cap;
936
+ };
937
+
938
+ int64_t gx_gds_plan_launch(const void* plan_bytes, int64_t nbytes, int64_t ti, int64_t pdl, void* stream) {
939
+ GdsPlan pl;
940
+ if (!sdm_plancap::from_bytes(pl, plan_bytes, nbytes, kGdsPlanMagic)) return -1;
941
+ if (ti < 0 || ti > pl.ti_max) return -2;
942
+ if (pl.cap.set == 0) return 0; // gx_gds launched nothing (empty grid)
943
+ if (sdm_plancap::current_device() != pl.device) return -5;
944
+ int* tip = sdm_plancap::param<int>(pl.cap, kGdsTiParam);
945
+ int64_t* r0p = sdm_plancap::param<int64_t>(pl.cap, kGdsRecChunk0Param);
946
+ if (tip == nullptr || r0p == nullptr) return -7;
947
+ *tip = static_cast<int>(ti);
948
+ *r0p = pl.rec_per_step ? ti * pl.P : 0;
949
+ return sdm_plancap::launch(pl.cap, pdl != 0, static_cast<cudaStream_t>(stream));
950
+ }
951
+ } // namespace
952
+
953
+ at::Tensor gx_gds_plan(at::Tensor bank, at::Tensor grad, at::Tensor gr, int64_t gr_step_rows, at::Tensor rec,
954
+ at::Tensor spay, at::Tensor tb, at::Tensor dots, int64_t ti_max, int64_t P, int64_t C, int64_t R,
955
+ int64_t K, int64_t nw, bool kbf) {
956
+ TORCH_CHECK(ti_max >= 0 && ti_max < (int64_t(1) << 31), "gx_gds_plan: ti_max");
957
+ GdsPlan pl;
958
+ std::memset(&pl, 0, sizeof(pl));
959
+ {
960
+ sdm_plancap::Scope scope(&pl.cap);
961
+ #if defined(GX_GDS_TAILS_ONLY)
962
+ TORCH_CHECK(!kbf, "gx_gds_plan: the bf16 entry is the regular module's");
963
+ gx_gds_run<false>(bank, grad, gr, gr_step_rows, rec, spay, tb, dots, ti_max, P, C, R, K, nw, 0);
964
+ #else
965
+ if (kbf) {
966
+ gx_gds_run<true>(bank, grad, gr, gr_step_rows, rec, spay, tb, dots, ti_max, P, C, R, K, nw, 0);
967
+ } else {
968
+ gx_gds_run<false>(bank, grad, gr, gr_step_rows, rec, spay, tb, dots, ti_max, P, C, R, K, nw, 0);
969
+ }
970
+ #endif
971
+ }
972
+ pl.magic = kGdsPlanMagic;
973
+ pl.device = static_cast<int32_t>(bank.get_device());
974
+ pl.rec_per_step = (rec.dim() == 3 && rec.size(0) != tb.size(0)) ? 1 : 0; // gx_gds: rec_chunk0 = (Nr == N) ? 0 : ti * P
975
+ pl.kbf = kbf ? 1 : 0;
976
+ pl.ti_max = ti_max;
977
+ pl.P = P;
978
+ TORCH_CHECK(pl.cap.set == 0 || (pl.cap.nparams == 13 && sdm_plancap::param<int>(pl.cap, kGdsTiParam) != nullptr &&
979
+ sdm_plancap::param<int64_t>(pl.cap, kGdsRecChunk0Param) != nullptr),
980
+ "gx_gds_plan: parameters");
981
+ return sdm_plancap::to_tensor(pl);
982
+ }
983
+
984
+ int64_t gx_gds_launch_addr() { return reinterpret_cast<int64_t>(&gx_gds_plan_launch); }
985
+
986
+ """.replace("@ROW_HELPERS@\n", ROW_HELPERS.lstrip("\n"))
987
+
988
+ @functools.cache
989
+ def _module():
990
+ return load_cuda_extension(
991
+ "sdm_gx_gds",
992
+ cpp_sources=_CPP,
993
+ cuda_sources=_CUDA,
994
+ functions=("gx_gds", "gx_gds_bf", "gx_gds_plan", "gx_gds_launch_addr"),
995
+ headers=headers(),
996
+ extra_cuda_cflags=("-O3", "-lineinfo"),
997
+ )
998
+
999
+
1000
+ @functools.cache
1001
+ def _tail_module():
1002
+ """Tails: the same source with only the tail instantiations (MODE 8 / 24), built on first use."""
1003
+ return load_cuda_extension(
1004
+ "sdm_gx_gds_tail",
1005
+ cpp_sources=_CPP,
1006
+ cuda_sources=_CUDA,
1007
+ functions=("gx_gds", "gx_gds_plan", "gx_gds_launch_addr"),
1008
+ headers=headers(),
1009
+ extra_cuda_cflags=("-O3", "-lineinfo", "-DGX_GDS_TAILS_ONLY"),
1010
+ )
1011
+
1012
+
1013
+ def _wave_warps(device=None) -> int:
1014
+ """Resident warps of one wave: 16 per SM at 128 registers (GB300: 2432)."""
1015
+ return 16 * num_sms(device)
1016
+
1017
+
1018
+ def _target_warps(device=None) -> int:
1019
+ """Warps a step aims for: the largest power of two within one wave (GB300: 2048)."""
1020
+ return prev_pow2(_wave_warps(device))
1021
+
1022
+
1023
+ def geometry(num_parallel: int = 0, nsl: int = 1, n: int = 0, chunk: int = 0, sw: int = 0, device=None) -> tuple:
1024
+ """(nw, ppw): warps per CTA and positions per warp tile, from shapes and the device's SM count (the outputs depend on
1025
+ neither: the walk's arithmetic is per sorted group, and tiles snap to group starts). Two shape regimes first:
1026
+ (a) the slice's token rows exceed 96 KB (chunk * sw * 4 B, e.g. C 256 with 128-column slices: one CTA per SM):
1027
+ 16 warps, 256-position tiles;
1028
+ (b) read width n / chunk >= 256: 4 warps, 32-position tiles.
1029
+ Otherwise 8 warps and ppw 128, halved (floor 32) while a step would launch fewer than _target_warps(device) warps,
1030
+ i.e. num_parallel * nsl * ceil(n / ppw) < target. When that still launches fewer than _wave_warps(device) warps
1031
+ (16 resident warps per SM at 128 registers), the step's warps fill one wave evenly instead: 4-warp CTAs (4 per SM:
1032
+ the same 16 warps), K = wave // (4 * num_parallel * nsl) CTAs per (chunk, slice) (at most ceil(n / 64): tiles of
1033
+ >= 16 positions), ppw = ceil(n / (4 K)); 8-warp CTAs would leave some SMs one CTA short."""
1034
+ if chunk > 0:
1035
+ if chunk * sw * 4 > 96 * 1024:
1036
+ return 16, 256
1037
+ if n >= 256 * chunk:
1038
+ return 4, 32
1039
+ nw = 8
1040
+ target = _target_warps(device)
1041
+ ppw = 128
1042
+ while ppw > 32 and num_parallel * nsl * (-(-n // ppw)) < target:
1043
+ ppw //= 2
1044
+ wave = _wave_warps(device)
1045
+ if n > 0 and num_parallel >= 1 and nsl >= 1 and num_parallel * nsl * nw * (-(-n // (nw * ppw))) < wave:
1046
+ k = max(1, min(wave // (4 * num_parallel * nsl), -(-n // 64)))
1047
+ return 4, -(-n // (4 * k))
1048
+ return nw, ppw
1049
+
1050
+
1051
+ def supported(bank: torch.Tensor, grad_bank: torch.Tensor, chunk_size: int, width: int) -> bool:
1052
+ """Shape / dtype / arch gate (never the data): a bf16 bank with bf16 or f32 grad rows, or an f32 bank with f32 grad
1053
+ rows, on every CUDA backend (sm_80+), rows of whole 4-element vectors, bank rows < 2^31, and the token rows of a slice
1054
+ fitting the device's opt-in shared memory."""
1055
+ return admits(bank, grad_bank.dtype, chunk_size, width)
1056
+
1057
+
1058
+ ROW_TYPES = ((torch.bfloat16, torch.bfloat16), (torch.bfloat16, torch.float32), (torch.float32, torch.float32))
1059
+
1060
+
1061
+ def wide_offsets(bank: torch.Tensor, grad_dtype) -> bool:
1062
+ """The call takes a MODE-4 instantiation (64-bit row offsets): float32 rows, or bf16 banks of >= 2^32 elements."""
1063
+ return not (bank.dtype == torch.bfloat16 and grad_dtype == torch.bfloat16) or bank.numel() >= 2 ** 32
1064
+
1065
+
1066
+ def slices(hd: int) -> tuple:
1067
+ """(nsl, sw, tail): equal slices of hd / nsl columns (a multiple of 4), or (tail) dc_pa3's uneven slices."""
1068
+ nsl = -(-hd // 128)
1069
+ if hd % nsl == 0 and (hd // nsl) % 4 == 0:
1070
+ return nsl, hd // nsl, False
1071
+ return nsl, 4 * (-(-(-(-hd // 4)) // nsl)), True
1072
+
1073
+
1074
+ def _fits(bank, chunk_size: int, width: int, sw: int, nsl: int, tail: bool) -> bool:
1075
+ nw = 8 if tail else geometry(0, nsl, chunk_size * width, chunk_size, sw, bank.device)[0] # the launch's warps
1076
+ tok = (chunk_size * sw * 4 + 64 + 127) // 128 * 128
1077
+ return tok + nw * 64 * 16 + 16 <= da2._smem_limit(bank.device)
1078
+
1079
+
1080
+ def tail_supported(bank, chunk_size, quant="none") -> bool:
1081
+ """Tail hook (movers.tail_admits): this GDS takes the call's head on every backend (rtx: chunks <= 128); it never
1082
+ reads snapshot payloads, so ``quant`` does not matter."""
1083
+ if not bank.is_cuda or bank.dim() != 2 or bank.dtype not in (torch.bfloat16, torch.float32):
1084
+ return False
1085
+ try:
1086
+ b = backend(bank.device)
1087
+ except RuntimeError:
1088
+ return False
1089
+ if not (b in ("modern", "a100") or (b == "rtx" and chunk_size <= 128)) or not (1 <= chunk_size <= 256):
1090
+ return False
1091
+ hd = bank.shape[-1]
1092
+ if not (1 <= hd <= 2 ** 20) or bank.shape[0] >= 2 ** 31: # hd bound: the forward's
1093
+ return False
1094
+ nsl, sw, tail = slices(hd)
1095
+ return _fits(bank, chunk_size, 256, sw, nsl, tail)
1096
+
1097
+
1098
+ def admits(bank: torch.Tensor, grad_dtype, chunk_size: int, width: int, quant="none") -> bool:
1099
+ """supported() with the grad bank's dtype instead of the tensor (callers deciding before it exists); tails via
1100
+ movers.tail_admits."""
1101
+ if bank.dim() != 2 or (bank.dtype, grad_dtype) not in ROW_TYPES or not bank.is_cuda:
1102
+ return False
1103
+ if not cuda_backend(bank.device):
1104
+ return False
1105
+ hd = bank.shape[-1]
1106
+ if not 0 < hd < 2 ** 31: # the native launchers take hd as int
1107
+ return False
1108
+ if not (1 <= chunk_size <= 256) or not (1 <= width <= 256):
1109
+ return False
1110
+ if bank.shape[0] >= 2 ** 31: # the records' slots are 31-bit
1111
+ return False
1112
+ nsl, sw, tail = slices(hd)
1113
+ if tail:
1114
+ from .. import movers
1115
+ if not movers.tail_admits(bank, chunk_size, quant):
1116
+ return False
1117
+ return _fits(bank, chunk_size, width, sw, nsl, tail)
1118
+
1119
+
1120
+ LAST_POLICY = {}
1121
+ # 16-byte records copied to the 8-byte layout for a MODE-4 instantiation (once per factory call; tests, diagnostics)
1122
+ REC8_COPIES = [0]
1123
+
1124
+
1125
+ def _factory_records(qrec):
1126
+ """The records of a provider that built ALL of them with ONE plain launch on the current stream when the backward
1127
+ factory ran (px_qrec_cuda.build_qrec8_steps under CUDA-graph capture or for Nt == 1, which the dispatch layer wraps
1128
+ as a record holder with events None and no release hook) are complete before the backward's step loop starts (its
1129
+ first dc_gwr is a plain launch, and nothing writes them afterwards), exactly like tensor records built at factory
1130
+ time (Phase 1's, or mover_da2_cuda.build_qrec): they are returned as that tensor, so that GDS may launch with PDL
1131
+ (its pre-wait reads of the records follow that plain-launch boundary). The kernel reads the same tensor either way
1132
+ (the holder's get(ti) returns (records, None) for every step), so outputs are unchanged. Any other provider
1133
+ (per-step events: records written on a side stream during the loop; a release hook; no `events` attribute) is
1134
+ returned unchanged and keeps plain launches."""
1135
+ if qrec is None or torch.is_tensor(qrec):
1136
+ return qrec
1137
+ rec = getattr(qrec, "rec8", None)
1138
+ if getattr(qrec, "events", 0) is None and getattr(qrec, "release", None) is None and torch.is_tensor(rec):
1139
+ return rec
1140
+ return qrec
1141
+
1142
+
1143
+ def records_ok(qrec, spay_q) -> bool:
1144
+ """The record inputs (any of them has the same content per sorted read position):
1145
+ * qrec int32 [N, C * R, 4] {spay_q element, absolute slot, scale bits, 0} (Phase 1 / mover_da2_cuda.build_qrec);
1146
+ * rec8 int32 [N, C * R, 2] {absolute slot, scale bits} (scale = __fmul_rn(f32(q_val), expf(log_q)); padding:
1147
+ 0, 0), with the flags / token / lane read from spay_q;
1148
+ * a provider with get(ti) -> (records of step ti or of every chunk as int32 [P or N, C * R, 2 or 4], CUDA event
1149
+ or None) and optionally release(ti, event): GDS(ti) waits on the event before it reads the records and calls
1150
+ release with an event recorded right after its launch (the records may be overwritten once it completed)."""
1151
+ if qrec is None:
1152
+ return False
1153
+ if hasattr(qrec, "get") and callable(qrec.get):
1154
+ return True
1155
+ if not torch.is_tensor(qrec) or qrec.dtype != torch.int32 or not qrec.is_contiguous():
1156
+ return False
1157
+ n = spay_q.shape[-1]
1158
+ N = spay_q.numel() // n
1159
+ if qrec.numel() == N * n * 4:
1160
+ return qrec.data_ptr() % 16 == 0
1161
+ if qrec.numel() == N * n * 2:
1162
+ return qrec.data_ptr() % 8 == 0
1163
+ return False
1164
+
1165
+
1166
+ def make_gds(bank, grad_bank, grad_readings, spay_q, qrec, d_q_w, *, chunk_size, num_parallel,
1167
+ steps, q_idx=None, valid_tokens=None, tiles_q=None):
1168
+ """Step launcher of this GDS mover; the same arguments and outputs as mover_da2_cuda.make_gds (which it delegates to
1169
+ outside its scope, keeping that kernel's bits there; it needs the 16-byte qrec). `qrec`: see records_ok()."""
1170
+ from_provider = qrec is not None and not torch.is_tensor(qrec)
1171
+ qrec = _factory_records(qrec) # factory-time records of a provider = tensor records
1172
+ from_provider = from_provider and torch.is_tensor(qrec)
1173
+ n = spay_q.shape[-1]
1174
+ r = n // chunk_size
1175
+ if not supported(bank, grad_bank, chunk_size, r) or not records_ok(qrec, spay_q):
1176
+ # mover_da2_cuda's GDS (its kernel and bits) for the shapes this one declines, on the caller's 16-byte records
1177
+ # (movers.make_mover_gds). gds_admits reads no read width: the admission is checked again here.
1178
+ grad_ok = grad_bank.dtype == bank.dtype or (bank.dtype == torch.bfloat16 and grad_bank.dtype == torch.float32)
1179
+ if not (da2.supported(bank, chunk_size) and grad_ok and da2.fits(bank, chunk_size, "gds", grad_bank.dtype)):
1180
+ raise ValueError(f"make_gds: the mover_da2 route admits its own GDS inputs only (bank bf16 / f32 [slots, hd] "
1181
+ f"on sm_90+, grad = bank dtype or f32 with a bf16 bank, fitting shapes); got bank "
1182
+ f"{bank.dtype} {tuple(bank.shape)}, grad {grad_bank.dtype}")
1183
+ LAST_POLICY.clear()
1184
+ LAST_POLICY["impl"] = "da2"
1185
+ inner = da2.make_gds(bank, grad_bank, grad_readings, spay_q, qrec.view(-1, n, 4), d_q_w,
1186
+ chunk_size=chunk_size, num_parallel=num_parallel, steps=steps, q_idx=q_idx,
1187
+ valid_tokens=valid_tokens, tiles_q=tiles_q)
1188
+
1189
+ def launch_da2(ti: int) -> None:
1190
+ inner(ti)
1191
+
1192
+ launch_da2.geom = dict(inner.geom, impl="da2")
1193
+ return launch_da2
1194
+ hd = bank.shape[-1]
1195
+ sc = num_parallel * chunk_size
1196
+ if grad_readings.numel() == steps * sc * hd:
1197
+ gr_rows = sc
1198
+ elif grad_readings.numel() == sc * hd:
1199
+ gr_rows = 0
1200
+ else:
1201
+ raise ValueError("grad_readings: unexpected size")
1202
+ nsl, sw, tail = slices(hd)
1203
+ nw, ppw = geometry(int(num_parallel), nsl, n, int(chunk_size), sw, bank.device)
1204
+ if tail:
1205
+ nw = 8 # the tail instantiations' warps (the launcher checks)
1206
+ elif grad_bank.dtype == torch.float32 and capability(bank.device) == (9, 0) and (nw, ppw) in ((8, 128), (16, 256)):
1207
+ # H100, float32 grad rows: 8-warp CTAs of 64-position tiles (16 warps starve the concurrent dc_gwr). Speed only.
1208
+ nw, ppw = 8, 64
1209
+ K = max(1, -(-n // (nw * ppw)))
1210
+ spay2 = spay_q.reshape(-1, n)
1211
+ N = spay2.shape[0]
1212
+ P = int(num_parallel)
1213
+ if P < 1 or N >= 2 ** 31:
1214
+ raise ValueError(f"make_gds: P >= 1 and N < 2^31 record chunks needed; got P {P}, N {N}")
1215
+ # the steps plan() covers (exact int64 products, ti < 2^31)
1216
+ lim = N // P if gr_rows == 0 else min(N // P, int(steps))
1217
+ tb = da2._tiles_or_build(spay2, tiles_q, K * nw) if ppw == 128 else None # Phase 1's tiles use L = 128
1218
+ if tb is None:
1219
+ tb = da2.build_tiles(spay2, K * nw)
1220
+ provider = qrec if not torch.is_tensor(qrec) else None
1221
+ w64 = wide_offsets(bank, grad_bank.dtype)
1222
+ if provider is None:
1223
+ rec_t = qrec.view(N, n, 4) if qrec.numel() == N * n * 4 else qrec.view(N, n, 2)
1224
+ if w64 and rec_t.shape[-1] == 4:
1225
+ # MODE 4 reads 8-byte records = words 1-2 of the 16-byte ones (one copy per factory call; direct callers only)
1226
+ rec_t = rec_t[:, :, 1:3].clone(memory_format=torch.contiguous_format) # own storage: 8-byte aligned
1227
+ REC8_COPIES[0] += 1
1228
+ dqw = d_q_w.view(-1)
1229
+ part = torch.empty(nsl * dqw.numel(), device=bank.device, dtype=torch.float32) if nsl > 1 else dqw
1230
+ gr_conv = grad_readings.dtype != torch.float32
1231
+ gr = grad_readings if not gr_conv else torch.empty(grad_readings.shape, device=grad_readings.device,
1232
+ dtype=torch.float32)
1233
+ mod = _tail_module() if tail else _module()
1234
+ # the entry, once per factory call: bf16 / bf16 rows below 2^32 elements in the regular module take the bf16 one
1235
+ gfn = mod.gx_gds_bf if not (tail or w64) else mod.gx_gds
1236
+ dmod = da2._module() if nsl > 1 else None
1237
+ LAST_POLICY.clear()
1238
+ # PDL is opt-in PER LAUNCH by the caller (launch(ti, pdl=True)), which vouches that the kernel right before this
1239
+ # launch writes none of the kernel's pre-wait inputs (tile bounds, records, grad_readings rows): the backward step
1240
+ # (gwr -> baddbmm -> PA -> GDS) does; direct calls (tests) get plain launches. Only possible with tensor records (a
1241
+ # provider's records may come from the kernel right before) and f32 readings (else gr is converted by the launch
1242
+ # right before), and only on the modern backend (the portable side has no griddepcontrol).
1243
+ pdl_ok = provider is None and not gr_conv and device_pdl_ok(bank.device)
1244
+ LAST_POLICY.update(impl="gx", nw=nw, ppw=ppw, K=K, nsl=nsl,
1245
+ pdl_capable=pdl_ok,
1246
+ records="provider" if provider is not None else ("qrec" if rec_t.shape[-1] == 4 else "rec8"),
1247
+ factory_records=from_provider, w64=w64,
1248
+ rows=(str(bank.dtype).split(".")[-1], str(grad_bank.dtype).split(".")[-1]))
1249
+ release = getattr(provider, "release", None) if provider is not None else None
1250
+
1251
+ def launch(ti: int, pdl: bool = False) -> None:
1252
+ if gr_conv:
1253
+ gr.copy_(grad_readings)
1254
+ if provider is not None:
1255
+ recs, ev = provider.get(ti)
1256
+ cur = torch.cuda.current_stream(bank.device)
1257
+ if ev is not None:
1258
+ cur.wait_event(ev)
1259
+ # The records are used on this stream (whatever stream produced them, event or not): the caching
1260
+ # allocator must not hand their storage out again before this launch completed, even if the provider
1261
+ # drops its reference. (A no-op when they were allocated on this stream, e.g. under graph capture.)
1262
+ if recs.is_cuda:
1263
+ recs.record_stream(cur)
1264
+ recs = recs if recs.dim() == 3 else recs.view(recs.shape[0], n, -1)
1265
+ if w64 and recs.shape[-1] != 2:
1266
+ raise ValueError("make_gds: this call's 64-bit-offset GDS reads 8-byte records; the provider returned "
1267
+ f"{tuple(recs.shape)}")
1268
+ else:
1269
+ recs = rec_t
1270
+ use_pdl = 1 if (pdl and pdl_ok) else 0
1271
+ gfn(bank, grad_bank, gr, gr_rows, recs, spay2, tb, part, ti, num_parallel, chunk_size, r, K, nw, use_pdl)
1272
+ if release is not None:
1273
+ done = torch.cuda.Event()
1274
+ done.record(torch.cuda.current_stream(bank.device))
1275
+ release(ti, done)
1276
+ if nsl > 1 and ti == 0:
1277
+ dmod.da2_dot_reduce(part, dqw, nsl)
1278
+
1279
+ def plan(nsteps: int, pdl: bool = False):
1280
+ """(plan, launch address, use) of steps 0 .. nsteps - 1 for bwd_steps_cuda, or None when a step's launch is more than
1281
+ one gx_gds launch; use: 1 = PDL. done() runs launch(0)'s slice-plane reduce."""
1282
+ if provider is not None or gr_conv or not 1 <= int(nsteps) <= lim:
1283
+ return None
1284
+ p = mod.gx_gds_plan(bank, grad_bank, gr, gr_rows, rec_t, spay2, tb, part, int(nsteps) - 1, num_parallel,
1285
+ chunk_size, r, K, nw, gfn is not mod.gx_gds) # the entry each launch() runs
1286
+ return p, int(mod.gx_gds_launch_addr()), (1 if (pdl and pdl_ok) else 0)
1287
+
1288
+ def done() -> None:
1289
+ if nsl > 1:
1290
+ dmod.da2_dot_reduce(part, dqw, nsl)
1291
+
1292
+ launch.geom = dict(nw=nw, ppw=ppw, K=K, nsl=nsl, impl="gx", pdl_capable=pdl_ok)
1293
+ launch.pdl_capable = pdl_ok
1294
+ launch.join = None # nothing to join (callers test for it)
1295
+ launch.plan = plan
1296
+ launch.done = done
1297
+ return launch