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,796 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
"""gather_cuda: the token gather kernel: per-token weighted sums of random bank rows, plus a copy of the rows.
|
|
3
|
+
|
|
4
|
+
For every token ``t`` of a launch and each operand ``o`` (one or two slot lists)
|
|
5
|
+
|
|
6
|
+
out_o[t] = sum_n weights_o[t, n] * bank[idx_o[t, n]] (float32 accumulation)
|
|
7
|
+
stash[t, n] = bank[idx_0[t, n]] (raw rows of operand 0)
|
|
8
|
+
|
|
9
|
+
The forward uses two operands: write slots weighted by ``k_eff`` give ``retrieved`` and the
|
|
10
|
+
snapshot, read slots weighted by ``q_w`` give ``inter``. The backward uses one: bank-gradient
|
|
11
|
+
rows at the write slots weighted by ``write_w`` give ``d_delta_v``, and the stash feeds the
|
|
12
|
+
per-entry kernels that follow.
|
|
13
|
+
|
|
14
|
+
The loads are random rows, so the kernel is latency bound and its geometry keeps rows in
|
|
15
|
+
flight. A row is read as 16-byte vectors by a row group of ``lanes`` threads (8, 16 or 32);
|
|
16
|
+
rows longer than one row group are split into column slices along the grid's y axis. The
|
|
17
|
+
entries of a token are spread over ``groups`` row groups: group ``g`` takes entries
|
|
18
|
+
``g, g + groups, ...`` in order and loads 4 (bf16) or 8 (fp32) rows across operands before
|
|
19
|
+
using any. Group partials are summed in a fixed order, so results are deterministic and
|
|
20
|
+
depend on ``groups`` only.
|
|
21
|
+
|
|
22
|
+
Tokens come in chunks of ``chunk``; positions ``>= valid`` within a chunk are padding: they
|
|
23
|
+
load nothing and produce zero outputs and zero stash rows. Launches use the current stream
|
|
24
|
+
and allocate nothing, so they are CUDA-graph safe.
|
|
25
|
+
|
|
26
|
+
The source also holds the no-stash variant that gather_ns_cuda builds (preprocessor variant GATHER_NS, plus GATHER_L2
|
|
27
|
+
for its L2 cache hints); see that module."""
|
|
28
|
+
|
|
29
|
+
import functools
|
|
30
|
+
from typing import NamedTuple
|
|
31
|
+
|
|
32
|
+
import torch
|
|
33
|
+
|
|
34
|
+
from ._build import load_cuda_extension
|
|
35
|
+
|
|
36
|
+
_MAX_THREADS = 512 # kMaxThreads, the kernel's launch bound
|
|
37
|
+
|
|
38
|
+
_CUDA_SOURCE = r"""
|
|
39
|
+
#include <torch/extension.h>
|
|
40
|
+
#include <ATen/cuda/CUDAContext.h>
|
|
41
|
+
#include <c10/cuda/CUDAGuard.h>
|
|
42
|
+
#include <cstdint>
|
|
43
|
+
#if defined(GATHER_NS)
|
|
44
|
+
#include <cstring>
|
|
45
|
+
#endif
|
|
46
|
+
#include <algorithm>
|
|
47
|
+
#include <limits>
|
|
48
|
+
|
|
49
|
+
namespace {
|
|
50
|
+
|
|
51
|
+
constexpr int kMaxThreads = 512;
|
|
52
|
+
|
|
53
|
+
#if defined(GATHER_NS)
|
|
54
|
+
#define FX_LDB(p) __ldcg(p) // bank rows a grid that may still be finishing (PDL) wrote: coherent path
|
|
55
|
+
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
|
|
56
|
+
#define FX_PDL_WAIT_LAUNCH() asm volatile("griddepcontrol.wait;\n\tgriddepcontrol.launch_dependents;" ::: "memory")
|
|
57
|
+
#else
|
|
58
|
+
#define FX_PDL_WAIT_LAUNCH() ((void)0)
|
|
59
|
+
#endif
|
|
60
|
+
#endif
|
|
61
|
+
#if defined(GATHER_L2)
|
|
62
|
+
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800)
|
|
63
|
+
__device__ __forceinline__ uint64_t fx_pol_ef() { // L2 evict-first policy (pure: CSE'd by the compiler)
|
|
64
|
+
uint64_t p;
|
|
65
|
+
asm("createpolicy.fractional.L2::evict_first.b64 %0, 1.0;" : "=l"(p));
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
template <typename V> __device__ __forceinline__ V fx_ld_ef(const V* p) { return FX_LDB(p); }
|
|
69
|
+
template <> __device__ __forceinline__ uint4 fx_ld_ef<uint4>(const uint4* p) { // L2-only, evict-first
|
|
70
|
+
uint4 v;
|
|
71
|
+
asm volatile("ld.global.cg.L2::cache_hint.v4.u32 {%0, %1, %2, %3}, [%4], %5;"
|
|
72
|
+
: "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) : "l"(p), "l"(fx_pol_ef()) : "memory");
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
template <typename V> __device__ __forceinline__ void fx_st_ef(V* p, const V& v) { *p = v; }
|
|
76
|
+
template <> __device__ __forceinline__ void fx_st_ef<uint4>(uint4* p, const uint4& v) {
|
|
77
|
+
asm volatile("st.global.L2::cache_hint.v4.u32 [%0], {%1, %2, %3, %4}, %5;"
|
|
78
|
+
:: "l"(p), "r"(v.x), "r"(v.y), "r"(v.z), "r"(v.w), "l"(fx_pol_ef()) : "memory");
|
|
79
|
+
}
|
|
80
|
+
#else
|
|
81
|
+
template <typename V> __device__ __forceinline__ V fx_ld_ef(const V* p) { return FX_LDB(p); }
|
|
82
|
+
template <typename V> __device__ __forceinline__ void fx_st_ef(V* p, const V& v) { *p = v; }
|
|
83
|
+
#endif
|
|
84
|
+
#endif
|
|
85
|
+
template <int B> struct Bits;
|
|
86
|
+
template <> struct Bits<2> { using type = uint16_t; };
|
|
87
|
+
template <> struct Bits<4> { using type = uint32_t; };
|
|
88
|
+
template <> struct Bits<16> { using type = uint4; };
|
|
89
|
+
|
|
90
|
+
// Elements are raw bits: uint16_t is bfloat16 (the high half of a float), uint32_t float.
|
|
91
|
+
__device__ __forceinline__ float to_float(uint16_t b) { return __uint_as_float(uint32_t(b) << 16); }
|
|
92
|
+
__device__ __forceinline__ float to_float(uint32_t b) { return __uint_as_float(b); }
|
|
93
|
+
|
|
94
|
+
struct Operand {
|
|
95
|
+
const void* idx; // [rows, width] int32 or int64 bank rows
|
|
96
|
+
const void* w; // [rows, width] bfloat16 or float32 weights
|
|
97
|
+
float* out; // [rows, hd]
|
|
98
|
+
int width;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
struct Args {
|
|
102
|
+
const void* bank; // [slots, hd]
|
|
103
|
+
void* stash; // [rows, width of operand 0, hd]
|
|
104
|
+
Operand op[2];
|
|
105
|
+
int64_t row0, stash_row0, out_row0; // first row of this launch in idx/w, stash, out
|
|
106
|
+
int hd, vpr; // row length, vectors per row
|
|
107
|
+
int tokens, chunk, valid;
|
|
108
|
+
int per_group; // entries per row group, ceil(max width / groups)
|
|
109
|
+
bool idx64, w_bf16;
|
|
110
|
+
#if defined(GATHER_NS)
|
|
111
|
+
const int* kpos; // [rows, width of operand 0]: FIRST position in the chunk's sorted row, or -1
|
|
112
|
+
int crow; // rows per chunk of the compact snapshot (C * W)
|
|
113
|
+
#endif
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Every input is read-only for the kernel's lifetime, so loads take the non-coherent path.
|
|
117
|
+
__device__ __forceinline__ int load_slot(const void* p, int64_t i, bool idx64) {
|
|
118
|
+
return idx64 ? static_cast<int>(__ldg(static_cast<const long long*>(p) + i))
|
|
119
|
+
: __ldg(static_cast<const int*>(p) + i);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
__device__ __forceinline__ float load_weight(const void* p, int64_t i, bool bf16) {
|
|
123
|
+
return bf16 ? to_float(__ldg(static_cast<const unsigned short*>(p) + i))
|
|
124
|
+
: __ldg(static_cast<const float*>(p) + i);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
template <int N>
|
|
128
|
+
__device__ __forceinline__ void store_floats(float* dst, const float (&v)[N]) {
|
|
129
|
+
if constexpr (N % 4 == 0) {
|
|
130
|
+
#pragma unroll
|
|
131
|
+
for (int i = 0; i < N; i += 4)
|
|
132
|
+
*reinterpret_cast<float4*>(dst + i) = make_float4(v[i], v[i + 1], v[i + 2], v[i + 3]);
|
|
133
|
+
} else {
|
|
134
|
+
#pragma unroll
|
|
135
|
+
for (int i = 0; i < N; ++i) dst[i] = v[i];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Grid (token blocks, column slices); block (LPR lanes, groups, tokens per CTA). A column
|
|
140
|
+
// slice is LPR vectors wide. Threads linearise x first, so a row group is LPR consecutive
|
|
141
|
+
// lanes. Row group g accumulates entries g, g + groups, ... in order and the group partials
|
|
142
|
+
// are summed in order, so the rounding depends on groups only.
|
|
143
|
+
template <typename E, int VB, int LPR, bool HAS_Q>
|
|
144
|
+
__global__ void __launch_bounds__(kMaxThreads) token_gather_kernel(const Args a) {
|
|
145
|
+
using V = typename Bits<VB>::type;
|
|
146
|
+
constexpr int VEC = VB / sizeof(E);
|
|
147
|
+
constexpr int NOP = HAS_Q ? 2 : 1;
|
|
148
|
+
// Rows in flight per row group, across operands: 4 for bf16 and 8 for fp32, whose
|
|
149
|
+
// accumulators take half the registers per 16-byte vector.
|
|
150
|
+
constexpr int STEP = (sizeof(E) == 4 ? 8 : 4) / NOP;
|
|
151
|
+
constexpr int SLICE = LPR * VEC; // elements per column slice
|
|
152
|
+
extern __shared__ float4 smem[];
|
|
153
|
+
float* partial = reinterpret_cast<float*>(smem); // [NOP][tpc][groups][SLICE] if groups > 1
|
|
154
|
+
|
|
155
|
+
const int lane = threadIdx.x, group = threadIdx.y, groups = blockDim.y;
|
|
156
|
+
const int t = blockIdx.x * blockDim.z + threadIdx.z;
|
|
157
|
+
const bool in_range = t < a.tokens;
|
|
158
|
+
const bool live = in_range && (a.valid == a.chunk || t % a.chunk < a.valid);
|
|
159
|
+
const int v = blockIdx.y * LPR + lane;
|
|
160
|
+
const bool lane_on = v < a.vpr;
|
|
161
|
+
const int64_t col = int64_t(v) * VEC;
|
|
162
|
+
const int64_t row = a.row0 + t;
|
|
163
|
+
const E* bank = static_cast<const E*>(a.bank);
|
|
164
|
+
#if defined(GATHER_NS)
|
|
165
|
+
E* stash = a.stash == nullptr ? nullptr : (a.kpos != nullptr
|
|
166
|
+
? static_cast<E*>(a.stash) + ((a.row0 + t) / a.chunk) * a.crow * a.hd + col
|
|
167
|
+
: static_cast<E*>(a.stash) + (a.stash_row0 + t) * a.op[0].width * a.hd + col);
|
|
168
|
+
#else
|
|
169
|
+
E* stash = static_cast<E*>(a.stash) + (a.stash_row0 + t) * a.op[0].width * a.hd + col;
|
|
170
|
+
#endif
|
|
171
|
+
|
|
172
|
+
float acc[NOP][VEC];
|
|
173
|
+
#pragma unroll
|
|
174
|
+
for (int o = 0; o < NOP; ++o)
|
|
175
|
+
#pragma unroll
|
|
176
|
+
for (int i = 0; i < VEC; ++i) acc[o][i] = 0.0f;
|
|
177
|
+
|
|
178
|
+
for (int jb = 0; jb < a.per_group; jb += LPR) {
|
|
179
|
+
// Lane l reads the slot and weight of entry jb + l; loads broadcast them.
|
|
180
|
+
#if defined(GATHER_NS)
|
|
181
|
+
int kp_l = -1;
|
|
182
|
+
if (a.kpos != nullptr) {
|
|
183
|
+
const int n = group + groups * (jb + lane);
|
|
184
|
+
const bool ok = live && jb + lane < a.per_group && n < a.op[0].width;
|
|
185
|
+
kp_l = ok ? __ldg(a.kpos + row * a.op[0].width + n) : -1;
|
|
186
|
+
}
|
|
187
|
+
#endif
|
|
188
|
+
int slot_l[NOP];
|
|
189
|
+
float w_l[NOP];
|
|
190
|
+
#pragma unroll
|
|
191
|
+
for (int o = 0; o < NOP; ++o) {
|
|
192
|
+
const int n = group + groups * (jb + lane);
|
|
193
|
+
const bool ok = live && jb + lane < a.per_group && n < a.op[o].width;
|
|
194
|
+
const int64_t i = row * a.op[o].width + n;
|
|
195
|
+
slot_l[o] = ok ? load_slot(a.op[o].idx, i, a.idx64) : 0;
|
|
196
|
+
w_l[o] = ok ? load_weight(a.op[o].w, i, a.w_bf16) : 0.0f;
|
|
197
|
+
}
|
|
198
|
+
#if defined(GATHER_NS)
|
|
199
|
+
if (jb == 0) FX_PDL_WAIT_LAUNCH(); // PDL: the previous grid (DW) has completed; before any bank read
|
|
200
|
+
#endif
|
|
201
|
+
const int batch = min(LPR, a.per_group - jb);
|
|
202
|
+
for (int s0 = 0; s0 < batch; s0 += STEP) {
|
|
203
|
+
// Addresses first, then all loads: issuing a stash store between two loads would
|
|
204
|
+
// stall on the first load and serialise the rest.
|
|
205
|
+
const V* src[NOP][STEP];
|
|
206
|
+
bool load[NOP][STEP];
|
|
207
|
+
float w[NOP][STEP];
|
|
208
|
+
#pragma unroll
|
|
209
|
+
for (int s = 0; s < STEP; ++s) {
|
|
210
|
+
const int n = group + groups * (jb + s0 + s);
|
|
211
|
+
#pragma unroll
|
|
212
|
+
for (int o = 0; o < NOP; ++o) {
|
|
213
|
+
const int slot = __shfl_sync(0xffffffffu, slot_l[o], s0 + s, LPR);
|
|
214
|
+
w[o][s] = __shfl_sync(0xffffffffu, w_l[o], s0 + s, LPR);
|
|
215
|
+
src[o][s] = reinterpret_cast<const V*>(bank + int64_t(slot) * a.hd + col);
|
|
216
|
+
load[o][s] = live && lane_on && s0 + s < batch && n < a.op[o].width;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
V r[NOP][STEP];
|
|
220
|
+
#pragma unroll
|
|
221
|
+
for (int s = 0; s < STEP; ++s)
|
|
222
|
+
#pragma unroll
|
|
223
|
+
#if defined(GATHER_NS)
|
|
224
|
+
#if defined(GATHER_L2)
|
|
225
|
+
for (int o = 0; o < NOP; ++o) r[o][s] = load[o][s] ? (o == 1 ? fx_ld_ef(src[o][s]) : FX_LDB(src[o][s])) : V{}; // read rows evict-first
|
|
226
|
+
#else
|
|
227
|
+
for (int o = 0; o < NOP; ++o) r[o][s] = load[o][s] ? FX_LDB(src[o][s]) : V{};
|
|
228
|
+
#endif
|
|
229
|
+
#else
|
|
230
|
+
for (int o = 0; o < NOP; ++o) r[o][s] = load[o][s] ? __ldg(src[o][s]) : V{};
|
|
231
|
+
#endif
|
|
232
|
+
#pragma unroll
|
|
233
|
+
for (int s = 0; s < STEP; ++s) {
|
|
234
|
+
const int n = group + groups * (jb + s0 + s);
|
|
235
|
+
#if defined(GATHER_NS)
|
|
236
|
+
if (a.kpos != nullptr) {
|
|
237
|
+
const int kj = __shfl_sync(0xffffffffu, kp_l, s0 + s, LPR);
|
|
238
|
+
if (kj >= 0 && in_range && lane_on && s0 + s < batch && n < a.op[0].width)
|
|
239
|
+
#if defined(GATHER_L2)
|
|
240
|
+
fx_st_ef(reinterpret_cast<V*>(stash + int64_t(kj) * a.hd), r[0][s]); // evict-first
|
|
241
|
+
#else
|
|
242
|
+
*reinterpret_cast<V*>(stash + int64_t(kj) * a.hd) = r[0][s];
|
|
243
|
+
#endif
|
|
244
|
+
} else if (a.stash != nullptr && in_range && lane_on && s0 + s < batch && n < a.op[0].width)
|
|
245
|
+
#else
|
|
246
|
+
if (in_range && lane_on && s0 + s < batch && n < a.op[0].width)
|
|
247
|
+
#endif
|
|
248
|
+
*reinterpret_cast<V*>(stash + int64_t(n) * a.hd) = r[0][s];
|
|
249
|
+
// Missing entries have weight 0 and a zero row, so their update adds an exact +0.
|
|
250
|
+
#pragma unroll
|
|
251
|
+
for (int o = 0; o < NOP; ++o) {
|
|
252
|
+
const E* e = reinterpret_cast<const E*>(&r[o][s]);
|
|
253
|
+
#pragma unroll
|
|
254
|
+
for (int i = 0; i < VEC; ++i)
|
|
255
|
+
acc[o][i] = fmaf(w[o][s], to_float(e[i]), acc[o][i]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#if defined(GATHER_NS)
|
|
262
|
+
FX_PDL_WAIT_LAUNCH(); // PDL: also when no entry loop ran (no store, no exit before the wait)
|
|
263
|
+
#endif
|
|
264
|
+
if (groups == 1) {
|
|
265
|
+
if (in_range && lane_on) {
|
|
266
|
+
#pragma unroll
|
|
267
|
+
for (int o = 0; o < NOP; ++o)
|
|
268
|
+
store_floats(a.op[o].out + (a.out_row0 + t) * a.hd + col, acc[o]);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
#pragma unroll
|
|
273
|
+
for (int o = 0; o < NOP; ++o) {
|
|
274
|
+
float* dst = partial + ((o * blockDim.z + threadIdx.z) * groups + group) * SLICE;
|
|
275
|
+
store_floats(dst + lane * VEC, acc[o]);
|
|
276
|
+
}
|
|
277
|
+
__syncthreads();
|
|
278
|
+
// Sum the group partials of each column in order.
|
|
279
|
+
const int tid = lane + LPR * (group + groups * threadIdx.z);
|
|
280
|
+
const int width = min(SLICE, a.hd - int(blockIdx.y) * SLICE);
|
|
281
|
+
for (int q = tid; q < blockDim.z * width; q += LPR * groups * blockDim.z) {
|
|
282
|
+
const int z = q / width, c = q - z * width;
|
|
283
|
+
const int tok = blockIdx.x * blockDim.z + z;
|
|
284
|
+
if (tok >= a.tokens) continue;
|
|
285
|
+
#pragma unroll
|
|
286
|
+
for (int o = 0; o < NOP; ++o) {
|
|
287
|
+
const float* src = partial + (o * blockDim.z + z) * groups * SLICE + c;
|
|
288
|
+
float sum = 0.0f;
|
|
289
|
+
for (int g = 0; g < groups; ++g) sum += src[g * SLICE];
|
|
290
|
+
a.op[o].out[(a.out_row0 + tok) * a.hd + blockIdx.y * SLICE + c] = sum;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#if defined(GATHER_NS)
|
|
296
|
+
static thread_local int fx_pdl_next = 0; // set by token_gather_dc for its own launch
|
|
297
|
+
struct FxGatherCap { // what launch() would launch (plan capture)
|
|
298
|
+
const void* fn;
|
|
299
|
+
Args a;
|
|
300
|
+
dim3 grid, block;
|
|
301
|
+
size_t smem;
|
|
302
|
+
int set;
|
|
303
|
+
};
|
|
304
|
+
static thread_local FxGatherCap* fx_cap = nullptr; // non-null only inside token_gather_dc_plan
|
|
305
|
+
#endif
|
|
306
|
+
template <typename E, int VB, int LPR, bool HAS_Q>
|
|
307
|
+
void launch(const Args& a, dim3 grid, dim3 block, size_t smem) {
|
|
308
|
+
#if defined(GATHER_NS)
|
|
309
|
+
if (fx_cap != nullptr) { // plan capture: record, do not launch
|
|
310
|
+
fx_cap->fn = reinterpret_cast<const void*>(token_gather_kernel<E, VB, LPR, HAS_Q>);
|
|
311
|
+
fx_cap->a = a;
|
|
312
|
+
fx_cap->grid = grid;
|
|
313
|
+
fx_cap->block = block;
|
|
314
|
+
fx_cap->smem = smem;
|
|
315
|
+
fx_cap->set = 1;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
auto stream = at::cuda::getCurrentCUDAStream();
|
|
319
|
+
if (fx_pdl_next) { // programmatic stream serialization (the caller guarantees the previous launch wrote nothing read before the wait)
|
|
320
|
+
cudaLaunchAttribute attr[1];
|
|
321
|
+
attr[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
|
|
322
|
+
attr[0].val.programmaticStreamSerializationAllowed = 1;
|
|
323
|
+
cudaLaunchConfig_t cfg = {};
|
|
324
|
+
cfg.gridDim = grid;
|
|
325
|
+
cfg.blockDim = block;
|
|
326
|
+
cfg.dynamicSmemBytes = smem;
|
|
327
|
+
cfg.stream = stream.stream();
|
|
328
|
+
cfg.attrs = attr;
|
|
329
|
+
cfg.numAttrs = 1;
|
|
330
|
+
C10_CUDA_CHECK(cudaLaunchKernelEx(&cfg, token_gather_kernel<E, VB, LPR, HAS_Q>, a));
|
|
331
|
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
#else
|
|
335
|
+
auto stream = at::cuda::getCurrentCUDAStream();
|
|
336
|
+
#endif
|
|
337
|
+
token_gather_kernel<E, VB, LPR, HAS_Q><<<grid, block, smem, stream>>>(a);
|
|
338
|
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
template <typename E, bool HAS_Q>
|
|
342
|
+
void dispatch(const Args& a, int vec_bytes, dim3 grid, dim3 block, size_t smem) {
|
|
343
|
+
if (vec_bytes != 16) return launch<E, sizeof(E), 32, HAS_Q>(a, grid, block, smem);
|
|
344
|
+
switch (block.x) {
|
|
345
|
+
case 8: return launch<E, 16, 8, HAS_Q>(a, grid, block, smem);
|
|
346
|
+
case 16: return launch<E, 16, 16, HAS_Q>(a, grid, block, smem);
|
|
347
|
+
default: return launch<E, 16, 32, HAS_Q>(a, grid, block, smem);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
} // namespace
|
|
352
|
+
|
|
353
|
+
#if defined(GATHER_NS)
|
|
354
|
+
void token_gather_dc(
|
|
355
|
+
#else
|
|
356
|
+
void token_gather(
|
|
357
|
+
#endif
|
|
358
|
+
at::Tensor bank, at::Tensor idx, at::Tensor w, at::Tensor stash, at::Tensor out,
|
|
359
|
+
std::optional<at::Tensor> q_idx, std::optional<at::Tensor> q_w, std::optional<at::Tensor> q_out,
|
|
360
|
+
int64_t row0, int64_t stash_row0, int64_t out_row0,
|
|
361
|
+
int64_t tokens, int64_t chunk, int64_t valid,
|
|
362
|
+
#if defined(GATHER_NS)
|
|
363
|
+
int64_t vec_bytes, int64_t lanes, int64_t groups, int64_t tpc,
|
|
364
|
+
std::optional<at::Tensor> kpos, int64_t crow, int64_t pdl)
|
|
365
|
+
#else
|
|
366
|
+
int64_t vec_bytes, int64_t lanes, int64_t groups, int64_t tpc)
|
|
367
|
+
#endif
|
|
368
|
+
{
|
|
369
|
+
const bool has_q = q_idx.has_value();
|
|
370
|
+
#if defined(GATHER_NS)
|
|
371
|
+
TORCH_CHECK(pdl == 0 || pdl == 1, "token_gather_dc: pdl must be 0 or 1");
|
|
372
|
+
#endif
|
|
373
|
+
TORCH_CHECK(has_q == q_w.has_value() && has_q == q_out.has_value(),
|
|
374
|
+
"token_gather: q_idx, q_weights and q_out go together");
|
|
375
|
+
TORCH_CHECK(bank.is_cuda() && bank.dim() == 2 && bank.is_contiguous(),
|
|
376
|
+
"token_gather: bank must be a contiguous CUDA tensor [slots, head_dim]");
|
|
377
|
+
TORCH_CHECK(bank.scalar_type() == at::kBFloat16 || bank.scalar_type() == at::kFloat,
|
|
378
|
+
"token_gather: bank must be bfloat16 or float32, got ", bank.scalar_type());
|
|
379
|
+
TORCH_CHECK(bank.size(0) <= std::numeric_limits<int>::max(),
|
|
380
|
+
"token_gather: the bank must have fewer than 2^31 rows");
|
|
381
|
+
TORCH_CHECK(stash.scalar_type() == bank.scalar_type(),
|
|
382
|
+
"token_gather: stash must match the bank dtype");
|
|
383
|
+
TORCH_CHECK(tokens >= 0 && tokens <= std::numeric_limits<int>::max() && chunk >= 1
|
|
384
|
+
&& tokens % chunk == 0 && valid >= 0 && valid <= chunk,
|
|
385
|
+
"token_gather: need tokens = k * chunk and 0 <= valid <= chunk");
|
|
386
|
+
const int64_t hd = bank.size(1), es = bank.element_size();
|
|
387
|
+
|
|
388
|
+
Args a{};
|
|
389
|
+
a.bank = bank.data_ptr();
|
|
390
|
+
a.idx64 = idx.scalar_type() == at::kLong;
|
|
391
|
+
a.w_bf16 = w.scalar_type() == at::kBFloat16;
|
|
392
|
+
const auto set_operand = [&](int o, const at::Tensor& oi, const at::Tensor& ow,
|
|
393
|
+
const at::Tensor& oo) {
|
|
394
|
+
for (const at::Tensor* x : {&oi, &ow, &oo})
|
|
395
|
+
TORCH_CHECK(x->device() == bank.device() && x->is_contiguous(),
|
|
396
|
+
"token_gather: every tensor must be contiguous and on the bank's device");
|
|
397
|
+
TORCH_CHECK(oi.scalar_type() == (a.idx64 ? at::kLong : at::kInt),
|
|
398
|
+
"token_gather: indices must be int32 or int64, the same for both operands");
|
|
399
|
+
TORCH_CHECK(ow.scalar_type() == (a.w_bf16 ? at::kBFloat16 : at::kFloat),
|
|
400
|
+
"token_gather: weights must be bfloat16 or float32, the same for both "
|
|
401
|
+
"operands");
|
|
402
|
+
TORCH_CHECK(oo.scalar_type() == at::kFloat, "token_gather: outputs must be float32");
|
|
403
|
+
TORCH_CHECK(ow.dim() >= 1 && oi.sizes() == ow.sizes(),
|
|
404
|
+
"token_gather: indices and weights must match");
|
|
405
|
+
const int64_t width = ow.size(-1);
|
|
406
|
+
TORCH_CHECK(width >= 1 && width <= 256,
|
|
407
|
+
"token_gather: widths must be in 1..256, got ", width);
|
|
408
|
+
TORCH_CHECK(row0 >= 0 && row0 + tokens <= ow.numel() / width,
|
|
409
|
+
"token_gather: rows [row0, row0 + tokens) exceed the index rows");
|
|
410
|
+
TORCH_CHECK(oo.numel() % hd == 0 && out_row0 >= 0 && out_row0 + tokens <= oo.numel() / hd,
|
|
411
|
+
"token_gather: rows [out_row0, out_row0 + tokens) exceed the output rows");
|
|
412
|
+
a.op[o] = {oi.data_ptr(), ow.data_ptr(), oo.data_ptr<float>(), static_cast<int>(width)};
|
|
413
|
+
};
|
|
414
|
+
set_operand(0, idx, w, out);
|
|
415
|
+
if (has_q) set_operand(1, *q_idx, *q_w, *q_out);
|
|
416
|
+
const int64_t stash_row = a.op[0].width * hd;
|
|
417
|
+
#if defined(GATHER_NS)
|
|
418
|
+
TORCH_CHECK(stash.numel() == 0 || (stash.device() == bank.device() && stash.is_contiguous()
|
|
419
|
+
&& stash.numel() % stash_row == 0 && stash_row0 >= 0
|
|
420
|
+
&& stash_row0 + tokens <= stash.numel() / stash_row),
|
|
421
|
+
#else
|
|
422
|
+
TORCH_CHECK(stash.device() == bank.device() && stash.is_contiguous()
|
|
423
|
+
&& stash.numel() % stash_row == 0 && stash_row0 >= 0
|
|
424
|
+
&& stash_row0 + tokens <= stash.numel() / stash_row,
|
|
425
|
+
#endif
|
|
426
|
+
"token_gather: stash must be contiguous [rows, W, head_dim] and cover "
|
|
427
|
+
"[stash_row0, stash_row0 + tokens)");
|
|
428
|
+
|
|
429
|
+
TORCH_CHECK(vec_bytes == 16 || vec_bytes == es,
|
|
430
|
+
"token_gather: vec_bytes must be 16 or the element size");
|
|
431
|
+
TORCH_CHECK((hd * es) % vec_bytes == 0, "token_gather: rows are not a whole number of vectors");
|
|
432
|
+
TORCH_CHECK(lanes == 8 || lanes == 16 || lanes == 32,
|
|
433
|
+
"token_gather: lanes must be 8, 16 or 32");
|
|
434
|
+
TORCH_CHECK(vec_bytes == 16 || lanes == 32, "token_gather: element vectors need 32 lanes");
|
|
435
|
+
// Rows are read through the non-coherent path, so nothing the kernel writes may alias the bank.
|
|
436
|
+
const auto address = [](const at::Tensor& x) {
|
|
437
|
+
return reinterpret_cast<uintptr_t>(x.data_ptr());
|
|
438
|
+
};
|
|
439
|
+
const auto overlaps_bank = [&](const at::Tensor& x) {
|
|
440
|
+
return address(x) < address(bank) + bank.nbytes()
|
|
441
|
+
&& address(bank) < address(x) + x.nbytes();
|
|
442
|
+
};
|
|
443
|
+
#if defined(GATHER_NS)
|
|
444
|
+
TORCH_CHECK((stash.numel() == 0 || !overlaps_bank(stash)) && !overlaps_bank(out) && !(has_q && overlaps_bank(*q_out)),
|
|
445
|
+
#else
|
|
446
|
+
TORCH_CHECK(!overlaps_bank(stash) && !overlaps_bank(out) && !(has_q && overlaps_bank(*q_out)),
|
|
447
|
+
#endif
|
|
448
|
+
"token_gather: stash and outputs must not overlap the bank");
|
|
449
|
+
const auto aligned = [&](const at::Tensor& x) { return address(x) % 16 == 0; };
|
|
450
|
+
#if defined(GATHER_NS)
|
|
451
|
+
TORCH_CHECK(vec_bytes != 16 || (aligned(bank) && (stash.numel() == 0 || aligned(stash)) && aligned(out)
|
|
452
|
+
#else
|
|
453
|
+
TORCH_CHECK(vec_bytes != 16 || (aligned(bank) && aligned(stash) && aligned(out)
|
|
454
|
+
#endif
|
|
455
|
+
&& (!has_q || aligned(*q_out))),
|
|
456
|
+
"token_gather: 16-byte vectors need 16-byte aligned bank, stash and outputs");
|
|
457
|
+
const int64_t threads = lanes * groups * tpc;
|
|
458
|
+
TORCH_CHECK(groups >= 1 && tpc >= 1 && threads % 32 == 0 && threads <= kMaxThreads,
|
|
459
|
+
"token_gather: inconsistent geometry");
|
|
460
|
+
const int64_t slice = lanes * vec_bytes / es;
|
|
461
|
+
const size_t smem = groups > 1 ? (has_q ? 2 : 1) * tpc * groups * slice * sizeof(float) : 0;
|
|
462
|
+
TORCH_CHECK(smem <= 48 * 1024,
|
|
463
|
+
"token_gather: geometry needs ", smem, " bytes of shared memory");
|
|
464
|
+
if (tokens == 0) return;
|
|
465
|
+
|
|
466
|
+
#if defined(GATHER_NS)
|
|
467
|
+
a.stash = stash.numel() ? stash.data_ptr() : nullptr;
|
|
468
|
+
TORCH_CHECK(!kpos.has_value() || (kpos->scalar_type() == at::kInt && kpos->is_contiguous() &&
|
|
469
|
+
kpos->device() == bank.device() && kpos->numel() == idx.numel() && stash.numel() > 0 &&
|
|
470
|
+
crow >= 1 && crow == chunk * a.op[0].width && stash.numel() % (crow * hd) == 0 &&
|
|
471
|
+
row0 % chunk == 0 && ((row0 + tokens + chunk - 1) / chunk) * crow * hd <= stash.numel()),
|
|
472
|
+
"token_gather_dc: kpos int32 like idx, and a compact snapshot of [chunks * crow, hd]");
|
|
473
|
+
a.kpos = kpos.has_value() ? kpos->data_ptr<int>() : nullptr;
|
|
474
|
+
a.crow = static_cast<int>(crow);
|
|
475
|
+
#else
|
|
476
|
+
a.stash = stash.data_ptr();
|
|
477
|
+
#endif
|
|
478
|
+
a.row0 = row0;
|
|
479
|
+
a.stash_row0 = stash_row0;
|
|
480
|
+
a.out_row0 = out_row0;
|
|
481
|
+
a.hd = static_cast<int>(hd);
|
|
482
|
+
a.vpr = static_cast<int>(hd * es / vec_bytes);
|
|
483
|
+
a.tokens = static_cast<int>(tokens);
|
|
484
|
+
a.chunk = static_cast<int>(chunk);
|
|
485
|
+
a.valid = static_cast<int>(valid);
|
|
486
|
+
const int width = has_q ? std::max(a.op[0].width, a.op[1].width) : a.op[0].width;
|
|
487
|
+
a.per_group = static_cast<int>((width + groups - 1) / groups);
|
|
488
|
+
|
|
489
|
+
const c10::cuda::CUDAGuard guard(bank.device());
|
|
490
|
+
const dim3 grid((tokens + tpc - 1) / tpc, (hd + slice - 1) / slice);
|
|
491
|
+
const dim3 block(lanes, groups, tpc);
|
|
492
|
+
const int vb = static_cast<int>(vec_bytes);
|
|
493
|
+
#if defined(GATHER_NS)
|
|
494
|
+
fx_pdl_next = static_cast<int>(pdl); // read by launch() of this call only (every call sets it first)
|
|
495
|
+
#endif
|
|
496
|
+
if (bank.scalar_type() == at::kBFloat16) {
|
|
497
|
+
if (has_q) dispatch<uint16_t, true>(a, vb, grid, block, smem);
|
|
498
|
+
else dispatch<uint16_t, false>(a, vb, grid, block, smem);
|
|
499
|
+
} else {
|
|
500
|
+
if (has_q) dispatch<uint32_t, true>(a, vb, grid, block, smem);
|
|
501
|
+
else dispatch<uint32_t, false>(a, vb, grid, block, smem);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
#if defined(GATHER_NS)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
// Plan: the checked launch of token_gather_dc for the no-snapshot call (stash empty, kpos absent) at the LARGEST row0
|
|
508
|
+
// a step will use (every check that depends on row0 is monotonic in it: 0 <= row0, row0 + tokens <= index rows; the
|
|
509
|
+
// stash / kpos checks are vacuous without a stash), captured instead of launched. A step re-checks 0 <= row0 <= row0_max
|
|
510
|
+
// and 0 <= valid <= chunk, then launches the captured kernel with Args.row0 / Args.valid replaced. The plan holds raw
|
|
511
|
+
// pointers: the caller keeps every tensor alive and unresized while it uses the plan.
|
|
512
|
+
namespace {
|
|
513
|
+
struct FxGatherPlan {
|
|
514
|
+
uint64_t magic;
|
|
515
|
+
int64_t row0_max;
|
|
516
|
+
int32_t device;
|
|
517
|
+
int32_t blocks; // 0: nothing to launch (tokens == 0)
|
|
518
|
+
int64_t chunk; // the planned call's chunk (valid's bound, also for an empty plan)
|
|
519
|
+
const void* fn;
|
|
520
|
+
Args a;
|
|
521
|
+
dim3 grid, block;
|
|
522
|
+
size_t smem;
|
|
523
|
+
};
|
|
524
|
+
constexpr uint64_t kGatherPlanMagic = 0x4e414c50475846ull;
|
|
525
|
+
} // namespace
|
|
526
|
+
|
|
527
|
+
at::Tensor token_gather_dc_plan(
|
|
528
|
+
at::Tensor bank, at::Tensor idx, at::Tensor w, at::Tensor stash, at::Tensor out,
|
|
529
|
+
std::optional<at::Tensor> q_idx, std::optional<at::Tensor> q_w, std::optional<at::Tensor> q_out,
|
|
530
|
+
int64_t row0_max, int64_t out_row0, int64_t tokens, int64_t chunk,
|
|
531
|
+
int64_t vec_bytes, int64_t lanes, int64_t groups, int64_t tpc)
|
|
532
|
+
{
|
|
533
|
+
TORCH_CHECK(stash.numel() == 0, "token_gather_dc_plan: the planned gather writes no compact rows (empty stash)");
|
|
534
|
+
TORCH_CHECK(row0_max >= 0, "token_gather_dc_plan: row0_max range");
|
|
535
|
+
TORCH_CHECK(fx_cap == nullptr, "token_gather_dc_plan: nested plan capture");
|
|
536
|
+
FxGatherCap cap{};
|
|
537
|
+
fx_cap = ∩
|
|
538
|
+
try {
|
|
539
|
+
token_gather_dc(bank, idx, w, stash, out, q_idx, q_w, q_out, row0_max, 0, out_row0, tokens, chunk, chunk,
|
|
540
|
+
vec_bytes, lanes, groups, tpc, std::nullopt, 0, 0);
|
|
541
|
+
} catch (...) {
|
|
542
|
+
fx_cap = nullptr;
|
|
543
|
+
throw;
|
|
544
|
+
}
|
|
545
|
+
fx_cap = nullptr;
|
|
546
|
+
FxGatherPlan pl;
|
|
547
|
+
std::memset(&pl, 0, sizeof(pl));
|
|
548
|
+
pl.magic = kGatherPlanMagic;
|
|
549
|
+
pl.row0_max = row0_max;
|
|
550
|
+
pl.device = static_cast<int32_t>(bank.get_device());
|
|
551
|
+
pl.chunk = chunk;
|
|
552
|
+
pl.blocks = cap.set ? 1 : 0;
|
|
553
|
+
if (cap.set) {
|
|
554
|
+
pl.fn = cap.fn;
|
|
555
|
+
pl.a = cap.a;
|
|
556
|
+
pl.grid = cap.grid;
|
|
557
|
+
pl.block = cap.block;
|
|
558
|
+
pl.smem = cap.smem;
|
|
559
|
+
}
|
|
560
|
+
at::Tensor t = at::empty({static_cast<int64_t>(sizeof(FxGatherPlan))}, at::TensorOptions().dtype(at::kByte));
|
|
561
|
+
std::memcpy(t.data_ptr<uint8_t>(), &pl, sizeof(pl));
|
|
562
|
+
return t;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
void token_gather_dc_step(at::Tensor plan, int64_t row0, int64_t valid, int64_t pdl)
|
|
566
|
+
{
|
|
567
|
+
TORCH_CHECK(plan.device().is_cpu() && plan.scalar_type() == at::kByte && plan.is_contiguous() &&
|
|
568
|
+
plan.numel() == static_cast<int64_t>(sizeof(FxGatherPlan)), "token_gather_dc_step: not a gather plan");
|
|
569
|
+
FxGatherPlan pl;
|
|
570
|
+
std::memcpy(&pl, plan.data_ptr<uint8_t>(), sizeof(pl));
|
|
571
|
+
TORCH_CHECK(pl.magic == kGatherPlanMagic, "token_gather_dc_step: not a gather plan (magic)");
|
|
572
|
+
TORCH_CHECK(pdl == 0 || pdl == 1, "token_gather_dc_step: pdl must be 0 or 1");
|
|
573
|
+
TORCH_CHECK(row0 >= 0 && row0 <= pl.row0_max, "token_gather_dc_step: row0 ", row0, " out of [0, ", pl.row0_max, "]");
|
|
574
|
+
TORCH_CHECK(valid >= 0 && valid <= pl.chunk, "token_gather_dc_step: valid out of [0, chunk]");
|
|
575
|
+
if (pl.blocks == 0) return;
|
|
576
|
+
pl.a.row0 = row0;
|
|
577
|
+
pl.a.valid = static_cast<int>(valid);
|
|
578
|
+
const c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(pl.device));
|
|
579
|
+
auto stream = at::cuda::getCurrentCUDAStream();
|
|
580
|
+
void* args[] = {(void*)&pl.a};
|
|
581
|
+
if (pdl == 1) {
|
|
582
|
+
cudaLaunchAttribute attr[1];
|
|
583
|
+
attr[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
|
|
584
|
+
attr[0].val.programmaticStreamSerializationAllowed = 1;
|
|
585
|
+
cudaLaunchConfig_t cfg = {};
|
|
586
|
+
cfg.gridDim = pl.grid;
|
|
587
|
+
cfg.blockDim = pl.block;
|
|
588
|
+
cfg.dynamicSmemBytes = pl.smem;
|
|
589
|
+
cfg.stream = stream.stream();
|
|
590
|
+
cfg.attrs = attr;
|
|
591
|
+
cfg.numAttrs = 1;
|
|
592
|
+
C10_CUDA_CHECK(cudaLaunchKernelExC(&cfg, pl.fn, args));
|
|
593
|
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
C10_CUDA_CHECK(cudaLaunchKernel(pl.fn, pl.grid, pl.block, args, pl.smem, stream.stream()));
|
|
597
|
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// token_gather_dc_step as a plain C++ entry point for mover_fx's fused deterministic step loop (one call per forward
|
|
601
|
+
// slab). Same checks and the same launch as token_gather_dc_step, on the given stream; returns 0, a cudaError_t, or a
|
|
602
|
+
// negative check code (-1 magic, -2 row0, -3 valid, -4 pdl, -5 device, -6 size). On a CUDA error the error state is
|
|
603
|
+
// consumed.
|
|
604
|
+
namespace {
|
|
605
|
+
int64_t fx_gather_plan_launch(const void* plan_bytes, int64_t nbytes, int64_t row0, int64_t valid, int64_t pdl,
|
|
606
|
+
void* stream) {
|
|
607
|
+
if (nbytes != static_cast<int64_t>(sizeof(FxGatherPlan))) return -6; // exact-size handshake
|
|
608
|
+
FxGatherPlan pl;
|
|
609
|
+
std::memcpy(&pl, plan_bytes, sizeof(pl));
|
|
610
|
+
if (pl.magic != kGatherPlanMagic) return -1;
|
|
611
|
+
if (!(pdl == 0 || pdl == 1)) return -4;
|
|
612
|
+
if (row0 < 0 || row0 > pl.row0_max) return -2;
|
|
613
|
+
if (valid < 0 || valid > pl.chunk) return -3;
|
|
614
|
+
if (pl.blocks == 0) return 0;
|
|
615
|
+
int dev = -1;
|
|
616
|
+
if (cudaGetDevice(&dev) != cudaSuccess || dev != pl.device) {
|
|
617
|
+
(void)cudaGetLastError();
|
|
618
|
+
return -5;
|
|
619
|
+
}
|
|
620
|
+
pl.a.row0 = row0;
|
|
621
|
+
pl.a.valid = static_cast<int>(valid);
|
|
622
|
+
void* args[] = {(void*)&pl.a};
|
|
623
|
+
cudaError_t e;
|
|
624
|
+
if (pdl == 1) {
|
|
625
|
+
cudaLaunchAttribute attr[1];
|
|
626
|
+
attr[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
|
|
627
|
+
attr[0].val.programmaticStreamSerializationAllowed = 1;
|
|
628
|
+
cudaLaunchConfig_t cfg = {};
|
|
629
|
+
cfg.gridDim = pl.grid;
|
|
630
|
+
cfg.blockDim = pl.block;
|
|
631
|
+
cfg.dynamicSmemBytes = pl.smem;
|
|
632
|
+
cfg.stream = static_cast<cudaStream_t>(stream);
|
|
633
|
+
cfg.attrs = attr;
|
|
634
|
+
cfg.numAttrs = 1;
|
|
635
|
+
e = cudaLaunchKernelExC(&cfg, pl.fn, args);
|
|
636
|
+
} else
|
|
637
|
+
{
|
|
638
|
+
e = cudaLaunchKernel(pl.fn, pl.grid, pl.block, args, pl.smem, static_cast<cudaStream_t>(stream));
|
|
639
|
+
}
|
|
640
|
+
if (e == cudaSuccess) e = cudaGetLastError(); // = C10_CUDA_KERNEL_LAUNCH_CHECK's probe
|
|
641
|
+
else (void)cudaGetLastError();
|
|
642
|
+
return static_cast<int64_t>(e);
|
|
643
|
+
}
|
|
644
|
+
} // namespace
|
|
645
|
+
|
|
646
|
+
int64_t token_gather_dc_launch_addr() { return reinterpret_cast<int64_t>(&fx_gather_plan_launch); }
|
|
647
|
+
#endif
|
|
648
|
+
"""
|
|
649
|
+
|
|
650
|
+
_CPP_SOURCE = r"""
|
|
651
|
+
#include <optional>
|
|
652
|
+
#if defined(GATHER_NS)
|
|
653
|
+
void token_gather_dc(
|
|
654
|
+
#else
|
|
655
|
+
void token_gather(
|
|
656
|
+
#endif
|
|
657
|
+
at::Tensor bank, at::Tensor idx, at::Tensor w, at::Tensor stash, at::Tensor out,
|
|
658
|
+
std::optional<at::Tensor> q_idx, std::optional<at::Tensor> q_w, std::optional<at::Tensor> q_out,
|
|
659
|
+
int64_t row0, int64_t stash_row0, int64_t out_row0,
|
|
660
|
+
int64_t tokens, int64_t chunk, int64_t valid,
|
|
661
|
+
#if defined(GATHER_NS)
|
|
662
|
+
int64_t vec_bytes, int64_t lanes, int64_t groups, int64_t tpc,
|
|
663
|
+
std::optional<at::Tensor> kpos, int64_t crow, int64_t pdl);
|
|
664
|
+
#else
|
|
665
|
+
int64_t vec_bytes, int64_t lanes, int64_t groups, int64_t tpc);
|
|
666
|
+
#endif
|
|
667
|
+
#if defined(GATHER_NS)
|
|
668
|
+
|
|
669
|
+
at::Tensor token_gather_dc_plan(
|
|
670
|
+
at::Tensor bank, at::Tensor idx, at::Tensor w, at::Tensor stash, at::Tensor out,
|
|
671
|
+
std::optional<at::Tensor> q_idx, std::optional<at::Tensor> q_w, std::optional<at::Tensor> q_out,
|
|
672
|
+
int64_t row0_max, int64_t out_row0, int64_t tokens, int64_t chunk,
|
|
673
|
+
int64_t vec_bytes, int64_t lanes, int64_t groups, int64_t tpc);
|
|
674
|
+
void token_gather_dc_step(at::Tensor plan, int64_t row0, int64_t valid, int64_t pdl);
|
|
675
|
+
int64_t token_gather_dc_launch_addr();
|
|
676
|
+
#endif
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
@functools.cache
|
|
680
|
+
def _module():
|
|
681
|
+
return load_cuda_extension(
|
|
682
|
+
"sdm_token_gather",
|
|
683
|
+
cpp_sources=_CPP_SOURCE,
|
|
684
|
+
cuda_sources=_CUDA_SOURCE,
|
|
685
|
+
functions=("token_gather",),
|
|
686
|
+
extra_cuda_cflags=("-O3",),
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class GatherGeometry(NamedTuple):
|
|
691
|
+
"""Launch geometry of one token_gather call (see the module docstring)."""
|
|
692
|
+
|
|
693
|
+
vec_bytes: int # bytes per vector load: 16, or one element for rows not a multiple of 16 B
|
|
694
|
+
lanes: int # threads per row group; a column slice is lanes vectors wide
|
|
695
|
+
slices: int # column slices per row (grid y)
|
|
696
|
+
groups: int # row groups per token; fixes the rounding
|
|
697
|
+
tpc: int # tokens per CTA
|
|
698
|
+
threads: int # threads per CTA
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
_ELEMENT_BYTES = {torch.bfloat16: 2, torch.float32: 4}
|
|
702
|
+
_WARPS_PER_SM = 24 # grid size that entry splitting aims for, in warps per SM
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _pow2_ceil(x: int) -> int:
|
|
706
|
+
return 1 << (x - 1).bit_length()
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _pow2_floor(x: int) -> int:
|
|
710
|
+
return 1 << (x.bit_length() - 1)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def gather_geometry(
|
|
714
|
+
head_dim: int, dtype: torch.dtype, width: int, tokens: int, *, num_sms: int
|
|
715
|
+
) -> GatherGeometry:
|
|
716
|
+
"""Geometry for ``tokens`` tokens of ``width`` = max(W, R) entries of ``head_dim``.
|
|
717
|
+
|
|
718
|
+
A row group has one lane per 16-byte vector of the row (8, 16 or 32 lanes; longer rows
|
|
719
|
+
are cut into 32-vector column slices). Entries are split over more row groups, keeping at
|
|
720
|
+
least four per group, until the grid holds about ``_WARPS_PER_SM`` warps per SM, and a
|
|
721
|
+
CTA takes enough tokens to reach 128 threads.
|
|
722
|
+
"""
|
|
723
|
+
if dtype not in _ELEMENT_BYTES:
|
|
724
|
+
raise TypeError(f"token_gather supports bfloat16 and float32 banks, got {dtype}")
|
|
725
|
+
es = _ELEMENT_BYTES[dtype]
|
|
726
|
+
vec_bytes = 16 if head_dim * es % 16 == 0 else es
|
|
727
|
+
vpr = head_dim * es // vec_bytes
|
|
728
|
+
lanes = max(8, _pow2_ceil(vpr)) if vec_bytes == 16 and vpr <= 16 else 32
|
|
729
|
+
slices = -(-vpr // lanes)
|
|
730
|
+
max_groups = _pow2_floor(max(1, width // 4))
|
|
731
|
+
groups = 1
|
|
732
|
+
while (groups < max_groups and 2 * lanes * groups <= _MAX_THREADS
|
|
733
|
+
and tokens * slices * groups * lanes < 32 * _WARPS_PER_SM * num_sms):
|
|
734
|
+
groups *= 2
|
|
735
|
+
tpc = max(1, 128 // (lanes * groups))
|
|
736
|
+
return GatherGeometry(vec_bytes, lanes, slices, groups, tpc, lanes * groups * tpc)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def token_gather(
|
|
740
|
+
bank: torch.Tensor,
|
|
741
|
+
idx: torch.Tensor,
|
|
742
|
+
weights: torch.Tensor,
|
|
743
|
+
stash: torch.Tensor,
|
|
744
|
+
out: torch.Tensor,
|
|
745
|
+
geometry: GatherGeometry,
|
|
746
|
+
*,
|
|
747
|
+
q_idx: torch.Tensor | None = None,
|
|
748
|
+
q_weights: torch.Tensor | None = None,
|
|
749
|
+
q_out: torch.Tensor | None = None,
|
|
750
|
+
tokens: int,
|
|
751
|
+
chunk: int,
|
|
752
|
+
valid: int,
|
|
753
|
+
row0: int = 0,
|
|
754
|
+
stash_row0: int = 0,
|
|
755
|
+
out_row0: int = 0,
|
|
756
|
+
) -> None:
|
|
757
|
+
"""Gather ``tokens`` consecutive token rows starting at ``row0``, in place.
|
|
758
|
+
|
|
759
|
+
The bank must not be written while the launch runs, and no output may overlap it.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
bank: [slots, head_dim] bfloat16 or float32, contiguous.
|
|
763
|
+
idx, weights: [..., W] bank rows (int32 or int64, not bounds checked) and weights
|
|
764
|
+
(bfloat16 or float32), read at rows ``row0 + t``.
|
|
765
|
+
stash: [..., W, head_dim] in the bank dtype; receives the raw rows at row
|
|
766
|
+
``stash_row0 + t``.
|
|
767
|
+
out: [..., head_dim] float32; receives the weighted sums at row ``out_row0 + t``.
|
|
768
|
+
geometry: from ``gather_geometry``.
|
|
769
|
+
q_idx, q_weights, q_out: optional second operand [..., R] (no stash); same
|
|
770
|
+
dtypes as the first, output rows at ``out_row0 + t``.
|
|
771
|
+
tokens: tokens in this launch, a multiple of ``chunk``.
|
|
772
|
+
chunk, valid: token ``t`` is padding when ``t % chunk >= valid``.
|
|
773
|
+
row0, stash_row0, out_row0: first rows of this launch in each tensor, so that
|
|
774
|
+
per-step launches need no slicing.
|
|
775
|
+
"""
|
|
776
|
+
_module().token_gather(
|
|
777
|
+
bank, idx, weights, stash, out, q_idx, q_weights, q_out,
|
|
778
|
+
row0, stash_row0, out_row0, tokens, chunk, valid,
|
|
779
|
+
geometry.vec_bytes, geometry.lanes, geometry.groups, geometry.tpc,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
def tail_supported(bank, chunk_size, quant="none") -> bool:
|
|
783
|
+
"""movers.tail_admits hook (native tails): this gather runs the call's tail class
|
|
784
|
+
(rows that are not 16-byte multiples take element-sized vectors, see gather_cuda.gather_geometry: arch-neutral code)
|
|
785
|
+
on every backend of _device.backend (modern, a100; rtx with chunks of <= 128 tokens, rule 0e), so a tail class takes
|
|
786
|
+
the same route everywhere. Unquantized snapshots only."""
|
|
787
|
+
import torch as _t
|
|
788
|
+
# snapshot_quant bf16 (float32 banks): the DW writes the compact rows; this gather is the unquantized one
|
|
789
|
+
if quant not in ("none", "bf16") or not bank.is_cuda or bank.dtype not in (_t.bfloat16, _t.float32):
|
|
790
|
+
return False
|
|
791
|
+
from ._device import backend
|
|
792
|
+
try:
|
|
793
|
+
b = backend(bank.device)
|
|
794
|
+
except RuntimeError:
|
|
795
|
+
return False
|
|
796
|
+
return b in ("modern", "a100") or (b == "rtx" and chunk_size <= 128)
|