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,13 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ from .cuda_graph import reset_cuda_graphs, suspend_cuda_graphs
4
+ from .layer import SparseDeltaMemory, SparseDeltaMemoryArgs
5
+ from .cuda._image import UntestedHardwareWarning
6
+ from .memory_ops import UnsupportedConfigurationError
7
+
8
+ __all__ = ["SparseDeltaMemory", "SparseDeltaMemoryArgs", "UnsupportedConfigurationError", "UntestedHardwareWarning",
9
+ "reset_cuda_graphs", "suspend_cuda_graphs"]
10
+
11
+ # The single source of the version: pyproject.toml reads it (distribution metadata), and it holds
12
+ # for a checkout or a vendored copy as well.
13
+ __version__ = "2.0.0"
@@ -0,0 +1,367 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """The backward of the chunked write/read on the CUDA kernels (``sdm_backward``).
3
+
4
+ Per step in reverse order: the gather-weighted read of the gradient rows (dc_gwr), the d_u update, Phase A (the
5
+ write-side backward and bank restore) and the read-side backward (``movers.make_mover_gds``); then Phase B, the matmuls
6
+ through the solve and the Phase-1 backward. Calls it does not support (``supported``) are refused at the forward
7
+ (memory_ops.refuse_non_native, 2.0.0): there is no other backward. memory_ops' entry points are looked up there at call time."""
8
+
9
+ import torch
10
+
11
+ from . import memory_ops as mo, movers
12
+ from .cuda import pa_dc_cuda as dc, phase1_bwd_cuda as _p1bwd
13
+ from .cuda._device import cuda_backend, smem_cap
14
+
15
+ STATS = {"dc": 0, "shipped": 0}
16
+ # The last backward's step loop: "eager_cpp_step_loop" (one C++ call) or "python_step_loop" (diagnostics).
17
+ LAST_LOOP = {"name": None}
18
+ QUANT_CODES = {"fp8": 1, "int4": 2} # gx_pa2_cuda's QUANT of each snapshot_quant mode
19
+
20
+
21
+ def pa_plan(C, bank_dtype, grad_dtype, device):
22
+ """(configuration bits, depth, warps per CTA) of Phase A's kernel, or None when even one warp does not fit the
23
+ device's opt-in shared memory. The footprint is the launcher's exact formula."""
24
+ depth, nw = 8, 8
25
+ optin = smem_cap(device)
26
+ rb = 16 if bank_dtype == torch.float32 else 8
27
+ gb = 16 if grad_dtype == torch.float32 else 8
28
+ head = C * 512 + ((C + 3) // 4) * 16
29
+
30
+ def need(w):
31
+ return head + w * depth * 32 * (gb + rb)
32
+ while nw > 1 and need(nw) > optin:
33
+ nw //= 2 # results do not depend on the warps per CTA
34
+ if need(nw) > optin:
35
+ return None
36
+ return 512, depth, nw
37
+
38
+
39
+ def compact_rows(lay) -> int:
40
+ """Rows of the call's compact snapshot buffer: Nt * P * C * W (forward and backward compute the same)."""
41
+ return int(lay.Nt) * int(lay.P) * int(lay.C) * int(lay.W)
42
+
43
+
44
+ def supported(memory, quant, C=None, grad_f32_possible=False, rows=None):
45
+ """The CUDA backward runs for this op (evaluated by the forward too, before it writes a compact snapshot):
46
+ dtype/layout/device checks plus a shared-memory plan for the worst grad dtype the backward may use. fp8 / int4 also
47
+ need the compact snapshot's row count (`rows`, compact_rows(lay); any count >= 0: gx_pa2 reads 64-bit snapshot rows),
48
+ so the forward writes quantized compact rows only where the backward's gx_pa2 reads them; unknown -> no."""
49
+ if C is not None and memory.is_cuda:
50
+ gdt = torch.float32 if (grad_f32_possible and memory.dtype == torch.bfloat16) else memory.dtype
51
+ if pa_plan(C, memory.dtype, gdt, memory.device) is None:
52
+ return False
53
+ # Shape / dtype / arch only: no alignment or memory-layout condition (the forward already requires a
54
+ # contiguous, vector-aligned bank; the backward's launchers raise rather than switch paths). head_dim % 4 != 0 (tails:
55
+ # gx_pa2 / gx_gds element pieces) only where the shared tail predicate admits the call and the compact
56
+ # snapshot's row count (`rows`) is known (any count: gx_pa2 reads 64-bit snapshot rows): Phase A has no dc_pa3
57
+ # fallback for those heads, so a call gx_pa2 would refuse is refused up front (memory_ops.refuse_non_native; unknown
58
+ # rows -> no).
59
+ dev_ok = memory.dtype in (torch.bfloat16, torch.float32) and memory.is_cuda and cuda_backend(memory.device)
60
+ ta = getattr(movers, "tail_admits", None) # (a tree without the shared predicate: no tails)
61
+ base = dev_ok and (memory.shape[-1] % 4 == 0 or (
62
+ C is not None and rows is not None and 0 <= rows and ta is not None and ta(memory, C, quant)))
63
+ if quant == "none" or not base:
64
+ return base and quant == "none"
65
+ # fp8 / int4: gx_pa2's quantized instantiations read the compact rows mover_fx_cuda's quantized DW writes;
66
+ # the calls they do not take are refused up front (slice rows that are not 16-byte rows). One
67
+ # predicate for both sides: the forward reads this one too (lean Phase-1 metadata, compact rows), so it also needs
68
+ # the producer (mover_fx_cuda.quant_supported).
69
+ from .cuda import gx_pa2_cuda, mover_fx_cuda
70
+ producer = getattr(mover_fx_cuda, "quant_supported", None)
71
+ if quant == "bf16":
72
+ # bf16 compact rows of a float32 bank (memory_ops runs "bf16" as "none" on other banks): written by mover_fx_cuda's
73
+ # bank-direct WR walk as bf16 rows (s16_supported; the unquantized forward), read by gx_pa2's bf16-snapshot rows
74
+ # (regular module, or its tail module for slices that are not 16-byte bf16 rows: s16_admits) restoring the float32
75
+ # bank with float32 grad rows (grad_bank_fp32 is a no-op on float32 banks). The same heads as 'none' (base above).
76
+ s16 = getattr(gx_pa2_cuda, "s16_admits", None)
77
+ prod = getattr(mover_fx_cuda, "s16_supported", None)
78
+ return (C is not None and memory.dtype == torch.float32 and s16 is not None and prod is not None
79
+ and rows is not None and 0 <= rows < 2 ** 31 and bool(s16(memory, C)) and bool(prod(memory, C)))
80
+ gdt = torch.float32 if (grad_f32_possible and memory.dtype == torch.bfloat16) else memory.dtype
81
+ code = QUANT_CODES.get(quant)
82
+ return (C is not None and code is not None and producer is not None and rows is not None and 0 <= rows
83
+ and gx_pa2_cuda.admits_rows(memory, gdt, C, code) and bool(producer(memory, C, code)))
84
+
85
+
86
+ def _supported(ctx, memory, C, saved=None):
87
+ ok = supported(memory, ctx.snapshot_quant, C=C, grad_f32_possible=bool(getattr(ctx, "grad_bank_fp32", False)),
88
+ rows=compact_rows(ctx.lay))
89
+ if ok and saved is not None and memory.shape[-1] % 4 and ctx.snapshot_quant in ("none", "bf16"):
90
+ # head_dim % 4 != 0: only where gx_pa2 runs (dense compact snapshot, layout 2, 16-byte aligned)
91
+ snap = saved[11]
92
+ ok = (ctx.quant_layout == 2 and snap is not None
93
+ and snap.numel() == compact_rows(ctx.lay) * memory.shape[-1] and snap.data_ptr() % 16 == 0)
94
+ return ok
95
+
96
+
97
+ def admits_at_forward(memory, C, snapshot_quant, rows=None):
98
+ """At forward time: the backward of this call runs the CUDA kernels whatever grad dtype it uses (float32 with
99
+ grad_bank_fp32 on a bfloat16 bank): ``supported`` with the worst grad dtype (and the compact row count `rows`),
100
+ and the CUDA GDS (movers.gds_admits) for every such grad dtype."""
101
+ gdts = (memory.dtype, torch.float32) if memory.dtype == torch.bfloat16 else (memory.dtype,)
102
+ return (supported(memory, snapshot_quant, C=C, grad_f32_possible=True, rows=rows)
103
+ and all(movers.gds_admits(memory, g, C, snapshot_quant) for g in gdts))
104
+
105
+
106
+ def _fp32_matmul_ieee() -> bool:
107
+ """A cheap prefilter of the run-time qualification (gx_gemm_cuda.matches_cublas): float32 matmuls in full IEEE
108
+ float32 (cuBLAS's effective TF32 switch off and no reduced-precision request). Unreadable precision state ->
109
+ False (keep cuBLAS)."""
110
+ try:
111
+ if torch._C._get_cublas_allow_tf32():
112
+ return False
113
+ new = getattr(torch.backends.cuda.matmul, "fp32_precision", None)
114
+ if new is not None and str(new) not in ("ieee", "none"):
115
+ return False
116
+ return torch.get_float32_matmul_precision() == "highest"
117
+ except RuntimeError:
118
+ return False
119
+
120
+
121
+ def _steps_cpp(*, Nt, T, C, P, SC, W, L, nw, depth, dbg, pipe, grad_memory, memory, snap_arg, snap_rows, d_u,
122
+ d_u_rows, x_t, d_delta_v, delta_v, beta, kpay, rec, eff_out, g_out, bpre, pa_q, gdwc, k_rows, write_w,
123
+ pd_out, geo, empty_i, gemm_step, gemm_pdl, pa_fn, gds, gds_pdl, gwr_pdl):
124
+ """sdm_backward's step loop as one C++ call (bwd_steps_cuda): True when it ran; False, with nothing launched, when a
125
+ per-step launcher has no plan (the Python loop runs). Same launches as the loop."""
126
+ if Nt < 1: # no step (T = 0): nothing to launch, as the loop
127
+ STATS["steps_cpp"] = STATS.get("steps_cpp", 0) + 1
128
+ LAST_LOOP["name"] = "eager_cpp_step_loop"
129
+ return True
130
+ from .cuda import gx_pa2_cuda as gxpa2
131
+ gds_plan = getattr(gds, "plan", None)
132
+ pa_dc3 = pa_fn is dc._module().dc_pa
133
+ if gds_plan is None or not (gxpa2.is_launcher(pa_fn) or pa_dc3) or (
134
+ gemm_step is not None and getattr(gemm_step, "plan", None) is None):
135
+ STATS["steps_python"] = STATS.get("steps_python", 0) + 1
136
+ LAST_LOOP["name"] = "python_step_loop"
137
+ return False
138
+ gd = gds_plan(Nt, pdl=gds_pdl)
139
+ if gd is None:
140
+ STATS["steps_python"] = STATS.get("steps_python", 0) + 1
141
+ LAST_LOOP["name"] = "python_step_loop"
142
+ return False
143
+ from .cuda import bwd_steps_cuda
144
+ gw = dc.gwr_plan(grad_memory, k_rows, write_w, d_delta_v, delta_v, pd_out, row_step=SC, out_row_step=SC,
145
+ tokens=SC, chunk=C, valid_tokens=T, ti_max=Nt - 1, vec_bytes=geo.vec_bytes, lanes=geo.lanes,
146
+ groups=geo.groups, tpc=geo.tpc, kpos=empty_i, gdwc=gdwc)
147
+ if pa_dc3: # dc_pa3 (Phase A for layouts gx_pa2 does not read): plain launches
148
+ pa = dc.pa_step_plan(grad_memory, memory, snap_arg, snap_rows, d_u_rows, beta, kpay, rec, eff_out, g_out,
149
+ Nt - 1, P, C, W, L, nw, depth, dbg, pipe, bpre, pa_q)
150
+ else:
151
+ pa = gxpa2.step_plan(pa_fn, grad_memory, memory, snap_arg, snap_rows, d_u_rows, beta, kpay, rec, eff_out,
152
+ g_out, Nt - 1, P, C, W, L, nw, depth, dbg, pipe, bpre, pa_q)
153
+ hd = d_u.shape[-1]
154
+ if gemm_step is not None:
155
+ gplan, gaddr, guse = gemm_step.plan(Nt, pdl=gemm_pdl)
156
+ gm, views = (gplan, gaddr), None
157
+ else:
158
+ gm, guse = None, 0
159
+ views = (d_u.view(Nt, P, C, hd), x_t.unflatten(0, (Nt, P)), d_delta_v.view(Nt, P, C, hd))
160
+ bwd_steps_cuda.run(gw, gm, pa, gd[:2], steps=Nt, gwr_pdl=1 if gwr_pdl else 0, gemm_use=guse, gds_use=gd[2],
161
+ device=grad_memory.device, blas_views=views)
162
+ if gemm_step is not None:
163
+ gemm_step.count(Nt, guse)
164
+ gds.done()
165
+ STATS["steps_cpp"] = STATS.get("steps_cpp", 0) + 1
166
+ LAST_LOOP["name"] = "eager_cpp_step_loop"
167
+ return True
168
+
169
+
170
+ NO_NATIVE_BACKWARD_MSG = ("SDM backward: internal error: this call has no native backward, but its forward was not refused "
171
+ "(memory_ops.refuse_non_native); please report it with the call's shapes, dtypes and GPU")
172
+
173
+
174
+ def sdm_backward(ctx, saved, grad_readings):
175
+ """SDMWriteRead's backward: the CUDA kernels (``_supported`` admits every call memory_ops.refuse_non_native lets
176
+ through; 2.0.0 removed the reference backward): a call it declines raises NO_NATIVE_BACKWARD_MSG, an internal
177
+ error, never a slow path."""
178
+ memory = saved[0]
179
+ if ctx.snapshot_quant == "bf16" and (not _supported(ctx, memory, ctx.lay.C, saved) or ctx.quant_layout != 2):
180
+ raise RuntimeError("SDM backward: bf16 compact snapshot rows that the CUDA backward cannot read (no fallback)")
181
+ if not _supported(ctx, memory, ctx.lay.C, saved) or (ctx.snapshot_quant != "none" and ctx.quant_layout != 2):
182
+ # 2.0.0: no reference backward; memory_ops.refuse_non_native refused such calls at the forward (internal error)
183
+ raise RuntimeError(NO_NATIVE_BACKWARD_MSG)
184
+ STATS["dc"] += 1
185
+ lay = ctx.lay
186
+ P, T, C, Nt, W, R, hd = lay.P, lay.T, lay.C, lay.Nt, lay.W, lay.R, lay.hd
187
+ N, SC = Nt * P, P * C
188
+ NC = N * C
189
+ (memory, k_idx, q_idx, k_val, q_val, g, beta, x, qx, delta_v, residual,
190
+ snapshot, scale, *p1) = saved
191
+ p1 = mo.Phase1(*p1)
192
+ dtype, dev, f32 = memory.dtype, memory.device, torch.float32
193
+ # A legal saved bank may be offset (e.g. +2 bytes with a scalar-gather head) or non-contiguous (a saved-tensor
194
+ # unpack hook, e.g. activation offloading, may return an equal-valued strided view): run on an aligned contiguous
195
+ # scratch copy and write the restored bank back with a raw (untracked) copy at the end. The restore lands in the
196
+ # tensor the backward received (the unpacked one), as with the reference backward; if an unpack hook returns a
197
+ # tensor that does not alias the caller's bank, the caller's bank is not restored (the same as there). Same bits.
198
+ bank_user = None
199
+ if not memory.is_contiguous() or memory.data_ptr() % 16:
200
+ if dc._module().dc_overlaps(memory): # before any work: a view whose elements share addresses
201
+ raise RuntimeError("SDM backward: the saved bank is a self-overlapping view (some of its elements refer to a single "
202
+ "memory location); it cannot be restored in place, as with Tensor.copy_")
203
+ bank_user = memory
204
+ memory = torch.empty(memory.shape, device=dev, dtype=dtype)
205
+ memory.copy_(bank_user)
206
+
207
+ gr = mo.stage_time_major(grad_readings, P, T, C, f32)
208
+ if not gr.is_contiguous() or gr.data_ptr() % 16:
209
+ # A legal autograd layout (P == 1 makes staging a view of grad_readings, which may be offset or strided):
210
+ # use aligned contiguous scratch, never raise. Same values, same bits.
211
+ gr = gr.contiguous() if not gr.is_contiguous() else gr.clone()
212
+ # Optional f32 grad bank (grad_bank_fp32, bf16 banks, default OFF): grad_memory and the gathered grad
213
+ # rows stay f32 inside the backward; one rounding to the bank dtype at the end.
214
+ grad_f32 = bool(getattr(ctx, "grad_bank_fp32", False)) and dtype == torch.bfloat16
215
+ grad_memory = torch.zeros(memory.shape, device=dev, dtype=f32 if grad_f32 else dtype)
216
+ d_u = torch.bmm(qx.transpose(1, 2), gr.view(N, C, hd))
217
+ write_w = p1.rex.exp().mul_(k_val).view(NC, W)
218
+ d_delta_v = torch.empty(NC, hd, device=dev, dtype=f32)
219
+ grad_g_decay = torch.empty(N, C, W, device=dev, dtype=f32)
220
+ d_k_pd = torch.empty_like(grad_g_decay)
221
+ d_k_eff = torch.empty_like(grad_g_decay)
222
+ d_q_w = torch.empty(N, C, R, device=dev, dtype=f32)
223
+
224
+ geo = mo.pinned_gather_geometry(hd, grad_memory.dtype, W, SC)
225
+ gslices = geo.slices
226
+ nsl = dc.slices(hd)
227
+ empty = torch.empty(0, device=dev, dtype=f32)
228
+ empty_i = torch.empty(0, device=dev, dtype=torch.int32)
229
+ part_pd = (torch.empty(gslices, NC * W, device=dev, dtype=f32) if gslices > 1 else empty)
230
+ part_eff = (torch.empty(nsl, NC * W, device=dev, dtype=f32) if nsl > 1 else empty)
231
+ part_g = (torch.empty(nsl, NC * W, device=dev, dtype=f32) if nsl > 1 else empty)
232
+ pd_out = part_pd if gslices > 1 else d_k_pd
233
+ eff_out = part_eff if nsl > 1 else d_k_eff
234
+ g_out = part_g if nsl > 1 else grad_g_decay
235
+
236
+ n = p1.spay_k.shape[-1]
237
+ kpay = p1.spay_k.reshape(-1, n)
238
+ rec = dc.build_records_pa(kpay, k_idx, k_val, p1.log_k, beta, p1.cpe)
239
+ L = dc.tile_len(C, W, hd, P)
240
+ quant = QUANT_CODES.get(ctx.snapshot_quant, 0) # fp8 / int4 here: compact rows (payload, scale) of the fx DW
241
+ snap_rows = SC * W if (snapshot.shape[0] == NC * W if quant else snapshot.numel() == NC * W * hd) else 0
242
+ # The snapshot layout the forward wrote decides the reader.
243
+ layout = ctx.quant_layout
244
+ plan = pa_plan(C, dtype, grad_memory.dtype, dev)
245
+ if plan is None: # admitted by _supported, so this cannot happen
246
+ raise RuntimeError("SDM backward: no write-side mover plan fits the device's shared memory")
247
+ vbits, depth, nw = plan
248
+ dbg = vbits
249
+ pipe = True # (the kernels' pipe argument)
250
+ mod = dc._module()
251
+ d_u_rows = d_u.view(NC, hd)
252
+ snap_arg = snapshot.view(torch.uint8) if quant else snapshot # fp8 payloads are float8_e4m3fn tensors: bytes
253
+ bpre = torch.empty(0, device=dev, dtype=torch.int32)
254
+ if layout and snap_rows:
255
+ dbg |= 256 # the forward's gather wrote the compact snapshot
256
+ if layout == 2:
257
+ dbg |= 1024 # dense rows (valid-group ordinal)
258
+ gdwc = torch.empty(0, device=dev, dtype=grad_memory.dtype) # (the kernels' dense grad-row argument: unused)
259
+ if dbg & 1024:
260
+ bpre = p1.dc_bpre.reshape(N, -1)
261
+
262
+ gds = mo.make_mover_gds(memory, grad_memory, gr, q_idx, q_val, p1.log_q, d_q_w, chunk_size=C, num_parallel=P,
263
+ valid_tokens=T, sorted_q=p1.spay_q, qrec=movers.read_records(p1),
264
+ tiles_q=mo._nonempty(p1.tiles_q), snapshot_quant=ctx.snapshot_quant)
265
+ k_rows = k_idx.view(NC, W)
266
+ x_t = x.transpose(1, 2)
267
+ # Phase A runs on gx_pa2_cuda where it applies (the same bits as pa_dc_cuda's kernel, fewer instructions per
268
+ # group), chosen once per backward from shapes and dtypes.
269
+ # The per-step update d_u(ti) += x(ti)^T d_dv(ti) runs on gx_gemm_cuda: one FP32 kernel with a fixed per-element
270
+ # order (fmaf over j ascending from +0, then one rounded add of d_u), so the step's Phase A can follow it with
271
+ # programmatic dependent launch. Used only where it reproduces this call's cuBLAS bits: the prefilters (IEEE fp32
272
+ # matmuls, P >= 2, C >= 2) and the run-time qualification matches_cublas() (this kernel against the torch.baddbmm
273
+ # call on random data, once per configuration and settings; cuBLAS during a capture of an unqualified
274
+ # configuration). Either way: cuBLAS's bits.
275
+ gemm_step = None
276
+ if P >= 2 and C >= 2 and _fp32_matmul_ieee():
277
+ from .cuda import gx_gemm_cuda as gxgemm
278
+ if gxgemm.supported(d_u, x, d_delta_v, C) and gxgemm.matches_cublas(d_u, x, d_delta_v, P):
279
+ gemm_step = gxgemm.make_step(d_u, x, d_delta_v, P)
280
+ STATS["gx_gemm"] = STATS.get("gx_gemm", 0) + 1
281
+ # With programmatic dependent launch, the GEMM launches behind the GWR (it reads x and its d_u values before its
282
+ # wait) and Phase A behind the GEMM (its setup and first loads overlap the GEMM). The GEMM's CTAs are capped at one
283
+ # per SM (shared-memory padding), so an early launch spreads them over the SMs instead of packing them onto the few
284
+ # the GWR's last wave frees.
285
+ gemm_pdl = gemm_step is not None and bool(gemm_step.pdl_capable)
286
+ pa_fn = mod.dc_pa
287
+ from .cuda import gx_pa2_cuda as gxpa2
288
+ pa_q = scale if quant else gdwc # the quantized rows' scales, in dc_pa's gdwc slot
289
+ if gxpa2.supported(grad_memory, memory, snap_arg, C, dbg, depth, nw, False, scales=scale if quant else None):
290
+ pa_fn = gxpa2.launcher(pdl=gemm_pdl, bank=memory, grad=grad_memory, snap=snap_arg) # once per backward
291
+ # its warps per CTA and ring depth (outputs depend on neither), for these rows (quantized rows: their footprint)
292
+ nw, depth = gxpa2.plan(grad_memory, memory, C, gxpa2.quant_of(memory, snap_arg))
293
+ # one wave on small grids (outputs do not depend on L)
294
+ L = gxpa2.tile_len(C, W, hd, P, L, device=memory.device)
295
+ STATS["gx_pa2"] = STATS.get("gx_pa2", 0) + 1
296
+ elif quant: # the forward wrote these rows only where backward.supported admits gx_pa2's quantized instantiations
297
+ raise RuntimeError("SDM backward: quantized compact snapshot rows that gx_pa2 cannot read")
298
+ elif ctx.snapshot_quant == "bf16": # bf16 rows: the forward wrote them only where gx_pa2 reads them (dc_pa3 cannot)
299
+ raise RuntimeError("SDM backward: bf16 compact snapshot rows that gx_pa2 cannot read")
300
+ elif hd % 4: # _supported admitted it for gx_pa2 only (dc_pa3 takes hd % 4 == 0); nothing written yet
301
+ raise RuntimeError("SDM backward: head_dim % 4 != 0 needs gx_pa2's Phase A, which declined this call")
302
+ elif snap_arg.numel() // hd >= dc._WIDE_SNAP_ROWS: # dc_pa3: its 64-bit snapshot rows (host-only dbg bit; dc_pa
303
+ dbg |= dc.DBG_WIDE # takes them by itself from 2**31 rows; tests force them)
304
+ gds_pdl = bool(getattr(gds, "pdl_capable", False))
305
+ # Step ti's GWR (ti < Nt - 1) launches right after step ti + 1's GDS with programmatic dependent launch (sm_90+).
306
+ from .cuda import mover_fx_cuda as _fxm_b # whether dc_gwr carries the launch hooks
307
+ gwr_pdl = _fxm_b.gwr_pdl_ok(grad_memory.device)
308
+ # The step loop below runs as ONE C++ call (bwd_steps_cuda) whenever every per-step launcher has a plan.
309
+ cpp = _steps_cpp(Nt=Nt, T=T, C=C, P=P, SC=SC, W=W, L=L, nw=nw, depth=depth, dbg=dbg, pipe=pipe,
310
+ grad_memory=grad_memory, memory=memory, snap_arg=snap_arg, snap_rows=snap_rows, d_u=d_u,
311
+ d_u_rows=d_u_rows, x_t=x_t, d_delta_v=d_delta_v, delta_v=delta_v, beta=beta, kpay=kpay, rec=rec,
312
+ eff_out=eff_out, g_out=g_out, bpre=bpre, pa_q=pa_q, gdwc=gdwc, k_rows=k_rows, write_w=write_w,
313
+ pd_out=pd_out, geo=geo, empty_i=empty_i, gemm_step=gemm_step, gemm_pdl=gemm_pdl, pa_fn=pa_fn,
314
+ gds=gds, gds_pdl=gds_pdl, gwr_pdl=gwr_pdl)
315
+ steps = () if cpp else zip(d_u.view(Nt, P, C, hd).unbind(0), x_t.unflatten(0, (Nt, P)).unbind(0),
316
+ d_delta_v.view(Nt, P, C, hd).unbind(0))
317
+ for ti, (d_u_ti, x_t_ti, d_dv_ti) in reversed(list(enumerate(steps))):
318
+ valid = min(C, T - ti * C)
319
+ mod.dc_gwr(grad_memory, k_rows, write_w, d_delta_v, delta_v, pd_out, ti * SC, ti * SC,
320
+ SC, C, valid, geo.vec_bytes, geo.lanes, geo.groups, geo.tpc,
321
+ empty_i, gdwc,
322
+ 1 if (ti < Nt - 1 and gwr_pdl) else 0)
323
+ if gemm_step is not None:
324
+ gemm_step(ti, pdl=gemm_pdl)
325
+ else:
326
+ torch.baddbmm(d_u_ti, x_t_ti, d_dv_ti, out=d_u_ti)
327
+ pa_fn(grad_memory, memory, snap_arg, snap_rows, d_u_rows, beta, kpay, rec, eff_out,
328
+ g_out, ti, P, C, W, L, nw, depth, dbg, pipe, bpre, pa_q)
329
+ # The GDS launches with programmatic dependent launch after Phase A (which triggers at its start, or after its
330
+ # own wait when it follows the GEMM); the kernel right before writes none of the GDS's pre-wait inputs (tile
331
+ # bounds, records, grad_readings rows).
332
+ if gds_pdl:
333
+ gds(ti, pdl=True)
334
+ else:
335
+ gds(ti)
336
+ if nsl > 1 or gslices > 1:
337
+ mod.dc_pa_reduce(part_eff, part_g, part_pd, beta, p1.cpe, d_k_eff, grad_g_decay, d_k_pd,
338
+ P, C, W, T)
339
+
340
+ d_delta_v = d_delta_v.view(N, C, hd)
341
+ torch.baddbmm(d_delta_v, p1.qk.transpose(1, 2), gr.view(N, C, hd), out=d_delta_v)
342
+ grad_v = torch.empty(N, C, hd, device=dev, dtype=dtype)
343
+ grad_beta = torch.empty(NC, device=dev, dtype=f32)
344
+ u, grad_beta_u = mo._pre_bmm(residual.view(N, C, hd), beta, d_u, grad_v)
345
+ d_x = torch.bmm(d_delta_v, u.transpose(1, 2))
346
+ d_m = torch.baddbmm(d_x, torch.bmm(x_t, d_x), x_t, beta=0.0, alpha=-1.0, out=d_x)
347
+ d_a = mo._post_bmm(d_m, p1.a, beta, grad_beta_u, grad_beta)
348
+ d_qk = torch.bmm(gr.view(N, C, hd), delta_v.view(N, C, hd).transpose(1, 2))
349
+ # The Phase-1 backward, with its faster variant at some chunk sizes (the same bits).
350
+ grad_k_val, grad_q_val, grad_g = _p1bwd.phase1_backward_det(
351
+ p1.payload, p1.log_k, p1.log_q, p1.rex, k_val, q_val, g, d_a, d_qk, d_k_eff,
352
+ d_k_pd, d_q_w)
353
+ grad_g += grad_g_decay
354
+ if lay.G == 1:
355
+ grad_g = grad_g.sum(-1)
356
+
357
+ grads = [mo.unstage_time_major(t, P, T, C, dt).view(shape) for t, (shape, dt) in
358
+ zip((grad_k_val, grad_v, grad_beta, grad_g, grad_q_val), ctx.inputs)]
359
+ grad_k_val, grad_v, grad_beta, grad_g, grad_q_val = grads
360
+ if grad_f32:
361
+ grad_memory = grad_memory.to(dtype)
362
+ if bank_user is not None:
363
+ # The restored bank into the caller's (offset) tensor through a raw copy: like the reference backward's raw
364
+ # restore, it does not bump the saved tensor's autograd version (a retained graph can run backward again).
365
+ dc._module().dc_copy_raw(bank_user, memory)
366
+ return (grad_memory, None, grad_k_val, grad_v, grad_beta, grad_g, None, grad_q_val,
367
+ None, None, None, None, None)
@@ -0,0 +1,70 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ """Inference state for SparseDeltaMemory.
4
+
5
+ ``Cache`` is a small ABC; ``SDMLayerState`` holds the memory bank and the running
6
+ sequence length.
7
+ """
8
+
9
+ from abc import ABC, abstractmethod
10
+
11
+ import torch
12
+
13
+
14
+ def l2_normalize(x: torch.Tensor, dim: int = -1, eps: float = 1e-12) -> torch.Tensor:
15
+ """L2 normalize a tensor along a given dimension."""
16
+ return x / (torch.norm(x, p=2, dim=dim, keepdim=True) + eps)
17
+
18
+
19
+ class Cache(ABC):
20
+ """Base class for all caches. A model's full cache is a ``list[Cache]``."""
21
+
22
+ @abstractmethod
23
+ def __getitem__(self, *args, **kwargs) -> torch.Tensor | int:
24
+ pass
25
+
26
+ @property
27
+ @abstractmethod
28
+ def cache_len(self) -> int:
29
+ pass
30
+
31
+ @abstractmethod
32
+ def update_(self, *args, **kwargs) -> "Cache":
33
+ """Update the cache in place."""
34
+ pass
35
+
36
+ @abstractmethod
37
+ def reset(self) -> None:
38
+ """Reset the state."""
39
+ pass
40
+
41
+
42
+ class SDMLayerState(Cache):
43
+ """Inference/decode state for a ``SparseDeltaMemory`` layer: the memory bank
44
+ plus the number of tokens consumed so far."""
45
+
46
+ def __init__(self, memory: torch.Tensor, seq_len: int = 0) -> None:
47
+ self.memory = memory
48
+ self._cache_len = seq_len
49
+
50
+ def __getitem__(self, key_idx: int) -> torch.Tensor | None:
51
+ if key_idx == 0:
52
+ return self.memory
53
+ raise KeyError
54
+
55
+ def __len__(self) -> int:
56
+ return 1
57
+
58
+ @property
59
+ def cache_len(self) -> int:
60
+ return self._cache_len
61
+
62
+ def reset(self) -> None:
63
+ self.memory.zero_()
64
+ self._cache_len = 0
65
+
66
+ def update_(self, new_memory: torch.Tensor, seq_len: int = 1) -> Cache:
67
+ assert self.memory is not None
68
+ self.memory = new_memory.detach()
69
+ self._cache_len += seq_len
70
+ return self
@@ -0,0 +1,3 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # sparse_delta_memory.cuda: the CUDA / C++ kernels of the package, one module per kernel family; each module builds
3
+ # its extension on first use (see _build), never at import.