sparse-delta-memory 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. sparse_delta_memory/__init__.py +13 -0
  2. sparse_delta_memory/backward.py +367 -0
  3. sparse_delta_memory/cache.py +70 -0
  4. sparse_delta_memory/cuda/__init__.py +3 -0
  5. sparse_delta_memory/cuda/_build.py +254 -0
  6. sparse_delta_memory/cuda/_device.py +158 -0
  7. sparse_delta_memory/cuda/_devmemo.py +64 -0
  8. sparse_delta_memory/cuda/_image.py +128 -0
  9. sparse_delta_memory/cuda/bwd_steps_cuda.py +111 -0
  10. sparse_delta_memory/cuda/gather_cuda.py +796 -0
  11. sparse_delta_memory/cuda/gather_ns_cuda.py +134 -0
  12. sparse_delta_memory/cuda/gx_gds_cuda.py +1297 -0
  13. sparse_delta_memory/cuda/gx_gemm_cuda.py +857 -0
  14. sparse_delta_memory/cuda/gx_pa2_cuda.py +1320 -0
  15. sparse_delta_memory/cuda/mover_da2_cuda.py +1802 -0
  16. sparse_delta_memory/cuda/mover_fx_cuda.py +2229 -0
  17. sparse_delta_memory/cuda/pa_dc_cuda.py +1500 -0
  18. sparse_delta_memory/cuda/phase1_bwd_cuda.py +852 -0
  19. sparse_delta_memory/cuda/phase1_common.cuh +181 -0
  20. sparse_delta_memory/cuda/phase1_cuda.py +3801 -0
  21. sparse_delta_memory/cuda/product_key_topk_anyf_cuda.py +668 -0
  22. sparse_delta_memory/cuda/product_key_topk_cuda.py +735 -0
  23. sparse_delta_memory/cuda/px_qrec_cuda.py +167 -0
  24. sparse_delta_memory/cuda/sdm_copy.cuh +90 -0
  25. sparse_delta_memory/cuda/sdm_plancap.cuh +150 -0
  26. sparse_delta_memory/cuda_graph.py +605 -0
  27. sparse_delta_memory/decode.py +140 -0
  28. sparse_delta_memory/forward.py +464 -0
  29. sparse_delta_memory/layer.py +663 -0
  30. sparse_delta_memory/memory_ops.py +1069 -0
  31. sparse_delta_memory/memory_stats.py +221 -0
  32. sparse_delta_memory/movers.py +198 -0
  33. sparse_delta_memory/phase2_kernels.py +267 -0
  34. sparse_delta_memory/product_key_topk.py +113 -0
  35. sparse_delta_memory/wy_solve.py +273 -0
  36. sparse_delta_memory-2.0.0.dist-info/METADATA +482 -0
  37. sparse_delta_memory-2.0.0.dist-info/RECORD +40 -0
  38. sparse_delta_memory-2.0.0.dist-info/WHEEL +5 -0
  39. sparse_delta_memory-2.0.0.dist-info/licenses/LICENSE +407 -0
  40. sparse_delta_memory-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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