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,1069 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
"""The chunked SDM write/read op (``sdm_write_read``) and its autograd function.
|
|
4
|
+
|
|
5
|
+
Layout. P parallel streams of T tokens ([P, T, *] inputs, or [P * T, *] rows with
|
|
6
|
+
``num_parallel=P``) are cut into Nt = cdiv(T, C) chunks of C tokens and staged time-major:
|
|
7
|
+
chunk ``ti * P + p`` is chunk ti of stream p. Tokens past a stream's end are padding: they
|
|
8
|
+
are staged as zeros, touch no bank row and produce no output.
|
|
9
|
+
|
|
10
|
+
Forward:
|
|
11
|
+
Phase 1 the memory-independent quantities of every chunk: per-slot log-decays, dedup
|
|
12
|
+
flags, gather weights and the decay-weighted Gram matrices A (writes x writes)
|
|
13
|
+
and QK (reads x writes)
|
|
14
|
+
solve X = (I + tril(beta * A, -1))^-1 and QX = QK @ X
|
|
15
|
+
Phase 2 Nt sequential steps over the P chunks of a step:
|
|
16
|
+
gather retrieved and inter (weighted bank rows) and the snapshot
|
|
17
|
+
dual matmul delta_v = X @ u, readings = QX @ u + inter, u = beta (v - retrieved)
|
|
18
|
+
DW decays and writes the bank rows (and quantizes the snapshot)
|
|
19
|
+
Backward: Phase 2 in reverse order (GWR, baddbmm, Phase A, GDS), then Phase B, the
|
|
20
|
+
matmuls through the solve, and the Phase-1 backward.
|
|
21
|
+
|
|
22
|
+
The forward steps live in forward.py, the backward in backward.py and the mover launchers in movers.py. A call the
|
|
23
|
+
native kernels do not take end to end is refused before any work (_admit, 2.0.0: no fallback route).
|
|
24
|
+
|
|
25
|
+
The bank is advanced in place to its final state; the backward restores it, exactly with
|
|
26
|
+
snapshot_quant "none" and to the dequantized snapshot rows with fp8/int4. A call that
|
|
27
|
+
needs no backward saves nothing: the snapshot and delta_v are per-step scratch, and the
|
|
28
|
+
steps and streams run in bounded slabs and groups (SLAB_BYTES, STEP_BYTES).
|
|
29
|
+
|
|
30
|
+
Determinism: every output, the final bank and every gradient are bitwise reproducible for
|
|
31
|
+
fixed inputs, settings and GPU type (no element receives two concurrent float adds: every bank and
|
|
32
|
+
bank-gradient row has one writer per launch and every sum runs in a fixed order).
|
|
33
|
+
|
|
34
|
+
Also here: the head split/merge row permutes and the per-batch expand of the learned bank
|
|
35
|
+
used by the layer. Every op call records its launch geometry (``last_dispatch``).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import contextlib
|
|
39
|
+
import functools
|
|
40
|
+
import threading
|
|
41
|
+
from typing import NamedTuple
|
|
42
|
+
|
|
43
|
+
import torch
|
|
44
|
+
import torch.utils.deterministic # noqa: F401 (fill_uninitialized_memory, see _no_det_fill)
|
|
45
|
+
import triton
|
|
46
|
+
import triton.language as tl
|
|
47
|
+
from torch.utils._python_dispatch import _disable_current_modes
|
|
48
|
+
|
|
49
|
+
from .cuda import _devmemo, _device
|
|
50
|
+
from .cuda.gather_cuda import gather_geometry, token_gather
|
|
51
|
+
from .cuda.phase1_bwd_cuda import phase1_backward
|
|
52
|
+
from .cuda.phase1_cuda import Phase1
|
|
53
|
+
from . import movers as _movers
|
|
54
|
+
from .movers import make_mover_gds
|
|
55
|
+
from .phase2_kernels import SNAPSHOT_QUANT_MODES
|
|
56
|
+
|
|
57
|
+
# The forward steps (forward.py) call these through this module, the one place a test substitutes them.
|
|
58
|
+
from .cuda.phase1_cuda import phase1_forward # noqa: F401
|
|
59
|
+
from .movers import make_mover_dw # noqa: F401
|
|
60
|
+
from .phase2_kernels import dual_matmul_geom, make_dual_matmul, mover_geom, quant_row # noqa: F401
|
|
61
|
+
from .wy_solve import wy_solve # noqa: F401
|
|
62
|
+
|
|
63
|
+
MIN_CHUNK, MAX_CHUNK = 16, 256
|
|
64
|
+
# RTX 30 / 40 / 50 (99 KB of shared memory per block): chunks of at most 128 tokens, refused before any work otherwise.
|
|
65
|
+
RTX_MAX_CHUNK = 128
|
|
66
|
+
RTX_CHUNK_MSG = ("chunk_size {chunk_size} is not supported on RTX 30 / 40 / 50 GPUs: their 99 KB of shared memory per "
|
|
67
|
+
"block allow chunk_size <= 128")
|
|
68
|
+
DEFAULT_CHUNK = 64 # chunk_size of sdm_write_read, and of a layer whose chunk_size is None (the previous release: 256)
|
|
69
|
+
MAX_WIDTH = 256 # write and read slots per token
|
|
70
|
+
|
|
71
|
+
# Working-set budgets of a forward without backward state. Its steps run in slabs whose
|
|
72
|
+
# staged inputs and Phase-1 state fit SLAB_BYTES, unless that leaves fewer than
|
|
73
|
+
# SLAB_CHUNKS_PER_SM chunks per SM (Phase 1 runs a block per chunk), and its streams run
|
|
74
|
+
# in groups whose per-step snapshot rows fit STEP_BYTES. Its working memory does not grow
|
|
75
|
+
# with T.
|
|
76
|
+
SLAB_BYTES = 512 << 20
|
|
77
|
+
SLAB_CHUNKS_PER_SM = 2
|
|
78
|
+
STEP_BYTES = 512 << 20
|
|
79
|
+
# The slabs of the forward without backward state hold NOGRAD_SLAB_FACTOR x SLAB_BYTES (read at every call, so a patched
|
|
80
|
+
# SLAB_BYTES applies): fewer slabs, less host work per call, larger Phase-1 / WY launches. Slabs are exact (Phase 1, WY,
|
|
81
|
+
# the gather geometry pinned by geom_tokens and the DW are per chunk), so the outputs do not depend on the budget.
|
|
82
|
+
NOGRAD_SLAB_FACTOR = 2
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ============================================================
|
|
86
|
+
# Dispatch record and repeated-slot counter
|
|
87
|
+
# ============================================================
|
|
88
|
+
# Plain Python values derived from shapes, dtypes and arguments only: recording never
|
|
89
|
+
# reads a tensor value, so it never synchronizes with the device.
|
|
90
|
+
_DISPATCH: dict = {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _record(**decisions) -> None:
|
|
94
|
+
_DISPATCH.update(decisions)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def last_dispatch() -> dict:
|
|
98
|
+
"""A copy of the launch decisions of the most recent op call (graph replays do not
|
|
99
|
+
update it)."""
|
|
100
|
+
return dict(_DISPATCH)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def reset_dispatch() -> None:
|
|
104
|
+
_DISPATCH.clear()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# One counter per device, created on its first call (run one eager call before capturing
|
|
108
|
+
# a CUDA graph).
|
|
109
|
+
_repeat_counters: dict = {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _repeat_counter(device) -> torch.Tensor:
|
|
113
|
+
t = _repeat_counters.get(str(device))
|
|
114
|
+
if t is None:
|
|
115
|
+
t = _repeat_counters[str(device)] = torch.zeros(1, device=device, dtype=torch.int64)
|
|
116
|
+
return t
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def repeated_write_slots(reset: bool = True) -> int:
|
|
120
|
+
"""Number of write entries seen so far that repeat a slot of their own token (the op
|
|
121
|
+
requires distinct write slots per token and decays a repeated slot once).
|
|
122
|
+
|
|
123
|
+
Synchronizes with the device.
|
|
124
|
+
"""
|
|
125
|
+
total = 0
|
|
126
|
+
for t in _repeat_counters.values():
|
|
127
|
+
total += int(t.item())
|
|
128
|
+
if reset:
|
|
129
|
+
t.zero_()
|
|
130
|
+
return total
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ============================================================
|
|
134
|
+
# Stream rows <-> time-major chunk rows
|
|
135
|
+
# ============================================================
|
|
136
|
+
# Stream rows [P, T] <-> time-major chunk rows [Nt, P, C]: chunk ti * P + p holds tokens
|
|
137
|
+
# ti * C .. ti * C + C - 1 of stream p (step ti). Time-major rows past a stream's last token
|
|
138
|
+
# are zero.
|
|
139
|
+
|
|
140
|
+
@triton.jit
|
|
141
|
+
def _stage_kernel(src_ptr, dst_ptr, rows, row0, P, T, C, D,
|
|
142
|
+
ROWS: tl.constexpr, BLOCK: tl.constexpr, TO_TM: tl.constexpr):
|
|
143
|
+
"""Copies time-major rows row0 .. row0 + rows - 1 from their stream rows (TO_TM) or
|
|
144
|
+
back, in the destination dtype; grid (cdiv(rows, ROWS), cdiv(D, BLOCK))."""
|
|
145
|
+
r = tl.program_id(0) * ROWS + tl.arange(0, ROWS)
|
|
146
|
+
col = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)
|
|
147
|
+
live = r < rows
|
|
148
|
+
n = (row0 + r) // C
|
|
149
|
+
t = n // P * C + (row0 + r) % C
|
|
150
|
+
stream_row = n % P * T + t
|
|
151
|
+
ok = live & (t < T)
|
|
152
|
+
if TO_TM:
|
|
153
|
+
src, dst, store = stream_row, r, live
|
|
154
|
+
else:
|
|
155
|
+
src, dst, store = r, stream_row, ok
|
|
156
|
+
cols = (col < D)[None, :]
|
|
157
|
+
x = tl.load(src_ptr + src.to(tl.int64)[:, None] * D + col[None, :],
|
|
158
|
+
mask=ok[:, None] & cols, other=0)
|
|
159
|
+
tl.store(dst_ptr + dst.to(tl.int64)[:, None] * D + col[None, :],
|
|
160
|
+
x.to(dst_ptr.dtype.element_ty), mask=store[:, None] & cols)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _launch_stage(src, dst, rows, row0, P, T, C, to_time_major):
|
|
164
|
+
D = dst.shape[-1]
|
|
165
|
+
block = min(1024, triton.next_power_of_2(D))
|
|
166
|
+
per = max(1, 2048 // block)
|
|
167
|
+
_device.tbind(_stage_kernel, (triton.cdiv(rows, per), triton.cdiv(D, block)), src)(
|
|
168
|
+
src, dst, rows, row0, P, T, C, D, ROWS=per, BLOCK=block, TO_TM=to_time_major,
|
|
169
|
+
num_warps=4)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def stage_time_major(x: torch.Tensor, P: int, T: int, C: int, dtype: torch.dtype,
|
|
173
|
+
steps: tuple[int, int] | None = None) -> torch.Tensor:
|
|
174
|
+
"""[P * T, ...] stream rows as [n * P * C, D] time-major chunk rows in ``dtype``, for
|
|
175
|
+
the n steps ``range(*steps)`` (default: all cdiv(T, C) of them), with zero rows past
|
|
176
|
+
each stream's end. A view of ``x`` when the layouts coincide (P == 1, no row past the
|
|
177
|
+
end, same dtype)."""
|
|
178
|
+
first, stop = steps or (0, triton.cdiv(T, C))
|
|
179
|
+
x = x.reshape(P * T, -1)
|
|
180
|
+
if P == 1 and stop * C <= T and x.dtype == dtype and x.is_contiguous():
|
|
181
|
+
return x[first * C:stop * C]
|
|
182
|
+
rows = (stop - first) * P * C
|
|
183
|
+
out = torch.empty(rows, x.shape[1], device=x.device, dtype=dtype)
|
|
184
|
+
_launch_stage(x.contiguous(), out, rows, first * P * C, P, T, C, True)
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def unstage_time_major(x: torch.Tensor, P: int, T: int, C: int, dtype: torch.dtype,
|
|
189
|
+
steps: tuple[int, int] | None = None,
|
|
190
|
+
out: torch.Tensor | None = None) -> torch.Tensor:
|
|
191
|
+
"""The inverse of ``stage_time_major``: writes the time-major rows of ``steps`` into
|
|
192
|
+
their rows of ``out`` ([P * T, D], allocated in ``dtype`` when None) and returns it.
|
|
193
|
+
Returns ``x`` itself when the layouts coincide and no ``out`` is given."""
|
|
194
|
+
first, stop = steps or (0, triton.cdiv(T, C))
|
|
195
|
+
rows = (stop - first) * P * C
|
|
196
|
+
x = x.reshape(rows, -1)
|
|
197
|
+
if out is None:
|
|
198
|
+
if P == 1 and rows == T and x.dtype == dtype:
|
|
199
|
+
return x
|
|
200
|
+
out = torch.empty(P * T, x.shape[1], device=x.device, dtype=dtype)
|
|
201
|
+
_launch_stage(x, out, rows, first * P * C, P, T, C, False)
|
|
202
|
+
return out
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ============================================================
|
|
206
|
+
# Elementwise kernels around the backward matmuls
|
|
207
|
+
# ============================================================
|
|
208
|
+
|
|
209
|
+
@triton.jit
|
|
210
|
+
def _fused_pre_bmm_residual_kernel(
|
|
211
|
+
residual_ptr, beta_ptr, d_dva_ptr,
|
|
212
|
+
u_ptr, # [N * C, dim] f32
|
|
213
|
+
grad_v_ptr, # [N * C, dim] bank dtype
|
|
214
|
+
grad_beta_part_ptr, # [N * C] f32
|
|
215
|
+
dim: tl.constexpr, BD: tl.constexpr,
|
|
216
|
+
):
|
|
217
|
+
"""u = beta * residual, grad_v = beta * d_dva and grad_beta_part = <residual, d_dva>,
|
|
218
|
+
with residual = v - retrieved. One program per token."""
|
|
219
|
+
t = tl.program_id(0).to(tl.int64)
|
|
220
|
+
beta = tl.load(beta_ptr + t)
|
|
221
|
+
acc = tl.zeros([1], dtype=tl.float32)
|
|
222
|
+
for ds in range(0, dim, BD):
|
|
223
|
+
do = ds + tl.arange(0, BD)
|
|
224
|
+
m = do < dim
|
|
225
|
+
res = tl.load(residual_ptr + t * dim + do, mask=m, other=0.0)
|
|
226
|
+
dva = tl.load(d_dva_ptr + t * dim + do, mask=m, other=0.0)
|
|
227
|
+
tl.store(u_ptr + t * dim + do, beta * res, mask=m)
|
|
228
|
+
tl.store(grad_v_ptr + t * dim + do, (beta * dva).to(grad_v_ptr.dtype.element_ty),
|
|
229
|
+
mask=m)
|
|
230
|
+
acc += tl.sum(res * dva)
|
|
231
|
+
tl.store(grad_beta_part_ptr + t, tl.sum(acc))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _pre_bmm(residual, beta, d_dva, grad_v):
|
|
235
|
+
"""Returns (u, grad_beta_part) [N, C, dim] / [N, C, 1] f32 and writes grad_v."""
|
|
236
|
+
N, C, dim = residual.shape
|
|
237
|
+
u = torch.empty_like(residual)
|
|
238
|
+
grad_beta_part = torch.empty(N, C, 1, device=residual.device, dtype=torch.float32)
|
|
239
|
+
_device.tbind(_fused_pre_bmm_residual_kernel, (N * C,), beta)(
|
|
240
|
+
residual, beta, d_dva, u, grad_v, grad_beta_part,
|
|
241
|
+
dim=dim, BD=min(1024, triton.next_power_of_2(dim)))
|
|
242
|
+
return u, grad_beta_part
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@triton.jit
|
|
246
|
+
def _fused_post_bmm_kernel(
|
|
247
|
+
d_m_ptr, a_ptr, beta_ptr, grad_beta_part_ptr, d_a_ptr, grad_beta_ptr,
|
|
248
|
+
C: tl.constexpr, BLOCK_C: tl.constexpr,
|
|
249
|
+
):
|
|
250
|
+
"""Strict lower triangle of d_m: d_a = beta * d_m and
|
|
251
|
+
grad_beta = grad_beta_part + <A, d_m> per row. One program per (chunk, row)."""
|
|
252
|
+
pid = tl.program_id(0).to(tl.int64)
|
|
253
|
+
batch = pid // C
|
|
254
|
+
row = pid % C
|
|
255
|
+
beta = tl.load(beta_ptr + batch * C + row).to(tl.float32)
|
|
256
|
+
grad_beta = tl.load(grad_beta_part_ptr + batch * C + row).to(tl.float32)
|
|
257
|
+
base = batch * C * C + row * C
|
|
258
|
+
col_offs = tl.arange(0, BLOCK_C)
|
|
259
|
+
for col_start in range(0, C, BLOCK_C):
|
|
260
|
+
col = col_start + col_offs
|
|
261
|
+
m = col < row
|
|
262
|
+
d_m = tl.load(d_m_ptr + base + col, mask=m, other=0.0)
|
|
263
|
+
a = tl.load(a_ptr + base + col, mask=m, other=0.0)
|
|
264
|
+
grad_beta += tl.sum(a * d_m)
|
|
265
|
+
tl.store(d_a_ptr + base + col, tl.where(m, beta * d_m, 0.0), mask=col < C)
|
|
266
|
+
tl.store(grad_beta_ptr + batch * C + row, grad_beta)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _post_bmm(d_m, a, beta, grad_beta_part, grad_beta):
|
|
270
|
+
"""Returns d_a [N, C, C] and writes grad_beta [N * C]."""
|
|
271
|
+
N, C, _ = d_m.shape
|
|
272
|
+
d_a = torch.empty_like(d_m)
|
|
273
|
+
_device.tbind(_fused_post_bmm_kernel, (N * C,), d_m)(
|
|
274
|
+
d_m, a, beta, grad_beta_part, d_a, grad_beta,
|
|
275
|
+
C=C, BLOCK_C=min(256, triton.next_power_of_2(C)), num_warps=4)
|
|
276
|
+
return d_a
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ============================================================
|
|
280
|
+
# The chunked write/read
|
|
281
|
+
# ============================================================
|
|
282
|
+
|
|
283
|
+
class _Layout(NamedTuple):
|
|
284
|
+
P: int # parallel streams
|
|
285
|
+
T: int # tokens per stream
|
|
286
|
+
C: int # chunk size
|
|
287
|
+
Nt: int # chunks per stream (Phase-2 steps)
|
|
288
|
+
W: int # write slots per token
|
|
289
|
+
R: int # read slots per token
|
|
290
|
+
G: int # gate width, 1 or W
|
|
291
|
+
hd: int # head_dim
|
|
292
|
+
stream_offset: int # slot offset between streams; 0 when all share one id space
|
|
293
|
+
num_keys: int # slot ids per stream
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _layout(memory, k_idx, k_val, v, beta, g, q_idx, q_val, chunk_size, num_memory_slots,
|
|
297
|
+
num_parallel) -> _Layout:
|
|
298
|
+
"""Validates the call and returns its static layout (once per op call; _admit then checks the route)."""
|
|
299
|
+
if memory.dtype not in (torch.bfloat16, torch.float32):
|
|
300
|
+
raise TypeError(f"memory must be bfloat16 or float32, got {memory.dtype}")
|
|
301
|
+
if memory.ndim != 2 or not memory.is_cuda or not memory.is_contiguous():
|
|
302
|
+
raise ValueError("memory must be a contiguous CUDA tensor [num_slots, head_dim]")
|
|
303
|
+
for name, n in (("chunk_size", chunk_size), ("num_memory_slots", num_memory_slots),
|
|
304
|
+
("num_parallel", num_parallel)):
|
|
305
|
+
if isinstance(n, bool) or not isinstance(n, int):
|
|
306
|
+
raise TypeError(f"{name} must be an int, got {n!r}")
|
|
307
|
+
if not MIN_CHUNK <= chunk_size <= MAX_CHUNK:
|
|
308
|
+
raise ValueError(f"chunk_size must be in {MIN_CHUNK}..{MAX_CHUNK}, got {chunk_size}")
|
|
309
|
+
if num_memory_slots < 0:
|
|
310
|
+
raise ValueError(f"num_memory_slots must be >= 0 (0: the whole bank), got "
|
|
311
|
+
f"{num_memory_slots}")
|
|
312
|
+
P = num_parallel
|
|
313
|
+
if P < 1:
|
|
314
|
+
raise ValueError(f"num_parallel must be positive, got {P}")
|
|
315
|
+
if k_idx.ndim == 3:
|
|
316
|
+
if k_idx.shape[0] != P:
|
|
317
|
+
raise ValueError(f"3-D inputs must have num_parallel={P} streams along dim 0, "
|
|
318
|
+
f"got k_idx of shape {tuple(k_idx.shape)}")
|
|
319
|
+
T = k_idx.shape[1]
|
|
320
|
+
else:
|
|
321
|
+
if k_idx.shape[0] % P:
|
|
322
|
+
raise ValueError(f"{k_idx.shape[0]} tokens do not split into num_parallel={P} "
|
|
323
|
+
"equal streams")
|
|
324
|
+
T = k_idx.shape[0] // P
|
|
325
|
+
if k_idx.dtype not in (torch.int32, torch.int64) or q_idx.dtype not in (torch.int32,
|
|
326
|
+
torch.int64):
|
|
327
|
+
raise TypeError(f"k_idx and q_idx must be int32 or int64, got {k_idx.dtype} and "
|
|
328
|
+
f"{q_idx.dtype}")
|
|
329
|
+
dev = memory.get_device()
|
|
330
|
+
for name, t in (("k_idx", k_idx), ("k_val", k_val), ("v", v), ("beta", beta), ("g", g),
|
|
331
|
+
("q_idx", q_idx), ("q_val", q_val)):
|
|
332
|
+
if not t.is_cuda or t.get_device() != dev:
|
|
333
|
+
raise ValueError(f"{name} is on {t.device}, memory on {memory.device}")
|
|
334
|
+
num_slots, hd = memory.shape
|
|
335
|
+
if num_slots < 1 or hd < 1:
|
|
336
|
+
raise ValueError(f"memory needs at least one slot and one column, got shape "
|
|
337
|
+
f"{tuple(memory.shape)}")
|
|
338
|
+
if num_slots >= 2**31:
|
|
339
|
+
raise ValueError(f"memory has {num_slots} rows; slot ids must fit int32")
|
|
340
|
+
W, R, G = k_idx.shape[-1], q_idx.shape[-1], g.shape[-1]
|
|
341
|
+
lead = k_idx.shape[:-1]
|
|
342
|
+
for name, t, width in (("k_val", k_val, W), ("v", v, hd), ("beta", beta, 1),
|
|
343
|
+
("g", g, G), ("q_idx", q_idx, R), ("q_val", q_val, R)):
|
|
344
|
+
if t.shape[:-1] != lead or t.shape[-1] != width:
|
|
345
|
+
raise ValueError(f"{name}: expected shape {(*lead, width)}, got {tuple(t.shape)}")
|
|
346
|
+
if T < 1 or not (1 <= W <= MAX_WIDTH and 1 <= R <= MAX_WIDTH):
|
|
347
|
+
raise ValueError(f"need T >= 1 and 1..{MAX_WIDTH} write/read slots per token, "
|
|
348
|
+
f"got T={T} W={W} R={R}")
|
|
349
|
+
if G not in (1, W):
|
|
350
|
+
raise ValueError(f"g must hold 1 or W={W} gates per token, got {G}")
|
|
351
|
+
Nt = -(-T // chunk_size)
|
|
352
|
+
if P * Nt * chunk_size >= 2**31:
|
|
353
|
+
raise ValueError(f"{P} streams of {T} tokens, padded to chunks of {chunk_size}, "
|
|
354
|
+
"exceed the 2**31 - 1 token rows the kernels address")
|
|
355
|
+
sph = num_memory_slots or num_slots
|
|
356
|
+
if num_slots % sph:
|
|
357
|
+
raise ValueError(f"num_memory_slots={sph} does not divide the {num_slots} bank slots")
|
|
358
|
+
streams = num_slots // sph
|
|
359
|
+
if P > 1 and streams not in (1, P):
|
|
360
|
+
raise ValueError(
|
|
361
|
+
f"memory holds {streams} streams of {sph} slots but num_parallel={P}: each "
|
|
362
|
+
f"parallel stream needs its own slot range (num_slots == num_parallel * "
|
|
363
|
+
f"num_memory_slots) or one shared bank (num_memory_slots == num_slots).")
|
|
364
|
+
own = P > 1 and streams == P
|
|
365
|
+
return _Layout(P, T, chunk_size, Nt, W, R, G, hd, sph if own else 0,
|
|
366
|
+
sph if own else num_slots)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ============================================================
|
|
370
|
+
# Admission: fp8 / int4 snapshot shapes and calls without a native route (refused before any work)
|
|
371
|
+
# ============================================================
|
|
372
|
+
# The quantized DW (mover_fx_cuda) and Phase A (gx_pa2_cuda) take fp8 / int4 snapshot rows only for heads of at most
|
|
373
|
+
# QUANT_TAIL_MAX_HEAD_DIM columns in ceil(head_dim / 128) equal slices of 16-byte rows, and int4 rows of float32 heads only
|
|
374
|
+
# in whole 8-column groups: the other heads (T0-T3, S4) are refused on saving calls. No-grad calls never quantize.
|
|
375
|
+
QUANT_TAIL_MAX_HEAD_DIM = 16384 # the quantized forward's head bound (cuda/mover_fx_cuda._QUANT_MAX_HEAD_DIM)
|
|
376
|
+
_DTYPE_NAMES = {torch.bfloat16: "bfloat16", torch.float32: "float32"}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class UnsupportedConfigurationError(ValueError):
|
|
380
|
+
"""A configuration this release refuses up front, before any work. The message names the reason and what is
|
|
381
|
+
supported; ``tail_class`` holds the class (quant_tail_class's "T0".."T3" / "S4", native_route_class's "H" / "S" /
|
|
382
|
+
"P"; for logs and tests, not in the message)."""
|
|
383
|
+
|
|
384
|
+
tail_class = None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
QUANT_TAIL_CLASSES = {
|
|
388
|
+
"T0": "more than 16384 columns",
|
|
389
|
+
"T1": "column slices whose rows are not 16-byte multiples",
|
|
390
|
+
"T2": "uneven column slices",
|
|
391
|
+
"T3": "head_dim not a multiple of 4",
|
|
392
|
+
}
|
|
393
|
+
QUANT_TAIL_MSG = (
|
|
394
|
+
"sparse_delta_memory: snapshot_quant={quant!r} on a {dtype} bank with head_dim {hd} is not supported for calls "
|
|
395
|
+
"that may need a backward ({what}); support is planned for a later release. Supported for such calls: "
|
|
396
|
+
"head_dim <= 16384 that splits into ceil(head_dim / 128) equal column slices whose width is a multiple of 8 "
|
|
397
|
+
"(bfloat16 bank, or int4 on a float32 bank) or of 4 (fp8 on a float32 bank), e.g. 64, 128, 480, 960, 1024, 2048; "
|
|
398
|
+
"snapshot_quant='none' supports every head_dim, and calls without a backward (torch.no_grad(), inference) are not "
|
|
399
|
+
"limited.")
|
|
400
|
+
QUANT_S4_MSG = (
|
|
401
|
+
"sparse_delta_memory: snapshot_quant={quant!r} on a {dtype} bank with head_dim {hd} is not supported for calls "
|
|
402
|
+
"that may need a backward (column slices {sw} columns wide, not a multiple of 8); native support is planned for "
|
|
403
|
+
"2.0.1. Supported for such calls: snapshot_quant='fp8' or 'none' with this head_dim, or int4 with a head_dim that "
|
|
404
|
+
"splits into ceil(head_dim / 128) equal column slices whose width is a multiple of 8, e.g. 64, 128, 480, 960, 1024, "
|
|
405
|
+
"2048; calls without a backward (torch.no_grad(), inference) are not limited.")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def quant_tail_class(bank_dtype, head_dim, snapshot_quant, compact_rows=0, device=None):
|
|
409
|
+
"""The class of a saving call with ``snapshot_quant`` rows on a ``bank_dtype`` bank of ``head_dim`` columns:
|
|
410
|
+
"T0".."T3" (QUANT_TAIL_CLASSES) or, when the caller passes the call's ``device`` (not read: no GPU is refused), "S4";
|
|
411
|
+
else None. Shapes, dtype and mode only; ``compact_rows`` is converted (int()) but not bounded (64-bit snapshot rows)."""
|
|
412
|
+
if snapshot_quant not in ("fp8", "int4"):
|
|
413
|
+
return None
|
|
414
|
+
hd = int(head_dim)
|
|
415
|
+
if hd > 2 ** 20:
|
|
416
|
+
return None
|
|
417
|
+
int(compact_rows)
|
|
418
|
+
if hd % 4:
|
|
419
|
+
return "T3"
|
|
420
|
+
nsl = -(-hd // 128)
|
|
421
|
+
sw = hd // nsl
|
|
422
|
+
if hd % nsl or sw % 4:
|
|
423
|
+
return "T2"
|
|
424
|
+
es = 2 if bank_dtype == torch.bfloat16 else 4
|
|
425
|
+
if (sw * es) % 16 or (hd * es) % 16:
|
|
426
|
+
return "T1"
|
|
427
|
+
if hd > QUANT_TAIL_MAX_HEAD_DIM:
|
|
428
|
+
return "T0"
|
|
429
|
+
if device is not None and snapshot_quant == "int4" and bank_dtype == torch.float32 and hd >= 1 and sw % 8:
|
|
430
|
+
return "S4"
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def refuse_quant_tail(bank_dtype, head_dim, snapshot_quant, compact_rows=0, device=None) -> None:
|
|
435
|
+
"""Raises UnsupportedConfigurationError for a saving call of a quant_tail_class class (same arguments)."""
|
|
436
|
+
cls = quant_tail_class(bank_dtype, head_dim, snapshot_quant, compact_rows, device=device)
|
|
437
|
+
if cls is not None:
|
|
438
|
+
dtype, hd = _DTYPE_NAMES.get(bank_dtype, str(bank_dtype)), int(head_dim)
|
|
439
|
+
err = UnsupportedConfigurationError(
|
|
440
|
+
QUANT_S4_MSG.format(quant=snapshot_quant, dtype=dtype, hd=hd, sw=hd // -(-hd // 128)) if cls == "S4" else
|
|
441
|
+
QUANT_TAIL_MSG.format(quant=snapshot_quant, dtype=dtype, hd=hd, what=QUANT_TAIL_CLASSES[cls]))
|
|
442
|
+
err.tail_class = cls
|
|
443
|
+
raise err
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# Calls without a native route (2.0.0: no fallback route): native_route_class composes the predicates the forward and
|
|
447
|
+
# the backward route on, so it refuses exactly the calls that took a fallback before; the bounds (2^20 columns, 2^31
|
|
448
|
+
# slots) only NAME the class. Modes: no-grad, saving (SDMWriteRead.forward) and training (saving + the CUDA backward).
|
|
449
|
+
NATIVE_CLASSES = {
|
|
450
|
+
"H": "head_dim {hd} is above 1,048,576 (2^20 columns)",
|
|
451
|
+
"S": "the bank has {slots} slots (2^31 or more)",
|
|
452
|
+
"P": "no native kernel plan for this shape fits this GPU ({gpu})",
|
|
453
|
+
}
|
|
454
|
+
NATIVE_MSG = ("sparse_delta_memory: no native route for this call: {what}. Bank {dtype} [{slots}, {hd}], chunk_size {C}, "
|
|
455
|
+
"snapshot_quant {quant}, {mode}. This release has no fallback route (removed in 2.0.0): {remedy}.")
|
|
456
|
+
NATIVE_MODES = {"training": "training call", "saving": "saving call (SDMWriteRead.apply without autograd recording)",
|
|
457
|
+
"no-grad": "no-grad call"}
|
|
458
|
+
# Each class's own remedy (NATIVE_MSG's {remedy}); a training / saving call whose no-grad route exists also gets
|
|
459
|
+
# NATIVE_NOGRAD_OK (checked: native_route_class in no-grad mode is None).
|
|
460
|
+
NATIVE_REMEDIES = {
|
|
461
|
+
"H": "use a head_dim of at most 1,048,576",
|
|
462
|
+
"S": "use a bank of fewer than 2^31 slots",
|
|
463
|
+
"P": "use a smaller chunk_size or another head_dim",
|
|
464
|
+
}
|
|
465
|
+
NATIVE_P_GPU = ", or an H100 / B200 / GB300 GPU (more shared memory per block)" # P on the A100 / RTX backends
|
|
466
|
+
NATIVE_P_QUANT = ", or snapshot_quant='none'" # P of a saving fp8 / int4 call
|
|
467
|
+
NATIVE_NOGRAD_OK = {"training": "; the same call without autograd (torch.no_grad(), inference) runs natively",
|
|
468
|
+
"saving": "; sdm_write_read runs it natively as a no-grad call"}
|
|
469
|
+
_NATIVE_MODS = None # (backward, forward, movers, cuda.mover_da2_cuda): imported once, at the first route lookup
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _native_preds():
|
|
473
|
+
"""The routing predicates native_route_class composes, as currently bound (part of route_token)."""
|
|
474
|
+
global _NATIVE_MODS
|
|
475
|
+
if _NATIVE_MODS is None:
|
|
476
|
+
from . import backward as _bw, forward as _fw, movers as _mv
|
|
477
|
+
from .cuda import mover_da2_cuda as _da2
|
|
478
|
+
_NATIVE_MODS = (_bw, _fw, _mv, _da2)
|
|
479
|
+
bw, fw, mv, da2 = _NATIVE_MODS
|
|
480
|
+
return (mv.dw_admits, mv.gds_admits, mv.tail_admits, bw.supported, da2._shape_supported, fw._fxq_rows,
|
|
481
|
+
fw.bf16_route)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def route_token():
|
|
485
|
+
"""What a call's admission and route depend on beside its own arguments and shapes: the forced test row and the
|
|
486
|
+
routing predicates as bound (a test that patches one gets fresh answers from the memos keyed on it)."""
|
|
487
|
+
return _device._FORCED, _native_preds()
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def native_route_class(memory, C, snapshot_quant="none", compact_rows=0, needs_backward=False, grad_bank_fp32=False,
|
|
491
|
+
saving=None):
|
|
492
|
+
"""None when ``memory`` with chunks of ``C`` runs natively end to end in the given mode, else the NATIVE_CLASSES key
|
|
493
|
+
(``compact_rows`` = P * Nt * C * W; saving=None: saving = needs_backward)."""
|
|
494
|
+
from . import backward as _bw, forward as _fw, movers as _mv
|
|
495
|
+
from .cuda import mover_da2_cuda as _da2
|
|
496
|
+
needs_backward = bool(needs_backward)
|
|
497
|
+
saving = needs_backward or bool(saving)
|
|
498
|
+
C, rows = int(C), int(compact_rows)
|
|
499
|
+
hd, slots = int(memory.shape[-1]), int(memory.shape[0])
|
|
500
|
+
# the forward's DW mode (forward.forward_steps: quant iff save); "bf16" runs as "none" where effective_snapshot_quant says
|
|
501
|
+
q = effective_snapshot_quant(memory, snapshot_quant, needs_backward) if saving else "none"
|
|
502
|
+
qn = "none" if q == "bf16" else q # bf16 rows (float32 banks): 'none''s DW and GDS (forward.bf16_route)
|
|
503
|
+
native = _mv.dw_admits(memory, C, qn) # else movers.make_mover_dw has no native DW (NO_NATIVE_MOVER_MSG)
|
|
504
|
+
if native and saving:
|
|
505
|
+
if q == "bf16":
|
|
506
|
+
# the saving forward's bf16 compact rows (mover_fx_cuda's WR walk) and the backward reading them (gx_pa2's
|
|
507
|
+
# bf16-snapshot rows): bf16_route
|
|
508
|
+
native = _fw.bf16_route(memory, C, rows)
|
|
509
|
+
elif q != "none" or hd % 4 or not _da2._shape_supported(memory, C):
|
|
510
|
+
# compact rows (layout 2) required: a tail head's DW (movers.make_mover_dw: tail_off), backward._supported
|
|
511
|
+
# (hd % 4 != 0) and every quantized backward (backward.sdm_backward) read them; the saving forward writes them
|
|
512
|
+
# iff backward.supported admits the worst grad dtype (forward.forward_steps), fp8 / int4 also need the
|
|
513
|
+
# quantized DW; otherwise it writes layout 0, which no native DW takes for these calls
|
|
514
|
+
native = _bw.supported(memory, q, C=C, grad_f32_possible=True, rows=rows) and (
|
|
515
|
+
q == "none" or _fw._fxq_rows(memory, C, q))
|
|
516
|
+
if native and needs_backward: # the CUDA backward (backward._supported) and its GDS (movers.make_mover_gds)
|
|
517
|
+
gdt = torch.float32 if (grad_bank_fp32 and memory.dtype == torch.bfloat16) else memory.dtype
|
|
518
|
+
native = (_bw.supported(memory, q, C=C, grad_f32_possible=bool(grad_bank_fp32), rows=rows)
|
|
519
|
+
and _mv.gds_admits(memory, gdt, C, qn))
|
|
520
|
+
if native:
|
|
521
|
+
return None
|
|
522
|
+
if hd > 2 ** 20:
|
|
523
|
+
return "H"
|
|
524
|
+
if slots >= 2 ** 31:
|
|
525
|
+
return "S"
|
|
526
|
+
return "P"
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def refuse_non_native(memory, C, snapshot_quant="none", compact_rows=0, needs_backward=False, grad_bank_fp32=False,
|
|
530
|
+
saving=None):
|
|
531
|
+
"""Raises UnsupportedConfigurationError (``tail_class`` = the NATIVE_CLASSES key) for a call without a native route
|
|
532
|
+
(native_route_class, same arguments); a saving "bf16" call on a float32 bank raises refuse_bf16_without_route's text.
|
|
533
|
+
First the device's class: a GPU outside the supported matrix (_device's RuntimeError), RTX_CHUNK_MSG on RTX."""
|
|
534
|
+
if _devmemo.backend(memory.device) == "rtx" and C > RTX_MAX_CHUNK:
|
|
535
|
+
raise ValueError(RTX_CHUNK_MSG.format(chunk_size=C))
|
|
536
|
+
saving = bool(needs_backward) or bool(saving)
|
|
537
|
+
cls = native_route_class(memory, C, snapshot_quant, compact_rows, needs_backward, grad_bank_fp32, saving)
|
|
538
|
+
if cls is None:
|
|
539
|
+
return
|
|
540
|
+
if saving and effective_snapshot_quant(memory, snapshot_quant, needs_backward) == "bf16":
|
|
541
|
+
refuse_bf16_without_route(memory, C, int(compact_rows)) # bf16 on a float32 bank: its own text first
|
|
542
|
+
hd, slots = int(memory.shape[-1]), int(memory.shape[0])
|
|
543
|
+
gpu = torch.cuda.get_device_name(memory.device) if cls == "P" else ""
|
|
544
|
+
what = NATIVE_CLASSES[cls].format(hd=hd, slots=slots, rows=int(compact_rows), gpu=gpu)
|
|
545
|
+
mode = "training" if needs_backward else "saving" if saving else "no-grad"
|
|
546
|
+
remedy = NATIVE_REMEDIES[cls]
|
|
547
|
+
if cls == "P":
|
|
548
|
+
remedy += (NATIVE_P_QUANT if saving and snapshot_quant != "none" else "") + (
|
|
549
|
+
NATIVE_P_GPU if _device.backend(memory.device) != "modern" else "")
|
|
550
|
+
if mode != "no-grad" and native_route_class(memory, C) is None:
|
|
551
|
+
remedy += NATIVE_NOGRAD_OK[mode]
|
|
552
|
+
err = UnsupportedConfigurationError(NATIVE_MSG.format(
|
|
553
|
+
what=what, dtype=_DTYPE_NAMES.get(memory.dtype, str(memory.dtype)), slots=slots, hd=hd, C=int(C),
|
|
554
|
+
quant=snapshot_quant if saving else "none", mode=NATIVE_MODES[mode], remedy=remedy))
|
|
555
|
+
err.tail_class = cls
|
|
556
|
+
raise err
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# The op's admission memo (item 3): the call signatures _admit admitted. Refused calls are never stored.
|
|
560
|
+
_NATIVE_MEMO = {}
|
|
561
|
+
_NATIVE_MEMO_MAX = 4096
|
|
562
|
+
_NATIVE_LOCK = threading.Lock()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _admit(memory, lay, snapshot_quant="none", needs_backward=False, saving=False, grad_bank_fp32=False) -> None:
|
|
566
|
+
"""The op's checks after _layout, once per call signature: a saving call's T0-T3 / S4 refusals, then the native
|
|
567
|
+
route's. Only admitted signatures are stored, so a refused call is checked, and raises, at every call. Returns the
|
|
568
|
+
snapshot mode the call runs (effective_snapshot_quant), stored with the admission."""
|
|
569
|
+
rows = lay.P * lay.Nt * lay.C * lay.W if saving else 0
|
|
570
|
+
key = (memory.device.index, memory.dtype, tuple(memory.shape), lay.C, rows, snapshot_quant, bool(needs_backward),
|
|
571
|
+
bool(saving), bool(grad_bank_fp32), route_token())
|
|
572
|
+
hit = _NATIVE_MEMO.get(key)
|
|
573
|
+
if hit is not None:
|
|
574
|
+
return hit
|
|
575
|
+
if saving:
|
|
576
|
+
refuse_quant_tail(memory.dtype, lay.hd, snapshot_quant, rows, device=memory.device)
|
|
577
|
+
refuse_non_native(memory, lay.C, snapshot_quant, rows, needs_backward, grad_bank_fp32, saving)
|
|
578
|
+
quant = effective_snapshot_quant(memory, snapshot_quant, needs_backward)
|
|
579
|
+
with _NATIVE_LOCK:
|
|
580
|
+
if len(_NATIVE_MEMO) < _NATIVE_MEMO_MAX:
|
|
581
|
+
_NATIVE_MEMO[key] = quant
|
|
582
|
+
return quant
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# The token gather's geometry (its row-group split fixes the rounding) is a function of the shapes only, computed for
|
|
586
|
+
# this reference SM count instead of the device's, so the bits do not depend on the GPU's SM count.
|
|
587
|
+
_DET_GEOM_REF_SMS = 148
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def pinned_gather_geometry(head_dim, dtype, width, tokens):
|
|
591
|
+
"""Shape-only geometry of the token gathers (forward and backward): REF_SMS is a constant, not the device's."""
|
|
592
|
+
return gather_geometry(head_dim, dtype, width, tokens, num_sms=_DET_GEOM_REF_SMS)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _num_sms(device_index: int) -> int:
|
|
596
|
+
return _device.num_sms(device_index) # (cached per device there; a forced test row overrides it)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _nonempty(t):
|
|
600
|
+
"""``t``, or None when it is None or empty (a Phase-1 field that was not requested)."""
|
|
601
|
+
return t if t is not None and t.numel() > 0 else None
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
_NO_FILL_LOCK = threading.Lock()
|
|
605
|
+
_NO_FILL_DEPTH = 0
|
|
606
|
+
_NO_FILL_PREV = None
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@contextlib.contextmanager
|
|
610
|
+
def _no_det_fill():
|
|
611
|
+
"""Allocate the op's own buffers without torch's deterministic NaN fill.
|
|
612
|
+
|
|
613
|
+
Under ``torch.use_deterministic_algorithms(True)`` torch fills every ``torch.empty`` with
|
|
614
|
+
NaN (``torch.utils.deterministic.fill_uninitialized_memory``, default True). Every buffer
|
|
615
|
+
this module allocates is written by its kernels before anything reads it (allocator-poison
|
|
616
|
+
tested), so the fill only costs bandwidth and launches. Results are unchanged.
|
|
617
|
+
|
|
618
|
+
The flag is process-global: a depth counter under a lock keeps it False while any SDM call
|
|
619
|
+
(on any thread, nested or overlapping) is inside, and the outermost exit restores the value
|
|
620
|
+
seen at the first entry. Other threads' allocations in that window skip the fill as well,
|
|
621
|
+
which never changes results.
|
|
622
|
+
"""
|
|
623
|
+
global _NO_FILL_DEPTH, _NO_FILL_PREV
|
|
624
|
+
det = torch.utils.deterministic
|
|
625
|
+
with _NO_FILL_LOCK:
|
|
626
|
+
if _NO_FILL_DEPTH == 0:
|
|
627
|
+
_NO_FILL_PREV = det.fill_uninitialized_memory
|
|
628
|
+
det.fill_uninitialized_memory = False
|
|
629
|
+
_NO_FILL_DEPTH += 1
|
|
630
|
+
try:
|
|
631
|
+
yield
|
|
632
|
+
finally:
|
|
633
|
+
with _NO_FILL_LOCK:
|
|
634
|
+
_NO_FILL_DEPTH -= 1
|
|
635
|
+
if _NO_FILL_DEPTH == 0:
|
|
636
|
+
det.fill_uninitialized_memory = _NO_FILL_PREV
|
|
637
|
+
_NO_FILL_PREV = None
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
@contextlib.contextmanager
|
|
641
|
+
def _kernel_context():
|
|
642
|
+
"""The context of the op's own work: no torch dispatch mode sees its internal ops (the
|
|
643
|
+
op saves its own backward state, so activation-checkpoint policies have nothing to
|
|
644
|
+
cache there), autocast is off (the backward matmuls keep their float32 inputs), and the
|
|
645
|
+
op's scratch is allocated without the deterministic NaN fill (``_no_det_fill``)."""
|
|
646
|
+
with _disable_current_modes(), torch.autocast("cuda", enabled=False), _no_det_fill():
|
|
647
|
+
yield
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _sdm_forward(memory, k_idx, k_val, v, beta, g, q_idx, q_val, lay: _Layout,
|
|
651
|
+
snapshot_quant: str, save: bool, grad_bank_fp32=None, info=None):
|
|
652
|
+
"""Runs the forward and returns the readings [P * T, hd] and, when ``save``, the
|
|
653
|
+
tensors the backward reads (see ``SDMWriteRead.backward``).
|
|
654
|
+
|
|
655
|
+
Without ``save`` the steps run in slabs and the streams in groups (see SLAB_BYTES,
|
|
656
|
+
NOGRAD_SLAB_FACTOR and STEP_BYTES). Both are exact: Phase 1 treats every chunk on its own,
|
|
657
|
+
and streams own disjoint slots. A group runs with global slot ids, which is Phase 1's
|
|
658
|
+
shared-bank mode. grad_bank_fp32: the op's setting (saving calls; None: unknown).
|
|
659
|
+
"""
|
|
660
|
+
inputs = (k_idx, k_val, v, beta, g, q_idx, q_val)
|
|
661
|
+
if save:
|
|
662
|
+
_record(streams=lay.P, steps=lay.Nt, slab_steps=lay.Nt, group_streams=lay.P)
|
|
663
|
+
return _forward.forward_steps(memory, *inputs, lay, snapshot_quant, True, (0, lay.Nt),
|
|
664
|
+
grad_bank_fp32=grad_bank_fp32, info=info)
|
|
665
|
+
es = memory.element_size()
|
|
666
|
+
group = max(1, min(lay.P, STEP_BYTES // (lay.C * lay.W * lay.hd * es)))
|
|
667
|
+
# About 64 B per slot entry (staged ids and weights, Phase-1 state and scratch), 16 B
|
|
668
|
+
# per element of the chunk matrices (A, QK, X, QX) and two token rows (v, readings).
|
|
669
|
+
per_step = group * lay.C * ((lay.W + lay.R) * 64 + 16 * lay.C + 2 * lay.hd * es)
|
|
670
|
+
fill = -(-SLAB_CHUNKS_PER_SM * _num_sms(memory.device.index) // group)
|
|
671
|
+
slab = min(lay.Nt, max(1, fill, NOGRAD_SLAB_FACTOR * SLAB_BYTES // per_step))
|
|
672
|
+
_record(streams=lay.P, steps=lay.Nt, slab_steps=slab, group_streams=group)
|
|
673
|
+
readings = torch.empty(lay.P * lay.T, lay.hd, device=memory.device, dtype=memory.dtype)
|
|
674
|
+
stream_rows = [t.reshape(lay.P * lay.T, -1) for t in inputs]
|
|
675
|
+
for p0 in range(0, lay.P, group):
|
|
676
|
+
rows = slice(p0 * lay.T, min(lay.P, p0 + group) * lay.T)
|
|
677
|
+
sub = lay if group == lay.P else lay._replace(
|
|
678
|
+
P=rows.stop // lay.T - p0, stream_offset=0, num_keys=memory.shape[0])
|
|
679
|
+
for s0 in range(0, lay.Nt, slab):
|
|
680
|
+
_forward.forward_steps(memory, *(t[rows] for t in stream_rows), sub, "none", False,
|
|
681
|
+
(s0, min(lay.Nt, s0 + slab)), readings[rows],
|
|
682
|
+
geom_tokens=lay.P * lay.C)
|
|
683
|
+
return readings, None
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
BF16_ROUTE_MSG = (
|
|
687
|
+
'sparse_delta_memory: snapshot_quant="bf16" on a float32 bank with head_dim {hd}, chunk_size {C} and {rows} compact '
|
|
688
|
+
'snapshot rows has no CUDA route on this device ({dev}); it runs only on the CUDA kernels, with no fallback. '
|
|
689
|
+
'snapshot_quant="none" keeps a float32 snapshot, and calls without a backward (torch.no_grad(), inference) are not '
|
|
690
|
+
'limited.')
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def refuse_bf16_without_route(memory, C, rows) -> None:
|
|
694
|
+
"""Raises UnsupportedConfigurationError (BF16_ROUTE_MSG) for a saving "bf16" call on a float32 bank (``rows`` = P * Nt
|
|
695
|
+
* C * W compact snapshot rows) without its native CUDA route (forward.bf16_route; no fallback)."""
|
|
696
|
+
if not _forward.bf16_route(memory, C, rows):
|
|
697
|
+
raise UnsupportedConfigurationError(BF16_ROUTE_MSG.format(hd=int(memory.shape[-1]), C=int(C), rows=int(rows),
|
|
698
|
+
dev=str(getattr(memory, "device", "?"))))
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def effective_snapshot_quant(memory, snapshot_quant, may_need_backward=True):
|
|
702
|
+
"""The snapshot mode a call runs: "bf16" runs as "none" on a bank that is not float32 (its snapshot rows are bf16
|
|
703
|
+
already: the same rows, the same route) and in a call that cannot need a backward (an unrecorded direct apply runs
|
|
704
|
+
exactly 'none''s call, as a no-grad call does); every other mode as given."""
|
|
705
|
+
if snapshot_quant == "bf16" and (not may_need_backward or getattr(memory, "dtype", None) != torch.float32):
|
|
706
|
+
return "none"
|
|
707
|
+
return snapshot_quant
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
class SDMWriteRead(torch.autograd.Function):
|
|
711
|
+
"""Chunk-wise gated delta-rule write/read with an exact intra-chunk recurrence.
|
|
712
|
+
|
|
713
|
+
Per token t (the reference token loop, which the chunked form reproduces up to
|
|
714
|
+
floating-point rounding):
|
|
715
|
+
at_k = bank[k_idx]
|
|
716
|
+
read = at_k * exp(g) (only the written rows decay)
|
|
717
|
+
retrieved = sum_w k_val_w * read_w
|
|
718
|
+
delta_v = beta * (v - retrieved)
|
|
719
|
+
bank[k] += read - at_k + k_val (x) delta_v
|
|
720
|
+
readings = sum_r q_val_r * bank[q_idx_r] (after the write)
|
|
721
|
+
|
|
722
|
+
Call through ``sdm_write_read``.
|
|
723
|
+
"""
|
|
724
|
+
|
|
725
|
+
@staticmethod
|
|
726
|
+
def forward(ctx, memory, k_idx, k_val, v, beta, g, q_idx, q_val, chunk_size,
|
|
727
|
+
num_memory_slots, num_parallel, snapshot_quant, grad_bank_fp32=False, lay=None):
|
|
728
|
+
# Autograd records this call (grad mode on and an input requires grad: a backward may follow) iff its node has
|
|
729
|
+
# next edges; cuda_graph's stand-in ctx (no next_functions) serves calls that may need a backward.
|
|
730
|
+
edges = getattr(ctx, "next_functions", None)
|
|
731
|
+
recorded = edges is None or len(edges) > 0
|
|
732
|
+
if lay is None: # (sdm_write_read passes its own; the graph runner's capture and a direct apply do not)
|
|
733
|
+
lay = _layout(memory, k_idx, k_val, v, beta, g, q_idx, q_val, chunk_size, num_memory_slots, num_parallel)
|
|
734
|
+
# Every call here runs the saving forward (an unrecorded one too); a recorded one also needs the CUDA backward.
|
|
735
|
+
# The mode it runs: "bf16" as "none" on a bank that is not float32 and in an unrecorded call (_admit).
|
|
736
|
+
snapshot_quant = _admit(memory, lay, snapshot_quant, recorded, True, grad_bank_fp32)
|
|
737
|
+
info = {}
|
|
738
|
+
with _kernel_context():
|
|
739
|
+
readings, saved = _sdm_forward(memory, k_idx, k_val, v, beta, g, q_idx, q_val,
|
|
740
|
+
lay, snapshot_quant, save=True,
|
|
741
|
+
grad_bank_fp32=bool(grad_bank_fp32), info=info)
|
|
742
|
+
ctx.save_for_backward(*saved)
|
|
743
|
+
ctx.lay = lay
|
|
744
|
+
ctx.snapshot_quant = snapshot_quant
|
|
745
|
+
ctx.quant_layout = info.get("quant_layout", 0) # the compact-snapshot layout written (2: compact rows, 0: per entry)
|
|
746
|
+
ctx.grad_bank_fp32 = bool(grad_bank_fp32)
|
|
747
|
+
ctx.inputs = [(t.shape, t.dtype) for t in (k_val, v, beta, g, q_val)]
|
|
748
|
+
return readings
|
|
749
|
+
|
|
750
|
+
@staticmethod
|
|
751
|
+
def backward(ctx, grad_readings):
|
|
752
|
+
# The forward advanced the bank in place and this backward restores it, so a second
|
|
753
|
+
# backward through the same call (retain_graph=True) would start from the restored bank.
|
|
754
|
+
# The cuda_graph runner's stand-in ctx is exempt (cuda_graph._Ctx.repeated_backward_ok):
|
|
755
|
+
# the runner manages that ctx's state itself (see _Ctx).
|
|
756
|
+
if not getattr(ctx, "repeated_backward_ok", False):
|
|
757
|
+
if getattr(ctx, "backward_done", False):
|
|
758
|
+
raise RuntimeError("sdm_write_read: a second backward through one call is not "
|
|
759
|
+
"supported (retain_graph=True): the first backward restored the "
|
|
760
|
+
"bank in place. Run the forward again, or use the cuda_graph "
|
|
761
|
+
"option.")
|
|
762
|
+
ctx.backward_done = True
|
|
763
|
+
saved = ctx.saved_tensors # runs any pending activation-checkpoint recompute
|
|
764
|
+
with _kernel_context():
|
|
765
|
+
grads = _backward.sdm_backward(ctx, saved, grad_readings)
|
|
766
|
+
return (*grads, None) # (no gradient for lay; autograd drops it for a call that did not pass lay)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def needs_backward(*tensors: torch.Tensor) -> bool:
|
|
772
|
+
"""Whether a call on ``tensors`` may be differentiated: grad mode is on and one of them
|
|
773
|
+
requires grad. This covers a non-reentrant activation-checkpoint forward (which drops
|
|
774
|
+
what is saved) and every recompute. A reentrant checkpoint runs its first forward
|
|
775
|
+
without grad mode and recomputes it before the backward."""
|
|
776
|
+
return torch.is_grad_enabled() and any(t.requires_grad for t in tensors)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def sdm_write_read(memory, k_idx, k_val, v, beta, g, q_idx, q_val, *, chunk_size: int = DEFAULT_CHUNK,
|
|
780
|
+
num_memory_slots: int = 0, num_parallel: int = 1,
|
|
781
|
+
snapshot_quant: str = "none", key_weighted_decay: bool | None = None,
|
|
782
|
+
shared_bank: bool = False, grad_bank_fp32: bool = False):
|
|
783
|
+
"""Chunked SDM write/read over P parallel streams; returns the readings [P * T, hd].
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
memory: [num_slots, hd] bfloat16 or float32 bank, advanced in place to its final
|
|
787
|
+
state. The backward restores it: exactly with snapshot_quant "none", and the
|
|
788
|
+
written rows to their dequantized snapshot with "fp8", "int4" or "bf16".
|
|
789
|
+
k_idx, k_val: [P, T, W] (or [P * T, W]) write slots and weights; the W slots of a
|
|
790
|
+
token must be distinct. Indices are int32 or int64 bank rows.
|
|
791
|
+
v: [..., hd] values; beta: [..., 1] write strengths.
|
|
792
|
+
g: [..., 1] per-token log-decay, or [..., W] one per write.
|
|
793
|
+
q_idx, q_val: [..., R] read slots and weights.
|
|
794
|
+
chunk_size: tokens per chunk C, 16..256 (default 64); T need not be a multiple of it.
|
|
795
|
+
num_memory_slots: slots per stream; 0 means one bank shared by all streams. With
|
|
796
|
+
num_parallel streams of their own, stream p owns rows [p * sph, (p + 1) * sph).
|
|
797
|
+
num_parallel: number of streams P; streams must not share slots.
|
|
798
|
+
shared_bank: required for several streams over one shared bank (num_memory_slots 0
|
|
799
|
+
or the bank's size with num_parallel > 1). It asserts that no two streams select
|
|
800
|
+
the same slot anywhere in the call (disjoint slot sets), which the op does not
|
|
801
|
+
check. Two streams that write one slot in the same chunk step lose one of the two
|
|
802
|
+
updates (as in the previous release); any other overlap (a read
|
|
803
|
+
and a write of one slot, or one slot in different steps) loses no update but
|
|
804
|
+
leaves the step-synchronous order: the no-grad path may run one stream's steps
|
|
805
|
+
before another's.
|
|
806
|
+
snapshot_quant: storage of the backward snapshot, "none", "fp8", "int4" or "bf16"
|
|
807
|
+
(bfloat16 rows, round to nearest even: on a float32 bank it halves the snapshot;
|
|
808
|
+
a bfloat16 bank runs it as "none", its snapshot being bf16 already).
|
|
809
|
+
key_weighted_decay: whether g holds one gate per write; None infers it from g.
|
|
810
|
+
grad_bank_fp32: accumulate the backward's bank gradient (and its gathered grad rows) in
|
|
811
|
+
float32 with one cast back to the bank dtype at the end (default False; a no-op for a
|
|
812
|
+
float32 bank). Forward and snapshot are unchanged; the other gradients change only
|
|
813
|
+
through the bank gradient's rounding, which feeds them.
|
|
814
|
+
|
|
815
|
+
k_val, v and q_val are used in the bank dtype, beta and g in float32. A call that does
|
|
816
|
+
not need a backward (``needs_backward``) saves nothing, and its working memory does
|
|
817
|
+
not grow with T. The kernels are deterministic: rows that several entries of a chunk write
|
|
818
|
+
(read) are summed by one program per column slice in a fixed order, and so are Phase 1's pair
|
|
819
|
+
sums.
|
|
820
|
+
"""
|
|
821
|
+
if snapshot_quant not in SNAPSHOT_QUANT_MODES:
|
|
822
|
+
raise ValueError(f"snapshot_quant must be one of {SNAPSHOT_QUANT_MODES}, "
|
|
823
|
+
f"got {snapshot_quant!r}")
|
|
824
|
+
if key_weighted_decay is not None and g.shape[-1] != (k_idx.shape[-1]
|
|
825
|
+
if key_weighted_decay else 1):
|
|
826
|
+
raise ValueError(f"key_weighted_decay={key_weighted_decay} does not match g of "
|
|
827
|
+
f"shape {tuple(g.shape)}")
|
|
828
|
+
args = (memory, k_idx, k_val, v, beta, g, q_idx, q_val, chunk_size, num_memory_slots,
|
|
829
|
+
num_parallel)
|
|
830
|
+
# Validates the call once (the training forward takes this layout), so malformed arguments get their own errors;
|
|
831
|
+
# then the shared-bank opt-in, then the route (_admit, in the forward for a training call).
|
|
832
|
+
lay = _layout(*args)
|
|
833
|
+
if lay.P > 1 and lay.stream_offset == 0 and not shared_bank:
|
|
834
|
+
raise ValueError(
|
|
835
|
+
f"num_parallel={lay.P} streams over one shared bank of {memory.shape[0]} slots: "
|
|
836
|
+
"pass shared_bank=True to assert that the streams select disjoint slots for the "
|
|
837
|
+
"whole call (the op does not check: two streams writing one slot in the same "
|
|
838
|
+
"chunk step lose an update, and any other overlap leaves the step-synchronous "
|
|
839
|
+
"order), or give each stream its own slot range (num_memory_slots = bank slots "
|
|
840
|
+
"// num_parallel).")
|
|
841
|
+
if needs_backward(memory, k_val, v, beta, g, q_val):
|
|
842
|
+
return SDMWriteRead.apply(*args, effective_snapshot_quant(memory, snapshot_quant), grad_bank_fp32, lay)
|
|
843
|
+
_admit(memory, lay) # the no-grad forward's route, after every check above, before any work
|
|
844
|
+
with torch.no_grad(), _kernel_context():
|
|
845
|
+
return _sdm_forward(*args[:8], lay, snapshot_quant, save=False)[0]
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
# ============================================================
|
|
849
|
+
# Layer I/O: head split / merge and the per-batch bank expand
|
|
850
|
+
# ============================================================
|
|
851
|
+
# head_split : dst_row = (b*H + h)*T + t <- src_row = (b*T + t)*H + h
|
|
852
|
+
# head_merge : dst_row = (b*T + t)*H + h <- src_row = (b*H + h)*T + t
|
|
853
|
+
# Whole contiguous rows are copied, so forward and backward are byte-exact
|
|
854
|
+
# equivalents of the einops rearranges.
|
|
855
|
+
_HEADPERM_ROWS_PER_CTA = 32 # narrow bf16 rows are packed this many per program
|
|
856
|
+
_HEADPERM_PACKED_WARPS = 2
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
@triton.jit
|
|
860
|
+
def _bht_permute_kernel(
|
|
861
|
+
src_ptr, dst_ptr,
|
|
862
|
+
H, T, CD,
|
|
863
|
+
BLOCK: tl.constexpr,
|
|
864
|
+
SPLIT: tl.constexpr, # True: [B,T,H*CD] -> [B*H,T,CD]; False: the inverse
|
|
865
|
+
):
|
|
866
|
+
row = tl.program_id(0)
|
|
867
|
+
if SPLIT:
|
|
868
|
+
t = row % T
|
|
869
|
+
bh = row // T
|
|
870
|
+
h = bh % H
|
|
871
|
+
b = bh // H
|
|
872
|
+
srow = (b * T + t) * H + h
|
|
873
|
+
else:
|
|
874
|
+
h = row % H
|
|
875
|
+
bt = row // H
|
|
876
|
+
t = bt % T
|
|
877
|
+
b = bt // T
|
|
878
|
+
srow = (b * H + h) * T + t
|
|
879
|
+
offs = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)
|
|
880
|
+
m = offs < CD
|
|
881
|
+
v = tl.load(src_ptr + srow.to(tl.int64) * CD + offs, mask=m)
|
|
882
|
+
tl.store(dst_ptr + row.to(tl.int64) * CD + offs, v, mask=m)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@triton.jit
|
|
886
|
+
def _bht_permute_packed_kernel(
|
|
887
|
+
src_ptr, dst_ptr,
|
|
888
|
+
H, T, CD, N,
|
|
889
|
+
BLOCK: tl.constexpr,
|
|
890
|
+
ROWS: tl.constexpr,
|
|
891
|
+
SPLIT: tl.constexpr,
|
|
892
|
+
):
|
|
893
|
+
"""Copies ROWS narrow rows per program; row addresses are computed once per row."""
|
|
894
|
+
rows = tl.program_id(0) * ROWS + tl.arange(0, ROWS)[:, None]
|
|
895
|
+
cols = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)[None, :]
|
|
896
|
+
valid_rows = rows < N
|
|
897
|
+
if SPLIT:
|
|
898
|
+
t = rows % T
|
|
899
|
+
bh = rows // T
|
|
900
|
+
h = bh % H
|
|
901
|
+
b = bh // H
|
|
902
|
+
src_rows = (b * T + t) * H + h
|
|
903
|
+
else:
|
|
904
|
+
h = rows % H
|
|
905
|
+
bt = rows // H
|
|
906
|
+
t = bt % T
|
|
907
|
+
b = bt // T
|
|
908
|
+
src_rows = (b * H + h) * T + t
|
|
909
|
+
mask = valid_rows & (cols < CD)
|
|
910
|
+
values = tl.load(src_ptr + src_rows.to(tl.int64) * CD + cols, mask=mask)
|
|
911
|
+
tl.store(dst_ptr + rows.to(tl.int64) * CD + cols, values, mask=mask)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _bht_permute(x, Bb, H, T, CD, split):
|
|
915
|
+
n = Bb * H * T
|
|
916
|
+
with _no_det_fill():
|
|
917
|
+
out = torch.empty(n * CD, device=x.device, dtype=x.dtype)
|
|
918
|
+
BLOCK = min(2048, triton.next_power_of_2(CD))
|
|
919
|
+
rows = _HEADPERM_ROWS_PER_CTA
|
|
920
|
+
if x.dtype == torch.bfloat16 and CD in (64, 128):
|
|
921
|
+
_device.tbind(_bht_permute_packed_kernel, (triton.cdiv(n, rows), triton.cdiv(CD, BLOCK)), x)(
|
|
922
|
+
x, out, H=H, T=T, CD=CD, N=n, BLOCK=BLOCK, ROWS=rows, SPLIT=split,
|
|
923
|
+
num_warps=_HEADPERM_PACKED_WARPS)
|
|
924
|
+
else:
|
|
925
|
+
_device.tbind(_bht_permute_kernel, (n, triton.cdiv(CD, BLOCK)), x)(
|
|
926
|
+
x, out, H=H, T=T, CD=CD, BLOCK=BLOCK, SPLIT=split,
|
|
927
|
+
num_warps=4 if BLOCK >= 1024 else 1)
|
|
928
|
+
return out
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
class _HeadSplit(torch.autograd.Function):
|
|
932
|
+
"""[B, T, H*CD] -> [B*H, T, CD] (rearrange 'b t (h k) -> (b h) t k')."""
|
|
933
|
+
|
|
934
|
+
@staticmethod
|
|
935
|
+
def forward(ctx, x, H):
|
|
936
|
+
Bb, T, HK = x.shape
|
|
937
|
+
CD = HK // H
|
|
938
|
+
ctx.dims = (Bb, H, T, CD)
|
|
939
|
+
return _bht_permute(x.contiguous(), Bb, H, T, CD, True).view(Bb * H, T, CD)
|
|
940
|
+
|
|
941
|
+
@staticmethod
|
|
942
|
+
def backward(ctx, g):
|
|
943
|
+
# Through the sibling Function so the gradient stays differentiable.
|
|
944
|
+
return _HeadMerge.apply(g, ctx.dims[1]), None
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
class _HeadMerge(torch.autograd.Function):
|
|
948
|
+
"""[B*H, T, CD] -> [B, T, H*CD] (rearrange '(b h) t d -> b t (h d)')."""
|
|
949
|
+
|
|
950
|
+
@staticmethod
|
|
951
|
+
def forward(ctx, x, H):
|
|
952
|
+
BH, T, CD = x.shape
|
|
953
|
+
Bb = BH // H
|
|
954
|
+
ctx.dims = (Bb, H, T, CD)
|
|
955
|
+
return _bht_permute(x.contiguous(), Bb, H, T, CD, False).view(Bb, T, H * CD)
|
|
956
|
+
|
|
957
|
+
@staticmethod
|
|
958
|
+
def backward(ctx, g):
|
|
959
|
+
return _HeadSplit.apply(g, ctx.dims[1]), None
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def head_split(x, H):
|
|
963
|
+
"""rearrange(x, 'b t (h k) -> (b h) t k', h=H) as a coalesced row copy.
|
|
964
|
+
|
|
965
|
+
Uses the reshape/transpose reference when it is a free view (H == 1, or T == 1 as in
|
|
966
|
+
decode), for strided or empty inputs, and when H does not divide the last dimension
|
|
967
|
+
(the reshape then raises like einops).
|
|
968
|
+
"""
|
|
969
|
+
if (H == 1 or x.shape[1] == 1 or not x.is_contiguous()
|
|
970
|
+
or x.numel() == 0 or x.shape[2] % H != 0):
|
|
971
|
+
return x.reshape(x.shape[0], x.shape[1], H, -1).transpose(1, 2).reshape(
|
|
972
|
+
x.shape[0] * H, x.shape[1], -1)
|
|
973
|
+
return _HeadSplit.apply(x, H)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def head_merge(x, H):
|
|
977
|
+
"""rearrange(x, '(b h) t d -> b t (h d)', h=H) as a coalesced row copy.
|
|
978
|
+
|
|
979
|
+
Same reference fallbacks as ``head_split``.
|
|
980
|
+
"""
|
|
981
|
+
BH, T, CD = x.shape
|
|
982
|
+
if (H == 1 or T == 1 or not x.is_contiguous()
|
|
983
|
+
or x.numel() == 0 or BH % H != 0):
|
|
984
|
+
return x.reshape(BH // H, H, T, CD).transpose(1, 2).reshape(BH // H, T, H * CD)
|
|
985
|
+
return _HeadMerge.apply(x, H)
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
@triton.jit
|
|
989
|
+
def _sum_batch_kernel(x_ptr, out_ptr,
|
|
990
|
+
B: tl.constexpr, S, D,
|
|
991
|
+
stride_xb, stride_xs, stride_xd,
|
|
992
|
+
stride_os, stride_od,
|
|
993
|
+
BLOCK_D: tl.constexpr):
|
|
994
|
+
"""Sum a [B, S, D] tensor over the batch dim into [S, D].
|
|
995
|
+
|
|
996
|
+
Grid = (S, cdiv(D, BLOCK_D)). All offsets are int64 (b * stride_xb exceeds int32 for
|
|
997
|
+
large banks); the pointer is advanced by stride_xb per batch element so the large
|
|
998
|
+
product is never formed.
|
|
999
|
+
"""
|
|
1000
|
+
s = tl.program_id(0).to(tl.int64)
|
|
1001
|
+
d64 = tl.program_id(1).to(tl.int64) * BLOCK_D + tl.arange(0, BLOCK_D).to(tl.int64)
|
|
1002
|
+
d_mask = d64 < D
|
|
1003
|
+
ptr = x_ptr + s * stride_xs + d64 * stride_xd
|
|
1004
|
+
acc = tl.zeros((BLOCK_D,), dtype=tl.float32)
|
|
1005
|
+
for _b in tl.static_range(B):
|
|
1006
|
+
acc += tl.load(ptr, mask=d_mask, other=0.0).to(tl.float32)
|
|
1007
|
+
ptr += stride_xb
|
|
1008
|
+
out_off = s * stride_os + d64 * stride_od
|
|
1009
|
+
tl.store(out_ptr + out_off, acc.to(out_ptr.dtype.element_ty), mask=d_mask)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def triton_sum_dim0(x):
|
|
1013
|
+
"""Sum over dim 0 of a [B, S, D] tensor -> [S, D] (int64-safe, any D)."""
|
|
1014
|
+
assert x.ndim == 3 and x.dtype in (torch.bfloat16, torch.float32)
|
|
1015
|
+
B, S, D = x.shape
|
|
1016
|
+
with _no_det_fill():
|
|
1017
|
+
out = torch.empty(S, D, device=x.device, dtype=x.dtype)
|
|
1018
|
+
BLOCK_D = min(1024, triton.next_power_of_2(D))
|
|
1019
|
+
_device.tbind(_sum_batch_kernel, (S, triton.cdiv(D, BLOCK_D)), x)(
|
|
1020
|
+
x, out,
|
|
1021
|
+
B=B, S=S, D=D,
|
|
1022
|
+
stride_xb=x.stride(0), stride_xs=x.stride(1), stride_xd=x.stride(2),
|
|
1023
|
+
stride_os=out.stride(0), stride_od=out.stride(1),
|
|
1024
|
+
BLOCK_D=BLOCK_D, num_warps=4,
|
|
1025
|
+
)
|
|
1026
|
+
return out
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
@triton.jit
|
|
1030
|
+
def _bcast_rows_kernel(
|
|
1031
|
+
src_ptr, out_ptr, S,
|
|
1032
|
+
D: tl.constexpr, B: tl.constexpr, BLOCK_D: tl.constexpr,
|
|
1033
|
+
):
|
|
1034
|
+
"""One program per source row: load it once, store it into every batch copy."""
|
|
1035
|
+
row = tl.program_id(0).to(tl.int64)
|
|
1036
|
+
d = tl.arange(0, BLOCK_D)
|
|
1037
|
+
m = d < D
|
|
1038
|
+
x = tl.load(src_ptr + row * D + d, mask=m)
|
|
1039
|
+
for b in tl.static_range(B):
|
|
1040
|
+
tl.store(out_ptr + (b * S + row) * D + d, x, mask=m)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
class _ExpandMemoryBatch(torch.autograd.Function):
|
|
1044
|
+
@staticmethod
|
|
1045
|
+
def forward(ctx, param, B):
|
|
1046
|
+
S, D = param.shape
|
|
1047
|
+
if not param.is_contiguous():
|
|
1048
|
+
raise ValueError("expand_memory_batch needs a contiguous [S, D] parameter")
|
|
1049
|
+
ctx.dims = (B, S, D)
|
|
1050
|
+
with _no_det_fill():
|
|
1051
|
+
out = torch.empty(B * S, D, dtype=param.dtype, device=param.device)
|
|
1052
|
+
_device.tbind(_bcast_rows_kernel, (S,), param)(
|
|
1053
|
+
param, out, S, D=D, B=B, BLOCK_D=triton.next_power_of_2(D), num_warps=8)
|
|
1054
|
+
return out
|
|
1055
|
+
|
|
1056
|
+
@staticmethod
|
|
1057
|
+
def backward(ctx, grad_out):
|
|
1058
|
+
return triton_sum_dim0(grad_out.view(ctx.dims)), None
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def expand_memory_batch(param, B):
|
|
1062
|
+
"""``param.unsqueeze(0).expand(B, S, D).reshape(B * S, D).clone()``: private, mutable
|
|
1063
|
+
per-batch copies of the [S, D] bank (the op advances them in place), reading the
|
|
1064
|
+
parameter once; the backward sums the B gradient copies in one pass."""
|
|
1065
|
+
return _ExpandMemoryBatch.apply(param, B)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
# The forward steps and the backward use this module's helpers and entry points (looked up here at call time).
|
|
1069
|
+
from . import backward as _backward, forward as _forward # noqa: E402
|