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,857 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """gx_gemm_cuda: the deterministic backward's per-step d_u update, torch.baddbmm(d_u_ti, x_t_ti, d_dv_ti, out=d_u_ti) with
3
+ x_t = x.transpose(1, 2), as one small FP32 kernel with a FIXED reduction order:
4
+
5
+ d_u[n, i, c] = d_u[n, i, c] + acc, acc = fmaf(x[n, j, i], d_dv[n, j, c], acc) for j = 0, 1, .., C-1 from acc = +0
6
+
7
+ for every chunk n = ti * P + p of step ti, row i < C and column c < hd. Each output element is read, computed and
8
+ written by ONE thread with exactly that chain, so the result does not depend on the tiling, the launch geometry or the
9
+ run, and the in-place update is race-free.
10
+
11
+ That chain and the single rounding of d_u + acc are what cuBLAS's FP32 batched GEMM computed for the backward's call
12
+ on every configuration tested (allow_tf32 off; batch P >= 2, C >= 2), but that is not a cuBLAS guarantee, so the kernel
13
+ is qualified at run time (matches_cublas: per key covering device / arch, cuBLAS version, shapes, strides, pointer
14
+ alignment classes, dtypes, precision / BLAS / workspace settings; 6 input patterns incl. zeros, large magnitudes,
15
+ denormals, Inf / NaN), and the backward uses it only where the two agree bit for bit; everywhere else, and during a
16
+ CUDA graph capture of a configuration not qualified yet, it keeps cuBLAS. Either way the outputs are cuBLAS's bits.
17
+ matches_cublas first applies a speed rule from shapes only (one_wave: the launch grid fits one wave at one CTA per SM);
18
+ multi-wave shapes keep cuBLAS without a probe.
19
+
20
+ PDL (per launch opt-in by the caller, step(ti, pdl=True); the modern backend only): the kernel's x block and its d_u
21
+ values (d_u(ti) is written only by this kernel after the backward's initial bmm) are read before griddepcontrol.wait;
22
+ d_dv (the previous kernel's output, the step's GWR) after it; griddepcontrol.launch_dependents follows the wait. The
23
+ portable device passes (sm_80 / 86 / 89 / 120 ...: SDM_PORTABLE_ARCH) compile without griddepcontrol and run plain
24
+ launches."""
25
+
26
+ import functools
27
+ import os
28
+ import threading
29
+ import time
30
+
31
+ import torch
32
+
33
+ from ._build import _target_architectures, load_cuda_extension
34
+ from ._device import (backend, capability as _device_capability, cuda_backend, headers, num_sms,
35
+ pdl_ok as device_pdl_ok, smem_cap, smem_per_sm)
36
+
37
+ _CPP = r"""
38
+ void gx_gemm(at::Tensor du, at::Tensor x, at::Tensor dv, int64_t ti, int64_t P, int64_t pdl, int64_t ti_rows,
39
+ int64_t smem_min);
40
+ void gx_gemm_attrs(at::Tensor out);
41
+ at::Tensor gx_gemm_plan(at::Tensor du, at::Tensor x, at::Tensor dv, int64_t ti_max, int64_t P, int64_t ti_rows,
42
+ int64_t smem_min);
43
+ int64_t gx_gemm_launch_addr();
44
+ """
45
+
46
+ _CUDA = r"""
47
+ #include <torch/extension.h>
48
+ #include <ATen/cuda/CUDAContext.h>
49
+ #include <c10/cuda/CUDAGuard.h>
50
+ #include <c10/cuda/CUDAException.h>
51
+ #include <algorithm>
52
+ #include <cstdint>
53
+ #include <map>
54
+ #include <mutex>
55
+ #include <utility>
56
+ #include "sdm_copy.cuh"
57
+ #include "sdm_plancap.cuh"
58
+
59
+ #ifndef GX_GEMM_TI
60
+ #define GX_GEMM_TI 16 // only the instance gx_gemm_attrs reports (rows per CTA are chosen per launch)
61
+ #endif
62
+
63
+ namespace {
64
+
65
+ constexpr int TC = 64; // columns c per CTA
66
+ constexpr int RC = 4; // columns per thread (one float4 of the staged d_dv row)
67
+ constexpr int TX = TC / RC; // 16 threads across the columns
68
+ constexpr int NT = 256;
69
+ constexpr int TY = NT / TX; // 16 thread rows
70
+
71
+ #ifdef SDM_PORTABLE_ARCH
72
+ #define GX_GEMM_SM90(...)
73
+ #else
74
+ #define GX_GEMM_SM90(...) __VA_ARGS__
75
+ #endif
76
+
77
+ // The staging copies are asynchronous (cp.async), so all of a thread's global loads are in flight at once; past C / hd
78
+ // they zero-fill (src-size 0 reads nothing). 16-byte copies (.cg: L2 only) when the rows are 16-byte aligned; x otherwise
79
+ // in 4-byte copies (.ca); d_dv otherwise with plain L2 loads. The caller passes a valid global address as `src` for
80
+ // zero-filled vectors (the tensor's base; never read: src-size 0), so no out-of-array pointer is ever formed. cp.async
81
+ // needs sm_80+; pre-sm_80 compile passes trap instead (supported() admits sm_80+ only, so they are never launched).
82
+ #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 800)
83
+ #define GX_GEMM_PRE_SM80 1
84
+ #endif
85
+ __device__ __forceinline__ void cp16(float* dst, const float* src, int bytes) {
86
+ #ifdef GX_GEMM_PRE_SM80
87
+ __trap();
88
+ #else
89
+ const uint32_t d = static_cast<uint32_t>(__cvta_generic_to_shared(dst));
90
+ asm volatile("cp.async.cg.shared.global [%0], [%1], 16, %2;\n" ::"r"(d), "l"(src), "r"(bytes) : "memory");
91
+ #endif
92
+ }
93
+ __device__ __forceinline__ void cp4(float* dst, const float* src, bool valid) {
94
+ #ifdef GX_GEMM_PRE_SM80
95
+ __trap();
96
+ #else
97
+ const uint32_t d = static_cast<uint32_t>(__cvta_generic_to_shared(dst));
98
+ asm volatile("cp.async.ca.shared.global [%0], [%1], 4, %2;\n" ::"r"(d), "l"(src), "r"(valid ? 4 : 0) : "memory");
99
+ #endif
100
+ }
101
+ __device__ __forceinline__ void cp_wait_all() {
102
+ #ifdef GX_GEMM_PRE_SM80
103
+ __trap();
104
+ #else
105
+ asm volatile("cp.async.wait_all;\n" ::: "memory");
106
+ #endif
107
+ }
108
+
109
+ // Grid: P * nib * ncb CTAs (b: cb = b % ncb, ib = b / ncb % nib, p = rest). Shared memory: xs [C][TI] = x[n, j, i0 + ii]
110
+ // (zero past C), ds [C][TC] = d_dv[n, j, c0 + cc] (zero past hd).
111
+ // TI (rows per CTA: 16, 32, 48 or 64) is a template parameter chosen per launch; PRE (always true): the thread's d_u
112
+ // values are read before griddepcontrol.wait into registers (d_u(ti) is written only by this kernel: the backward's
113
+ // initial bmm wrote it long before the step loop; nothing in flight writes it) and the epilogue stores float4s. The
114
+ // chain per element and the single rounding of d_u + acc do not depend on TI.
115
+ template <int TI, bool PRE>
116
+ __global__ void __launch_bounds__(NT) gx_gemm_kernel(float* __restrict__ du, const float* __restrict__ x,
117
+ const float* __restrict__ dv, int ti, int P, int C, int hd,
118
+ int nib, int ncb) {
119
+ constexpr int RI = TI / TY; // rows per thread
120
+ static_assert(TI % TY == 0 && RI >= 1 && RI <= 4, "TI: 16, 32, 48 or 64");
121
+ extern __shared__ __align__(16) float sm[];
122
+ float* xs = sm;
123
+ float* ds = sm + C * TI;
124
+ unsigned b = blockIdx.x;
125
+ const int cb = static_cast<int>(b % static_cast<unsigned>(ncb));
126
+ b /= static_cast<unsigned>(ncb);
127
+ const int ib = static_cast<int>(b % static_cast<unsigned>(nib));
128
+ const int p = static_cast<int>(b / static_cast<unsigned>(nib));
129
+ const int64_t n = static_cast<int64_t>(ti) * P + p;
130
+ const int i0 = ib * TI, c0 = cb * TC;
131
+ const float* xn = x + n * C * C; // x[n] [C][C]: x[n, j, i] at j * C + i
132
+ const float* dvn = dv + n * static_cast<int64_t>(C) * hd; // d_dv rows of chunk n [C][hd]
133
+ float* dun = du + n * static_cast<int64_t>(C) * hd; // d_u rows of chunk n [C][hd]
134
+ // x: a forward tensor (staged before the wait under PDL). Rows of C floats: 16-byte vectors when C % 4 == 0 (i0 and
135
+ // ii are multiples of 4, so a vector is entirely valid or entirely past C).
136
+ if ((C & 3) == 0 && (reinterpret_cast<uintptr_t>(x) & 15u) == 0) {
137
+ #pragma unroll 4
138
+ for (int v = threadIdx.x; v < C * (TI / 4); v += NT) {
139
+ const int j = v / (TI / 4), ii = 4 * (v - j * (TI / 4));
140
+ const bool ok = i0 + ii < C;
141
+ cp16(xs + j * TI + ii, ok ? xn + static_cast<int64_t>(j) * C + i0 + ii : x, ok ? 16 : 0);
142
+ }
143
+ } else {
144
+ #pragma unroll 4
145
+ for (int e = threadIdx.x; e < C * TI; e += NT) {
146
+ const int j = e / TI, ii = e - j * TI;
147
+ const bool ok = i0 + ii < C;
148
+ cp4(xs + e, ok ? xn + static_cast<int64_t>(j) * C + i0 + ii : x, ok);
149
+ }
150
+ }
151
+ const int tx = threadIdx.x % TX, ty = threadIdx.x / TX;
152
+ // PRE: this thread's d_u values (rows ty + TY * r, columns c0 + tx * RC + q), before the wait. Vector path when
153
+ // hd % 4 == 0 and d_u is 16-byte aligned (a float4 is then entirely valid or entirely past hd).
154
+ const bool vec_u = (hd & 3) == 0 && (reinterpret_cast<uintptr_t>(du) & 15u) == 0;
155
+ float4 u0[PRE ? RI : 1];
156
+ {
157
+ #pragma unroll
158
+ for (int r = 0; r < RI; ++r) {
159
+ const int i = i0 + ty + TY * r;
160
+ const int c = c0 + tx * RC;
161
+ u0[r] = make_float4(0.f, 0.f, 0.f, 0.f);
162
+ if (i < C && c < hd) { // pointers formed inside the row only
163
+ const float* o = dun + static_cast<int64_t>(i) * hd + c;
164
+ if (vec_u) {
165
+ u0[r] = __ldcg(reinterpret_cast<const float4*>(o)); // hd % 4 == 0: the 4 floats are in the row
166
+ } else {
167
+ u0[r].x = __ldcg(o);
168
+ if (c + 1 < hd) u0[r].y = __ldcg(o + 1);
169
+ if (c + 2 < hd) u0[r].z = __ldcg(o + 2);
170
+ if (c + 3 < hd) u0[r].w = __ldcg(o + 3);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ GX_GEMM_SM90(asm volatile("griddepcontrol.wait;" ::: "memory")); // the previous kernel: complete
176
+ GX_GEMM_SM90(asm volatile("griddepcontrol.launch_dependents;" ::: "memory"));
177
+ // d_dv: the previous kernel's output (after the wait), through L2 only. 16-byte vectors when hd % 4 == 0 (c0, cc are
178
+ // multiples of 4: a vector is entirely valid or entirely past hd); otherwise plain L2 loads.
179
+ if ((hd & 3) == 0 && (reinterpret_cast<uintptr_t>(dv) & 15u) == 0) {
180
+ #pragma unroll 4
181
+ for (int v = threadIdx.x; v < C * (TC / 4); v += NT) {
182
+ const int j = v / (TC / 4), cc = 4 * (v - j * (TC / 4));
183
+ const bool ok = c0 + cc < hd;
184
+ cp16(ds + j * TC + cc, ok ? dvn + static_cast<int64_t>(j) * hd + c0 + cc : dv, ok ? 16 : 0);
185
+ }
186
+ } else {
187
+ for (int e = threadIdx.x; e < C * TC; e += NT) {
188
+ const int j = e / TC, cc = e - j * TC;
189
+ ds[e] = (c0 + cc < hd) ? __ldcg(dvn + static_cast<int64_t>(j) * hd + c0 + cc) : 0.f;
190
+ }
191
+ }
192
+ cp_wait_all(); // this thread's copies landed; the barrier publishes
193
+ __syncthreads(); // every thread's copies and stores to the CTA
194
+ float acc[RI][RC];
195
+ #pragma unroll
196
+ for (int r = 0; r < RI; ++r) {
197
+ #pragma unroll
198
+ for (int q = 0; q < RC; ++q) acc[r][q] = 0.f;
199
+ }
200
+ // The fixed chain: j = 0, 1, .., C - 1 for every element (rows ty + TY * r, columns tx * RC + q of the tile).
201
+ #pragma unroll 4
202
+ for (int j = 0; j < C; ++j) {
203
+ const float4 d4 = *reinterpret_cast<const float4*>(ds + j * TC + tx * RC);
204
+ #pragma unroll
205
+ for (int r = 0; r < RI; ++r) {
206
+ const float xv = xs[j * TI + ty + TY * r];
207
+ acc[r][0] = fmaf(xv, d4.x, acc[r][0]);
208
+ acc[r][1] = fmaf(xv, d4.y, acc[r][1]);
209
+ acc[r][2] = fmaf(xv, d4.z, acc[r][2]);
210
+ acc[r][3] = fmaf(xv, d4.w, acc[r][3]);
211
+ }
212
+ }
213
+ #pragma unroll
214
+ for (int r = 0; r < RI; ++r) {
215
+ const int i = i0 + ty + TY * r;
216
+ if (i >= C) continue;
217
+ {
218
+ const int c = c0 + tx * RC;
219
+ if (c >= hd) continue; // the thread's 4 columns are all past hd
220
+ float* o = dun + static_cast<int64_t>(i) * hd + c;
221
+ const float4 v = make_float4(u0[r].x + acc[r][0], u0[r].y + acc[r][1], u0[r].z + acc[r][2],
222
+ u0[r].w + acc[r][3]); // single rounding of each f32 sum
223
+ if (vec_u) {
224
+ *reinterpret_cast<float4*>(o) = v;
225
+ } else {
226
+ o[0] = v.x;
227
+ if (c + 1 < hd) o[1] = v.y;
228
+ if (c + 2 < hd) o[2] = v.z;
229
+ if (c + 3 < hd) o[3] = v.w;
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ using GemmK = void (*)(float*, const float*, const float*, int, int, int, int, int, int);
236
+
237
+ void set_smem(GemmK kern, size_t bytes) { // cudaFuncSetAttribute is per (kernel, device)
238
+ static std::mutex mu;
239
+ static std::map<std::pair<const void*, int>, size_t> done;
240
+ int dev = 0;
241
+ C10_CUDA_CHECK(cudaGetDevice(&dev));
242
+ std::lock_guard<std::mutex> lk(mu);
243
+ const auto key = std::make_pair(reinterpret_cast<const void*>(kern), dev);
244
+ auto it = done.find(key);
245
+ if (it != done.end() && it->second >= bytes) return;
246
+ C10_CUDA_CHECK(cudaFuncSetAttribute(kern, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast<int>(bytes)));
247
+ done[key] = bytes;
248
+ }
249
+
250
+ GemmK pick(int ti_rows) {
251
+ switch (ti_rows) {
252
+ case 16: return &gx_gemm_kernel<16, true>;
253
+ case 32: return &gx_gemm_kernel<32, true>;
254
+ case 48: return &gx_gemm_kernel<48, true>;
255
+ default: return &gx_gemm_kernel<64, true>;
256
+ }
257
+ }
258
+
259
+ } // namespace
260
+
261
+ void gx_gemm(at::Tensor du, at::Tensor x, at::Tensor dv, int64_t ti, int64_t P, int64_t pdl, int64_t ti_rows,
262
+ int64_t smem_min) {
263
+ const at::cuda::CUDAGuard guard(du.device());
264
+ TORCH_CHECK(ti_rows == 16 || ti_rows == 32 || ti_rows == 48 || ti_rows == 64, "gx_gemm: rows per CTA 16, 32, 48, 64");
265
+ const int TI = static_cast<int>(ti_rows);
266
+ const GemmK kern = pick(TI);
267
+ TORCH_CHECK(du.scalar_type() == at::kFloat && x.scalar_type() == at::kFloat && dv.scalar_type() == at::kFloat,
268
+ "gx_gemm: f32 d_u, x, d_dv");
269
+ TORCH_CHECK(du.is_contiguous() && x.is_contiguous() && dv.is_contiguous(), "gx_gemm: contiguous operands");
270
+ // Raw pointers read the storage: refuse lazy negative / conjugate views (materialized values only).
271
+ TORCH_CHECK(!du.is_neg() && !x.is_neg() && !dv.is_neg() && !du.is_conj() && !x.is_conj() && !dv.is_conj(),
272
+ "gx_gemm: lazy negative / conjugate views are not supported (materialize them first)");
273
+ TORCH_CHECK(x.dim() == 3 && x.size(1) == x.size(2) && du.dim() == 3 && du.size(0) == x.size(0) &&
274
+ du.size(1) == x.size(1), "gx_gemm: x [N, C, C], d_u [N, C, hd]");
275
+ const int64_t N = x.size(0), C = x.size(1), hd = du.size(2);
276
+ TORCH_CHECK(C >= 1 && C <= 256 && hd >= 1 && hd < (int64_t(1) << 20), "gx_gemm: 1 <= C <= 256, hd");
277
+ TORCH_CHECK(dv.numel() == du.numel(), "gx_gemm: d_dv [N * C, hd] like d_u");
278
+ // Bounds by division before any product (no signed overflow for any int64 ti / P).
279
+ TORCH_CHECK(N >= 1 && N < (int64_t(1) << 31) && P >= 1 && P <= N && ti >= 0 && ti < N / P,
280
+ "gx_gemm: step out of range (need 1 <= P <= N < 2^31 and 0 <= ti < N / P)");
281
+ TORCH_CHECK(x.device() == du.device() && dv.device() == du.device(), "gx_gemm: operands on one device");
282
+ // d_u (written in place) must not overlap the operands it is computed from.
283
+ {
284
+ auto lo_hi = [](const at::Tensor& t) {
285
+ const uintptr_t a = reinterpret_cast<uintptr_t>(t.data_ptr());
286
+ return std::make_pair(a, a + static_cast<uintptr_t>(t.numel()) * sizeof(float));
287
+ };
288
+ const auto u = lo_hi(du), xr = lo_hi(x), vr = lo_hi(dv);
289
+ TORCH_CHECK(!(u.first < xr.second && xr.first < u.second) && !(u.first < vr.second && vr.first < u.second),
290
+ "gx_gemm: d_u overlaps x or d_dv (in-place update needs disjoint operands)");
291
+ }
292
+ TORCH_CHECK(reinterpret_cast<uintptr_t>(du.data_ptr()) % 4 == 0 && reinterpret_cast<uintptr_t>(x.data_ptr()) % 4 == 0 &&
293
+ reinterpret_cast<uintptr_t>(dv.data_ptr()) % 4 == 0, "gx_gemm: 4-byte aligned operands");
294
+ const int nib = static_cast<int>((C + TI - 1) / TI), ncb = static_cast<int>((hd + TC - 1) / TC);
295
+ const int64_t blocks64 = P * static_cast<int64_t>(nib) * ncb;
296
+ TORCH_CHECK(blocks64 <= 0x7FFFFFFF, "gx_gemm: grid");
297
+ const int blocks = static_cast<int>(blocks64);
298
+ TORCH_CHECK(smem_min >= 0 && smem_min <= (int64_t(1) << 20), "gx_gemm: smem_min");
299
+ const size_t smem = std::max(static_cast<size_t>(C) * (TI + TC) * sizeof(float), static_cast<size_t>(smem_min));
300
+ int dev = 0, optin = 0;
301
+ C10_CUDA_CHECK(cudaGetDevice(&dev));
302
+ C10_CUDA_CHECK(cudaDeviceGetAttribute(&optin, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev));
303
+ TORCH_CHECK(smem <= static_cast<size_t>(optin), "gx_gemm: ", smem, " bytes of shared memory exceed ", optin);
304
+ if (blocks == 0) return;
305
+ set_smem(kern, smem);
306
+ auto st = at::cuda::getCurrentCUDAStream();
307
+ float* dup = du.data_ptr<float>();
308
+ const float* xp = x.data_ptr<float>();
309
+ const float* dvp = dv.data_ptr<float>();
310
+ const int ti_ = static_cast<int>(ti), P_ = static_cast<int>(P), C_ = static_cast<int>(C), hd_ = static_cast<int>(hd);
311
+ // plan capture: records this launch; gx_gemm_plan_launch issues it per step, plain or PDL as below
312
+ if (sdm_plancap::take(kern, dim3(blocks), dim3(NT), smem, dup, xp, dvp, ti_, P_, C_, hd_, nib, ncb)) return;
313
+ if (pdl) {
314
+ cudaLaunchConfig_t cfg = {};
315
+ cfg.gridDim = dim3(blocks);
316
+ cfg.blockDim = dim3(NT);
317
+ cfg.dynamicSmemBytes = smem;
318
+ cfg.stream = st;
319
+ cudaLaunchAttribute attr[1];
320
+ attr[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
321
+ attr[0].val.programmaticStreamSerializationAllowed = 1;
322
+ cfg.attrs = attr;
323
+ cfg.numAttrs = 1;
324
+ C10_CUDA_CHECK(cudaLaunchKernelEx(&cfg, kern, dup, xp, dvp, ti_, P_, C_, hd_, nib, ncb));
325
+ return;
326
+ }
327
+ kern<<<blocks, NT, smem, st>>>(dup, xp, dvp, ti_, P_, C_, hd_, nib, ncb);
328
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
329
+ }
330
+
331
+ void gx_gemm_attrs(at::Tensor out) { // [4] int64 CPU: regs, local bytes, static smem, max threads
332
+ TORCH_CHECK(out.device().is_cpu() && out.scalar_type() == at::kLong && out.numel() == 4, "gx_gemm_attrs: int64 [4]");
333
+ cudaFuncAttributes a{};
334
+ C10_CUDA_CHECK(cudaFuncGetAttributes(&a, pick(GX_GEMM_TI)));
335
+ auto o = out.accessor<int64_t, 1>();
336
+ o[0] = a.numRegs;
337
+ o[1] = static_cast<int64_t>(a.localSizeBytes);
338
+ o[2] = static_cast<int64_t>(a.sharedSizeBytes);
339
+ o[3] = a.maxThreadsPerBlock;
340
+ }
341
+
342
+ // ---- the backward's C++ step loop (bwd_steps_cuda): gx_gemm as a per-backward plan ------------------------------------
343
+ // gx_gemm_plan runs gx_gemm ONCE under plan capture at step ti_max (every check runs; its step bound ti < N / P covers
344
+ // every ti <= ti_max) and records the launch. gx_gemm_plan_launch (a plain C++ entry point; address from
345
+ // gx_gemm_launch_addr) sets the step parameter ti and issues the recorded launch: with programmatic stream serialization
346
+ // when pdl != 0, else plain -- gx_gemm's choice for the same pdl. The plan holds raw pointers into d_u, x and d_dv.
347
+ namespace {
348
+ constexpr uint64_t kGemmPlanMagic = 0x314e414c504d4547ull;
349
+ constexpr int kGemmTiParam = 3; // (du, x, dv, ti, P, C, hd, nib, ncb)
350
+ struct GemmPlan {
351
+ uint64_t magic;
352
+ int32_t device;
353
+ int32_t pad;
354
+ int64_t ti_max;
355
+ sdm_plancap::Launch cap;
356
+ };
357
+
358
+ int64_t gx_gemm_plan_launch(const void* plan_bytes, int64_t nbytes, int64_t ti, int64_t pdl, void* stream) {
359
+ GemmPlan pl;
360
+ if (!sdm_plancap::from_bytes(pl, plan_bytes, nbytes, kGemmPlanMagic)) return -1;
361
+ if (ti < 0 || ti > pl.ti_max) return -2;
362
+ if (pl.cap.set == 0) return 0; // gx_gemm launched nothing (empty grid)
363
+ if (sdm_plancap::current_device() != pl.device) return -5;
364
+ int* tip = sdm_plancap::param<int>(pl.cap, kGemmTiParam);
365
+ if (tip == nullptr) return -7;
366
+ *tip = static_cast<int>(ti);
367
+ return sdm_plancap::launch(pl.cap, pdl != 0, static_cast<cudaStream_t>(stream));
368
+ }
369
+ } // namespace
370
+
371
+ at::Tensor gx_gemm_plan(at::Tensor du, at::Tensor x, at::Tensor dv, int64_t ti_max, int64_t P, int64_t ti_rows,
372
+ int64_t smem_min) {
373
+ TORCH_CHECK(ti_max >= 0 && ti_max < (int64_t(1) << 31), "gx_gemm_plan: ti_max");
374
+ GemmPlan pl;
375
+ std::memset(&pl, 0, sizeof(pl));
376
+ {
377
+ sdm_plancap::Scope scope(&pl.cap);
378
+ gx_gemm(du, x, dv, ti_max, P, 0, ti_rows, smem_min);
379
+ }
380
+ pl.magic = kGemmPlanMagic;
381
+ pl.device = static_cast<int32_t>(du.get_device());
382
+ pl.ti_max = ti_max;
383
+ TORCH_CHECK(pl.cap.set == 0 || (pl.cap.nparams == 9 && sdm_plancap::param<int>(pl.cap, kGemmTiParam) != nullptr),
384
+ "gx_gemm_plan: parameters");
385
+ return sdm_plancap::to_tensor(pl);
386
+ }
387
+
388
+ int64_t gx_gemm_launch_addr() { return reinterpret_cast<int64_t>(&gx_gemm_plan_launch); }
389
+ """
390
+
391
+ def _one_wave_ctas(device=None) -> int:
392
+ """CTAs of one wave at the default one-CTA-per-SM cap: the SM count (GB300: 152)."""
393
+ return num_sms(device)
394
+
395
+
396
+ def _grid_target(device=None) -> int:
397
+ """CTAs per launch the rows rule aims at: the SM count rounded up to whole 32-CTA groups (GB300: 160)."""
398
+ return 32 * -(-num_sms(device) // 32)
399
+
400
+
401
+ def one_wave(P: int, C: int, hd: int, device=None) -> bool:
402
+ """Speed rule (shapes only): the launch grid at the rows rule's rows per CTA fits one wave of _one_wave_ctas(device)
403
+ CTAs at the default one-CTA-per-SM cap. On multi-wave grids the kernel would serialize waves of single CTAs per SM,
404
+ while cuBLAS packs several."""
405
+ ti = _rows(P, C, hd, device)
406
+ return P * (-(-C // ti)) * (-(-hd // 64)) <= _one_wave_ctas(device)
407
+
408
+
409
+ def _rows(P: int = 1, C: int = 64, hd: int = 1024, device=None) -> int:
410
+ """Rows per CTA (outputs do not depend on it): the smallest of 16, 32, 48, 64 whose grid P * ceil(C / TI) *
411
+ ceil(hd / 64) has at most _grid_target(device) CTAs, else 64."""
412
+ target = _grid_target(device)
413
+ for ti in (16, 32, 48):
414
+ if P * (-(-C // ti)) * (-(-hd // 64)) <= target:
415
+ return ti
416
+ return 64
417
+
418
+
419
+ def _cap() -> int:
420
+ """Max CTAs of this kernel per SM (1) under PDL, enforced by padding the dynamic shared memory request to more than
421
+ 1 / (cap + 1) of the SM's shared memory (_device.smem_per_sm: 228 KB on H100 / B200 / GB300; the padding is never
422
+ read): a PDL launch behind a kernel that is still finishing then spreads its CTAs one per SM instead of packing up
423
+ to 8 onto the few SMs the predecessor's last wave frees. Clamped to the device's opt-in shared memory per block.
424
+ Plain launches (no PDL: the portable backends) are not padded. Outputs do not depend on it."""
425
+ return 1
426
+
427
+
428
+ @functools.cache
429
+ def _module():
430
+ return load_cuda_extension(
431
+ "sdm_gx_gemm",
432
+ cpp_sources=_CPP,
433
+ cuda_sources=_CUDA,
434
+ functions=("gx_gemm", "gx_gemm_attrs", "gx_gemm_plan", "gx_gemm_launch_addr"),
435
+ headers=headers(),
436
+ extra_cuda_cflags=("-O3", "-lineinfo"),
437
+ )
438
+
439
+
440
+ def supported(d_u, x, d_dv, chunk_size: int) -> bool:
441
+ """The backward's check, once per backward (shapes, dtypes, layout only; the policy is the caller's): f32 contiguous
442
+ d_u [N, C, hd], x [N, C, C] and d_dv (N * C * hd elements) on one CUDA device, 1 <= C <= 256, and the kernel's shared
443
+ memory (C * (TI + 64) * 4 bytes) within the device's opt-in limit."""
444
+ ts = (d_u, x, d_dv)
445
+ if not all(torch.is_tensor(t) and t.is_cuda and t.dtype == torch.float32 and t.is_contiguous() for t in ts):
446
+ return False
447
+ if any(t.is_neg() or t.is_conj() for t in ts): # the kernel reads storage (no lazy views)
448
+ return False
449
+ if len({t.device for t in ts}) != 1 or x.dim() != 3 or d_u.dim() != 3:
450
+ return False
451
+ if not cuda_backend(d_u.device): # cp.async: sm_80+ (PDL on the modern backend only)
452
+ return False
453
+ if any(t.data_ptr() % 4 for t in ts): # the native float-alignment check, mirrored
454
+ return False
455
+ N, C = x.shape[0], x.shape[1]
456
+ if not 1 <= N < 2 ** 31: # no empty step set (make_step / the native entry need N >= 1)
457
+ return False
458
+ if x.shape[2] != C or C != chunk_size or not 1 <= C <= 256 or d_u.shape[0] != N or d_u.shape[1] != C:
459
+ return False
460
+ hd = d_u.shape[2]
461
+ if not 1 <= hd < 2 ** 20 or d_dv.numel() != d_u.numel() or N >= 2 ** 31:
462
+ return False
463
+ if _overlaps(d_u, x) or _overlaps(d_u, d_dv): # the in-place update needs disjoint operands
464
+ return False
465
+ # the launch grid (P * ceil(C / TI) * ceil(hd / 64) CTAs, P <= N) must fit the native int limit for any
466
+ # TI (the largest grid: TI 16), and the shared memory C * (TI + 64) * 4 bytes the opt-in limit for any TI (64).
467
+ if N * (-(-C // 16)) * (-(-hd // 64)) > 0x7FFFFFFF:
468
+ return False
469
+ return C * (64 + 64) * 4 <= smem_cap(d_u.device)
470
+
471
+
472
+ def _overlaps(a, b) -> bool:
473
+ a0, b0 = a.data_ptr(), b.data_ptr()
474
+ return a0 < b0 + b.numel() * b.element_size() and b0 < a0 + a.numel() * a.element_size()
475
+
476
+
477
+ PDL_LAUNCHES = [0, 0] # launches without / with programmatic dependent launch since import (tests, diagnostics)
478
+ LAST_GEOM = {} # the last production make_step's launch geometry (tests, diagnostics)
479
+ PROBE_LAUNCHES = [0, 0] # the qualification probe's launches (kept out of PDL_LAUNCHES)
480
+ _DEV = {} # device index -> (compute capability, name): read once (the key is computed per backward)
481
+
482
+ _QUAL = {} # qualification key -> bool (this kernel == the production cuBLAS call, bitwise)
483
+ _QUAL_ERR = {} # key -> exceptions seen (transient failures are retried up to 3 times, then cached False)
484
+ _QUAL_LOCK = threading.Lock()
485
+ QUAL_STATS = {"lookups": 0, "probes": 0, "equal": 0, "differ": 0, "errors": 0, "capture_skips": 0, "declined_key": 0,
486
+ "declined_shape": 0, "key_s": [], "lock_s": [], "build_s": [], "probe_s": [], "failed_probe_s": []}
487
+ _ALIGN = 256 # pointer alignment classes kept distinct in the key and replicated exactly by the probe
488
+ _LIB = {}
489
+
490
+
491
+ def _blas_libs():
492
+ """(libcublas path, version, libcublasLt path, Lt version) of the libraries this process actually loaded (from
493
+ /proc/self/maps: exactly one distinct path of each, else None; versions via cublasGetProperty / cublasLtGetVersion),
494
+ plus the libcublas ctypes handle for the native getters. Cached per process; anything unreadable -> None."""
495
+ if "v" in _LIB:
496
+ return _LIB["v"]
497
+ out = None
498
+ try:
499
+ import ctypes
500
+ torch.cuda.current_blas_handle() # cuBLAS is initialized (and its libraries mapped) from here on
501
+ blas, lt = set(), set()
502
+ with open("/proc/self/maps") as f:
503
+ for ln in f:
504
+ parts = ln.split()
505
+ if len(parts) >= 6:
506
+ base = os.path.basename(parts[5])
507
+ if base.startswith("libcublasLt.so"):
508
+ lt.add(os.path.realpath(parts[5]))
509
+ elif base.startswith("libcublas.so"):
510
+ blas.add(os.path.realpath(parts[5]))
511
+ if len(blas) == 1 and len(lt) <= 1: # an ambiguous library set -> no key
512
+ bpath = next(iter(blas))
513
+ lib = ctypes.CDLL(bpath)
514
+ ver = []
515
+ for prop in (0, 1, 2): # MAJOR_VERSION, MINOR_VERSION, PATCH_LEVEL
516
+ v = ctypes.c_int(-1)
517
+ if lib.cublasGetProperty(ctypes.c_int(prop), ctypes.byref(v)) != 0 or v.value < 0:
518
+ raise RuntimeError("cublasGetProperty")
519
+ ver.append(v.value)
520
+ ltpath, ltver = "", -1
521
+ if lt:
522
+ ltpath = next(iter(lt))
523
+ ltlib = ctypes.CDLL(ltpath)
524
+ ltlib.cublasLtGetVersion.restype = ctypes.c_size_t
525
+ ltver = int(ltlib.cublasLtGetVersion())
526
+ out = (bpath, tuple(ver), ltpath, ltver, lib)
527
+ except Exception: # noqa: BLE001
528
+ out = None
529
+ _LIB["v"] = out
530
+ return out
531
+
532
+
533
+ # Native cuBLAS handle state that can change the algorithm cuBLAS picks or its arithmetic: read with the
534
+ # library's getters on the handle torch uses for the current stream. A getter the loaded library does not export is
535
+ # recorded as absent (that feature does not exist there); one that exists but fails -> no key (decline).
536
+ _NATIVE_GETTERS = ("cublasGetMathMode", "cublasGetAtomicsMode", "cublasGetPointerMode_v2", "cublasGetSmCountTarget",
537
+ "cublasGetEmulationStrategy", "cublasGetEmulationSpecialValuesSupport",
538
+ "cublasGetFixedPointEmulationMantissaControl", "cublasGetFixedPointEmulationMaxMantissaBitCount",
539
+ "cublasGetFixedPointEmulationMantissaBitOffset")
540
+
541
+
542
+ def _native_state(lib):
543
+ """(handle pointer, {getter: value | 'absent'}, cuBLAS workspace bytes, cuBLASLt workspace bytes) of the current
544
+ stream's torch cuBLAS handle, or None if anything is unreadable."""
545
+ try:
546
+ import ctypes
547
+ h = torch.cuda.current_blas_handle()
548
+ vals = []
549
+ for fn in _NATIVE_GETTERS:
550
+ f = getattr(lib, fn, None)
551
+ if f is None:
552
+ vals.append((fn, "absent"))
553
+ continue
554
+ v = ctypes.c_int64(0) # every getter writes a 32-bit enum / int (zero-extended here)
555
+ if f(ctypes.c_void_p(h), ctypes.byref(v)) != 0:
556
+ return None
557
+ vals.append((fn, int(v.value) & 0xFFFFFFFF))
558
+ ws = getattr(torch._C, "_cuda_getCublasWorkspaceSize", None)
559
+ wsl = getattr(torch._C, "_cuda_getCublasLtWorkspaceSize", None)
560
+ if ws is None or wsl is None: # the effective workspace policy is unreadable -> decline
561
+ return None
562
+ return (int(h), tuple(vals), int(ws()), int(wsl()))
563
+ except Exception: # noqa: BLE001
564
+ return None
565
+
566
+
567
+ # The environment variables that can change the production GEMM's algorithm or bits, read once (torch / cuBLAS
568
+ # read them once per process). Read sites: torch 2.13.0 sources; cuBLAS: its documentation.
569
+ _BLAS_ENV = (
570
+ "CUBLAS_WORKSPACE_CONFIG", # torch CublasHandlePool.cpp:313 (parseChosenWorkspaceSize), kept in
571
+ # getChosenWorkspaceSize's function-local static (:422); cuBLAS: handle creation
572
+ "CUBLASLT_WORKSPACE_SIZE", # torch CublasHandlePool.cpp:383, kept in getCUDABlasLtWorkspaceSize's static (:448)
573
+ "TORCH_CUBLASLT_UNIFIED_WORKSPACE", # torch CublasHandlePool.cpp:358 / :372 (function-local static)
574
+ "TORCH_CUBLAS_WORKSPACE_CACHE", # torch CublasHandlePool.cpp:359 / :362-363 (function-local static)
575
+ "TORCH_BLAS_PREFER_CUBLASLT", # torch aten/src/ATen/Context.h:547-555 (at::Context member initializer: once)
576
+ "TORCH_BLAS_PREFER_HIPBLASLT", # torch Context.h:549 / :552 (the alias, same initializer)
577
+ "DISABLE_ADDMM_CUDA_LT", # torch aten/src/ATen/native/cuda/Blas.cpp:177 (function-local static)
578
+ "NVIDIA_TF32_OVERRIDE", # cuBLAS / CUDA math libraries: TF32 off, read at library initialization
579
+ "CUBLAS_EMULATE_SINGLE_PRECISION", # cuBLAS 12.9+: FP32 emulation, read at handle creation (torch pools handles)
580
+ "CUBLAS_EMULATION_STRATEGY", # cuBLAS 12.9+: the emulation strategy, read at handle creation
581
+ )
582
+ # Deliberately not read: cuBLAS's logging-only variables (CUBLAS_LOGINFO_DBG, CUBLAS_LOGDEST_DBG, CUBLASLT_LOG_*), which
583
+ # change no algorithm and no bit.
584
+ _BLAS_ENV_VALUES = None # _BLAS_ENV's (name, value or None) pairs, read once (_blas_env)
585
+ _BLAS_ENV_LOCK = threading.Lock()
586
+
587
+
588
+ def _blas_env():
589
+ """_BLAS_ENV's values, read once per process (torch and cuBLAS read them only at initialization)."""
590
+ global _BLAS_ENV_VALUES
591
+ vals = _BLAS_ENV_VALUES
592
+ if vals is None:
593
+ with _BLAS_ENV_LOCK:
594
+ if _BLAS_ENV_VALUES is None:
595
+ _BLAS_ENV_VALUES = tuple((k, os.environ.get(k)) for k in _BLAS_ENV)
596
+ vals = _BLAS_ENV_VALUES
597
+ return vals
598
+
599
+
600
+ def _blas_settings():
601
+ """Everything torch / cuBLAS read when choosing and running the production GEMM that this process can change, as a
602
+ hashable tuple; None when any of it is unreadable (the caller then declines: any doubt -> cuBLAS). The environment
603
+ part is _BLAS_ENV's values as read once (_blas_env): no environment read per call."""
604
+ try:
605
+ new = getattr(torch.backends.cuda.matmul, "fp32_precision", None)
606
+ env = _blas_env()
607
+ return (bool(torch._C._get_cublas_allow_tf32()), torch.get_float32_matmul_precision(), str(new),
608
+ str(torch.backends.cuda.preferred_blas_library()),
609
+ bool(torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction),
610
+ bool(torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction),
611
+ torch.are_deterministic_algorithms_enabled(), env)
612
+ except Exception: # noqa: BLE001 (unreadable state -> decline)
613
+ return None
614
+
615
+
616
+ def _align_classes(d_u, x, d_dv, P, steps):
617
+ """The distinct (d_u, x, d_dv) step-pointer residues modulo _ALIGN over all steps (each operand's residue sequence
618
+ repeats with period <= _ALIGN / 4, so 64 steps cover every combination)."""
619
+ C, hd = x.shape[1], d_u.shape[2]
620
+ bu, bx, bv = d_u.data_ptr(), x.data_ptr(), d_dv.data_ptr()
621
+ su, sx = P * C * hd * 4, P * C * C * 4
622
+ return tuple(sorted({((bu + ti * su) % _ALIGN, (bx + ti * sx) % _ALIGN, (bv + ti * su) % _ALIGN)
623
+ for ti in range(min(steps, _ALIGN // 4))}))
624
+
625
+
626
+ def _qual_key(d_u, x, d_dv, P):
627
+ """The qualification key: device (index, compute capability, name), the routing profile and code image, the loaded
628
+ cuBLAS / cuBLASLt (paths + versions), torch and CUDA runtime versions, the production call's shapes, dtypes, strides
629
+ (step views) and pointer alignment classes, every precision / BLAS setting (_blas_settings) and the native handle
630
+ state (_native_state: torch's handle for the current stream, its math / atomics / pointer modes, SM-count target,
631
+ FP32-emulation settings, and the effective cuBLAS / cuBLASLt workspace sizes). None if any part is unreadable."""
632
+ libs = _blas_libs()
633
+ st = _blas_settings()
634
+ if libs is None or st is None:
635
+ return None
636
+ nat = _native_state(libs[4])
637
+ if nat is None:
638
+ return None
639
+ dev = d_u.device
640
+ idx = dev.index if dev.index is not None else torch.cuda.current_device()
641
+ N, C, hd = x.shape[0], x.shape[1], d_u.shape[2]
642
+ steps = N // P
643
+ x_t = x.transpose(1, 2)
644
+ views = (tuple(d_u.stride()), tuple(x_t.stride()), tuple(d_dv.view(N, C, hd).stride()))
645
+ dv = _DEV.get(idx)
646
+ if dv is None:
647
+ dv = _DEV.setdefault(idx, (torch.cuda.get_device_capability(idx), torch.cuda.get_device_name(idx)))
648
+ # LAST field: the routing profile and the code image's build inputs (a qualification never crosses either)
649
+ arch = _target_architectures()
650
+ prof = (_device_capability(idx), backend(idx), arch["TORCH_CUDA_ARCH_LIST"], tuple(map(tuple, arch["capabilities"])))
651
+ return (idx, dv[0], dv[1], libs[:4], torch.__version__,
652
+ torch.version.cuda, int(P), int(C), int(hd), (d_u.dtype, x.dtype, d_dv.dtype), views,
653
+ _align_classes(d_u, x, d_dv, P, steps), st, nat, prof)
654
+
655
+
656
+ def _probe_inputs(P, C, hd, dev, pattern, seed):
657
+ """One step's operands (x [P, C, C], d_dv [P * C, hd], d_u [P, C, hd]) on the CPU, deterministic, in one of the
658
+ qualification patterns: normal (with signed zeros), zeros, large magnitudes, denormals, cancellation, specials."""
659
+ g = torch.Generator().manual_seed(seed)
660
+ x = torch.randn(P, C, C, generator=g) * 0.25
661
+ dv = torch.randn(P * C, hd, generator=g)
662
+ du = torch.randn(P, C, hd, generator=g)
663
+ if pattern == "normal":
664
+ x.view(-1)[::7] = -0.0
665
+ dv.view(-1)[::11] = 0.0
666
+ du.view(-1)[::13] = -0.0
667
+ elif pattern == "zeros":
668
+ x.mul_((torch.rand(P, C, C, generator=g) < 0.5).float())
669
+ x[x == 0] = -0.0
670
+ dv.mul_((torch.rand(P * C, hd, generator=g) < 0.5).float())
671
+ du[torch.rand(P, C, hd, generator=g) < 0.5] = -0.0
672
+ elif pattern == "large":
673
+ for t in (x, dv, du):
674
+ t.mul_(torch.exp2(torch.randint(-60, 61, t.shape, generator=g).float()))
675
+ x.view(-1)[::17] = 3.0e38
676
+ dv.view(-1)[::19] = -3.0e38
677
+ elif pattern == "denormal":
678
+ x.mul_(1e-20)
679
+ dv.mul_(1e-19)
680
+ du.mul_(1e-38)
681
+ du.view(-1)[::5] = 1e-41
682
+ elif pattern == "cancel":
683
+ x.copy_(torch.sign(torch.randn(P, C, C, generator=g)) * (1.0 + torch.rand(P, C, C, generator=g)))
684
+ dv.mul_(1e4)
685
+ v3 = dv.view(P, C, hd)
686
+ h = C // 2
687
+ if h:
688
+ v3[:, 1:2 * h:2, :] = -v3[:, 0:2 * h:2, :] * (1.0 + 1e-3 * torch.randn(P, h, hd, generator=g))
689
+ du.mul_(1e-3)
690
+ elif pattern == "special":
691
+ pats = torch.tensor([float("nan"), float("inf"), -float("inf"), -0.0, 1e-41, 3.4e38, -3.4e38])
692
+ for t, k in ((dv, 64), (x, 16), (du, 32)):
693
+ fl = t.view(-1)
694
+ k = min(k, fl.numel())
695
+ fl[torch.randperm(fl.numel(), generator=g)[:k]] = pats[torch.arange(k) % pats.numel()]
696
+ return x.to(dev), dv.to(dev), du.to(dev)
697
+
698
+
699
+ PATTERNS = ("normal", "zeros", "large", "denormal", "cancel", "special")
700
+
701
+
702
+ def _at(n, res, dev):
703
+ """A contiguous float32 view of n elements whose data pointer is `res` (mod _ALIGN) past an _ALIGN boundary."""
704
+ buf = torch.empty(n + _ALIGN // 4 * 2, device=dev, dtype=torch.float32)
705
+ off = ((res - buf.data_ptr()) % _ALIGN) // 4
706
+ return buf[off:off + n]
707
+
708
+
709
+ def _probe(P, C, hd, dev, classes):
710
+ """True iff, for every alignment class and every pattern, this kernel and the production cuBLAS call
711
+ (torch.baddbmm(u, x_t, d_dv, out=u) on [P, C, *] step views: u, d_dv contiguous, x_t = x.transpose(1, 2)) give
712
+ bitwise identical results, under the current settings."""
713
+ seed = 20261020 + C * 131 + hd
714
+ for ru, rx, rv in classes:
715
+ for k, pat in enumerate(PATTERNS):
716
+ x0, dv0, du0 = _probe_inputs(P, C, hd, dev, pat, seed + 7919 * k)
717
+ x = _at(P * C * C, rx, dev).view(P, C, C)
718
+ dv = _at(P * C * hd, rv, dev).view(P * C, hd)
719
+ ref = _at(P * C * hd, ru, dev).view(P, C, hd)
720
+ out = _at(P * C * hd, ru, dev).view(P, C, hd)
721
+ x.copy_(x0)
722
+ dv.copy_(dv0)
723
+ ref.copy_(du0)
724
+ out.copy_(du0)
725
+ torch.baddbmm(ref, x.transpose(1, 2), dv.view(P, C, hd), out=ref)
726
+ make_step(out, x, dv, P, _probe_counts=PROBE_LAUNCHES)(0, pdl=False)
727
+ if not torch.equal(out.view(torch.int32), ref.view(torch.int32)):
728
+ return False
729
+ return True
730
+
731
+
732
+ def matches_cublas(d_u, x, d_dv, num_parallel: int) -> bool:
733
+ """Run-time qualification: True iff this kernel reproduces the backward's production cuBLAS call bit for bit for THIS
734
+ backward's operands, as far as anything that can change cuBLAS's algorithm or its arithmetic is concerned. The
735
+ verdict is cached per key (_qual_key: device / arch / name, loaded cuBLAS and cuBLASLt paths + versions, torch /
736
+ CUDA versions, P, C, hd, dtypes, step-view strides, step-pointer alignment classes mod 256, every precision / BLAS
737
+ setting incl. the cuBLAS workspace variables, the native state of torch's cuBLAS handle and the effective workspace
738
+ sizes). A miss runs _probe: every alignment class of this backward replicated exactly, 6 input patterns (normal with
739
+ signed zeros, zeros, large magnitudes, denormals, cancellation, Inf / NaN), every output bit compared. Any doubt ->
740
+ False (cuBLAS): unreadable key, probe exception, a key change during the probe, a capture in progress on a miss.
741
+ Everything runs on the OPERANDS' device (torch.cuda.device(d_u.device)): the native handle state read, the capture
742
+ check, the probe and the re-read concern the device the production call runs on, whatever the ambient current
743
+ device. The lookup runs once per backward (in its factory), never per step. First the speed rule (one_wave, shapes
744
+ only): a multi-wave shape returns False at once (no key, no probe), so the backward keeps cuBLAS there."""
745
+ try:
746
+ P0, C0, hd0 = int(num_parallel), int(x.shape[1]), int(d_u.shape[2])
747
+ fast = one_wave(P0, C0, hd0, d_u.device)
748
+ except Exception: # noqa: BLE001
749
+ fast = False
750
+ if not fast:
751
+ with _QUAL_LOCK:
752
+ QUAL_STATS["lookups"] += 1
753
+ QUAL_STATS["declined_shape"] += 1
754
+ return False
755
+ t_key = time.perf_counter()
756
+ try:
757
+ dev_ctx = torch.cuda.device(d_u.device)
758
+ except Exception: # noqa: BLE001
759
+ return False
760
+ with dev_ctx:
761
+ try:
762
+ P = int(num_parallel)
763
+ key = _qual_key(d_u, x, d_dv, P)
764
+ except Exception: # noqa: BLE001
765
+ key = None
766
+ t_key = time.perf_counter() - t_key
767
+ t_lock = time.perf_counter()
768
+ with _QUAL_LOCK:
769
+ t_lock = time.perf_counter() - t_lock
770
+ QUAL_STATS["lookups"] += 1
771
+ if len(QUAL_STATS["key_s"]) < 64:
772
+ QUAL_STATS["key_s"].append(round(t_key, 6))
773
+ QUAL_STATS["lock_s"].append(round(t_lock, 6))
774
+ if key is None:
775
+ QUAL_STATS["declined_key"] += 1
776
+ return False
777
+ hit = _QUAL.get(key)
778
+ if hit is not None:
779
+ return hit
780
+ try:
781
+ capturing = torch.cuda.is_current_stream_capturing()
782
+ except Exception: # noqa: BLE001
783
+ capturing = True
784
+ if capturing:
785
+ QUAL_STATS["capture_skips"] += 1
786
+ return False
787
+ t0 = time.perf_counter()
788
+ try:
789
+ tb = time.perf_counter()
790
+ _module() # first use: JIT build / load (timed apart from the probe)
791
+ QUAL_STATS["build_s"].append(round(time.perf_counter() - tb, 6))
792
+ t0 = time.perf_counter()
793
+ ok = _probe(P, x.shape[1], d_u.shape[2], d_u.device, key[11])
794
+ torch.cuda.synchronize(d_u.device)
795
+ if _qual_key(d_u, x, d_dv, P) != key: # settings changed during the probe -> not a valid verdict
796
+ raise RuntimeError("qualification key changed during the probe")
797
+ except Exception: # noqa: BLE001
798
+ QUAL_STATS["failed_probe_s"].append(round(time.perf_counter() - t0, 6))
799
+ QUAL_STATS["errors"] += 1
800
+ n = _QUAL_ERR.get(key, 0) + 1
801
+ _QUAL_ERR[key] = n
802
+ if n >= 3:
803
+ _QUAL[key] = False
804
+ return False
805
+ QUAL_STATS["probe_s"].append(round(time.perf_counter() - t0, 6))
806
+ _QUAL[key] = ok
807
+ QUAL_STATS["probes"] += 1
808
+ QUAL_STATS["equal" if ok else "differ"] += 1
809
+ return ok
810
+
811
+
812
+ def make_step(d_u, x, d_dv, num_parallel: int, _probe_counts=None):
813
+ """The per-step update d_u(ti) += x(ti)^T d_dv(ti) for the backward's reverse loop: step(ti, pdl=False). The caller
814
+ opts in to PDL per launch (it vouches that the kernel right before writes nothing the kernel reads before its wait:
815
+ x and d_u(ti), which the backward's initial bmm wrote before the step loop and only this kernel writes afterwards);
816
+ step.pdl_capable says whether this build can. (_probe_counts: the qualification probe's own launch counter, so that
817
+ PDL_LAUNCHES / LAST_GEOM describe production launches only.)"""
818
+ mod = _module()
819
+ counts = PDL_LAUNCHES if _probe_counts is None else _probe_counts
820
+ P = int(num_parallel)
821
+ du3 = d_u
822
+ dv = d_dv
823
+ pdl_ok = device_pdl_ok(d_u.device)
824
+ N = x.shape[0]
825
+ if not 1 <= P <= N:
826
+ raise ValueError(f"gx_gemm: need 1 <= num_parallel <= N chunks; got P {P}, N {N}")
827
+ lim = N // P
828
+ ti_rows = _rows(P, x.shape[1], d_u.shape[2], d_u.device) # once per factory call (once per backward)
829
+ cap = _cap()
830
+ # > 1 / (cap + 1) of the SM: at most cap CTAs (programmatic launches only)
831
+ smem_min = 0 if cap == 0 or not pdl_ok else smem_per_sm(d_u.device) // (cap + 1) + 1024
832
+ if smem_min:
833
+ smem_min = min(smem_min, smem_cap(d_u.device))
834
+
835
+ def step(ti: int, pdl: bool = False) -> None:
836
+ if not 0 <= int(ti) < lim:
837
+ raise ValueError(f"gx_gemm: step {ti} outside [0, {lim})")
838
+ use = 1 if (pdl and pdl_ok) else 0
839
+ mod.gx_gemm(du3, x, dv, int(ti), P, use, ti_rows, smem_min)
840
+ counts[use] += 1
841
+
842
+ def plan(nsteps: int, pdl: bool = False):
843
+ """(plan, launch address, use) of steps 0 .. nsteps - 1 for bwd_steps_cuda; use: 1 = PDL."""
844
+ p = mod.gx_gemm_plan(du3, x, dv, int(nsteps) - 1, P, ti_rows, smem_min)
845
+ return p, int(mod.gx_gemm_launch_addr()), (1 if (pdl and pdl_ok) else 0)
846
+
847
+ def count(n: int, use: int) -> None:
848
+ counts[use] += int(n)
849
+
850
+ step.pdl_capable = pdl_ok
851
+ step.plan = plan
852
+ step.count = count
853
+ step.geom = dict(ti_rows=ti_rows, cap=cap, smem_min=smem_min)
854
+ if _probe_counts is None:
855
+ LAST_GEOM.clear()
856
+ LAST_GEOM.update(step.geom)
857
+ return step