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.
- sparse_delta_memory/__init__.py +13 -0
- sparse_delta_memory/backward.py +367 -0
- sparse_delta_memory/cache.py +70 -0
- sparse_delta_memory/cuda/__init__.py +3 -0
- sparse_delta_memory/cuda/_build.py +254 -0
- sparse_delta_memory/cuda/_device.py +158 -0
- sparse_delta_memory/cuda/_devmemo.py +64 -0
- sparse_delta_memory/cuda/_image.py +128 -0
- sparse_delta_memory/cuda/bwd_steps_cuda.py +111 -0
- sparse_delta_memory/cuda/gather_cuda.py +796 -0
- sparse_delta_memory/cuda/gather_ns_cuda.py +134 -0
- sparse_delta_memory/cuda/gx_gds_cuda.py +1297 -0
- sparse_delta_memory/cuda/gx_gemm_cuda.py +857 -0
- sparse_delta_memory/cuda/gx_pa2_cuda.py +1320 -0
- sparse_delta_memory/cuda/mover_da2_cuda.py +1802 -0
- sparse_delta_memory/cuda/mover_fx_cuda.py +2229 -0
- sparse_delta_memory/cuda/pa_dc_cuda.py +1500 -0
- sparse_delta_memory/cuda/phase1_bwd_cuda.py +852 -0
- sparse_delta_memory/cuda/phase1_common.cuh +181 -0
- sparse_delta_memory/cuda/phase1_cuda.py +3801 -0
- sparse_delta_memory/cuda/product_key_topk_anyf_cuda.py +668 -0
- sparse_delta_memory/cuda/product_key_topk_cuda.py +735 -0
- sparse_delta_memory/cuda/px_qrec_cuda.py +167 -0
- sparse_delta_memory/cuda/sdm_copy.cuh +90 -0
- sparse_delta_memory/cuda/sdm_plancap.cuh +150 -0
- sparse_delta_memory/cuda_graph.py +605 -0
- sparse_delta_memory/decode.py +140 -0
- sparse_delta_memory/forward.py +464 -0
- sparse_delta_memory/layer.py +663 -0
- sparse_delta_memory/memory_ops.py +1069 -0
- sparse_delta_memory/memory_stats.py +221 -0
- sparse_delta_memory/movers.py +198 -0
- sparse_delta_memory/phase2_kernels.py +267 -0
- sparse_delta_memory/product_key_topk.py +113 -0
- sparse_delta_memory/wy_solve.py +273 -0
- sparse_delta_memory-2.0.0.dist-info/METADATA +482 -0
- sparse_delta_memory-2.0.0.dist-info/RECORD +40 -0
- sparse_delta_memory-2.0.0.dist-info/WHEEL +5 -0
- sparse_delta_memory-2.0.0.dist-info/licenses/LICENSE +407 -0
- 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
|