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,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
|