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,663 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
"""Sparse Delta Memory layer: a gated delta-rule recurrence over a sparse memory bank.
|
|
4
|
+
|
|
5
|
+
Each token writes to and reads from ``num_writes`` / ``num_reads`` slots of a per-head
|
|
6
|
+
memory bank selected by product keys. The forget gate and input gate are applied only
|
|
7
|
+
to the slots a token touches.
|
|
8
|
+
|
|
9
|
+
The chunk-wise training forward uses the WY representation (triangular solve) to
|
|
10
|
+
compute the exact intra-chunk delta-rule recurrence, so ``chunk_size`` changes
|
|
11
|
+
the output only through floating-point rounding.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import math
|
|
16
|
+
import warnings
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Tuple
|
|
19
|
+
|
|
20
|
+
import torch
|
|
21
|
+
from torch import nn
|
|
22
|
+
from torch.nn import functional as F
|
|
23
|
+
|
|
24
|
+
from einops import rearrange
|
|
25
|
+
|
|
26
|
+
from .cache import SDMLayerState, l2_normalize
|
|
27
|
+
from .cuda_graph import CudaGraphRunner, cuda_graphs_suspended
|
|
28
|
+
from .decode import _decode_launch, sdm_decode_step
|
|
29
|
+
from .memory_ops import (DEFAULT_CHUNK, MAX_CHUNK, MAX_WIDTH, MIN_CHUNK, expand_memory_batch, head_merge,
|
|
30
|
+
head_split, needs_backward, sdm_write_read)
|
|
31
|
+
from .memory_stats import SDMStatsMixin
|
|
32
|
+
from .phase2_kernels import SNAPSHOT_QUANT_MODES
|
|
33
|
+
from .product_key_topk import check_product_key_factors, product_key_topk
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Largest product-key factor (the selector's column ids are 16 bits wide).
|
|
38
|
+
_MAX_PRODUCT_KEY_SIDE = 65535
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _stream_is_capturing(x: torch.Tensor) -> bool:
|
|
42
|
+
"""True when the current CUDA stream is capturing a graph (host syncs are illegal)."""
|
|
43
|
+
if not x.is_cuda:
|
|
44
|
+
return False
|
|
45
|
+
try:
|
|
46
|
+
return torch.cuda.is_current_stream_capturing()
|
|
47
|
+
except RuntimeError:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ScaledSoftmax(nn.Module):
|
|
52
|
+
"""Softmax with logit scaling: softmax(x * scale)."""
|
|
53
|
+
def __init__(self, scale: float = 1.0, dim: int = -1):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.scale = scale
|
|
56
|
+
self.dim = dim
|
|
57
|
+
|
|
58
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
59
|
+
return F.softmax(x * self.scale, dim=self.dim)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class SparseDeltaMemoryArgs:
|
|
64
|
+
"""Configuration for SparseDeltaMemory.
|
|
65
|
+
|
|
66
|
+
Layer placement (which layers use SDM) is controlled by the transformer config's
|
|
67
|
+
`sdm_at` list, not here; this dataclass holds only SDM hyper-parameters.
|
|
68
|
+
"""
|
|
69
|
+
dim: int = -1
|
|
70
|
+
num_writes: int = 64
|
|
71
|
+
num_reads: int = 64
|
|
72
|
+
slots_per_head: int = 10000 # Per-head memory slots: side ** 2 for two product-key factors of side <= 65535, or prod(product_key_factor_dims).
|
|
73
|
+
chunk_size: int | None = None # Kernel chunk size (16..256; None: the op's default, 64): changes results only through floating-point rounding (the intra-chunk recurrence is exact).
|
|
74
|
+
read_act: str = "Softmax"
|
|
75
|
+
write_act: str = "Softmax"
|
|
76
|
+
read_act_scale: float = 1.0
|
|
77
|
+
write_act_scale: float = 1.0
|
|
78
|
+
normalize_readings: bool = False # LayerNorm on per-head readings before the output projection
|
|
79
|
+
norm_eps: float = 1e-6
|
|
80
|
+
# Gating parameters
|
|
81
|
+
dt_bias_init: float = 0.0 # Unused; kept for config compatibility (dt_bias follows the GDN/FLA init).
|
|
82
|
+
A_init_range: tuple[float, float] = (0.0, 16.0) # Range for decay parameter init (uniform random, matching GDN/FLA)
|
|
83
|
+
backprop_on_memory: bool = True # Learn the initial memory (parametric memory).
|
|
84
|
+
output_gate: bool = False # Sigmoid output gate: sigmoid(Wog(x)) * readings
|
|
85
|
+
query_batchnorm: bool = False # BatchNorm on PKM query/key projections (prevents slot collapse)
|
|
86
|
+
log_memory_access_stats: bool = False # Log memory access statistics in eval mode (read/write idx frequency, gate stats)
|
|
87
|
+
log_memory_norms: bool = False # Log memory norm/max stats (expensive: .abs() copies full memory bank)
|
|
88
|
+
num_heads: int = 1 # Independent memory heads. Each head has (slots_per_head, dim/num_heads) memory.
|
|
89
|
+
key_weighted_decay: bool = False # Weight forget gate by key values: exp(g * k_val) instead of exp(g)
|
|
90
|
+
snapshot_quant: str = "none" # Quantize saved snapshot to reduce activation memory: "none" (bank dtype), "fp8", "int4", "bf16" (halves it on float32 banks; "none" on bf16 banks)
|
|
91
|
+
# Replay the write/read of calls that may need a backward from CUDA graphs (see
|
|
92
|
+
# SparseDeltaMemory.forward; under non-reentrant activation checkpointing the layers of one
|
|
93
|
+
# shape share them). Removes host launch overhead; costs static buffers.
|
|
94
|
+
# Calls the graphs cannot serve run eager. On in the debug and flagship configs.
|
|
95
|
+
cuda_graph: bool = False
|
|
96
|
+
# Accumulate the backward's bank gradient (and its gathered grad rows) in float32, cast back to the bank dtype
|
|
97
|
+
# once at the end (no-op for a float32 bank). Forward and snapshot are unchanged; the other gradients
|
|
98
|
+
# differ only through the bank gradient's rounding, which feeds them.
|
|
99
|
+
grad_bank_fp32: bool = False
|
|
100
|
+
# Widths of the per-head product-key factors, any number F >= 1 of them, whose product
|
|
101
|
+
# must equal slots_per_head; the key and query projections then have sum(widths) columns
|
|
102
|
+
# per head. None: two factors of side sqrt(slots_per_head) (a perfect square).
|
|
103
|
+
product_key_factor_dims: list[int] | None = None
|
|
104
|
+
# Former name of chunk_size, still accepted (configs and checkpoints of the previous
|
|
105
|
+
# release): mapped to chunk_size with a DeprecationWarning.
|
|
106
|
+
memory_block_size: int | None = None
|
|
107
|
+
|
|
108
|
+
def __post_init__(self):
|
|
109
|
+
if self.memory_block_size is None:
|
|
110
|
+
return
|
|
111
|
+
if self.chunk_size is not None and self.chunk_size != self.memory_block_size:
|
|
112
|
+
raise ValueError(f"chunk_size={self.chunk_size!r} and memory_block_size="
|
|
113
|
+
f"{self.memory_block_size!r} (its former name) differ; set "
|
|
114
|
+
f"chunk_size only.")
|
|
115
|
+
warnings.warn("SparseDeltaMemoryArgs.memory_block_size is deprecated: use chunk_size.",
|
|
116
|
+
DeprecationWarning, stacklevel=3)
|
|
117
|
+
self.chunk_size, self.memory_block_size = self.memory_block_size, None
|
|
118
|
+
|
|
119
|
+
def __setstate__(self, state):
|
|
120
|
+
# Unpickling skips __post_init__. Args pickled by the previous release hold
|
|
121
|
+
# memory_block_size (default 256 there, stored explicitly) and no chunk_size: map them as construction does,
|
|
122
|
+
# so such layers keep their chunk size.
|
|
123
|
+
self.__dict__.update(state)
|
|
124
|
+
self.__post_init__()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _product_key_side(args: SparseDeltaMemoryArgs) -> int:
|
|
128
|
+
"""Validate the product-key geometry and return the side of its two square factors."""
|
|
129
|
+
sph = args.slots_per_head
|
|
130
|
+
if isinstance(sph, bool) or not isinstance(sph, int) or sph < 1:
|
|
131
|
+
raise ValueError(f"slots_per_head must be a positive int, got {sph!r}.")
|
|
132
|
+
# Slot a * side + b for factor columns a, b < side: only a square bank is addressable.
|
|
133
|
+
side = math.isqrt(sph)
|
|
134
|
+
if side * side != sph:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"slots_per_head={sph} is not a perfect square, so the two-factor product key "
|
|
137
|
+
f"would leave {sph - side * side} slots per head unaddressable. Use "
|
|
138
|
+
f"{side * side} or {(side + 1) ** 2}.")
|
|
139
|
+
if side > _MAX_PRODUCT_KEY_SIDE:
|
|
140
|
+
raise ValueError(f"slots_per_head={sph} exceeds the largest product-key bank, "
|
|
141
|
+
f"{_MAX_PRODUCT_KEY_SIDE}**2.")
|
|
142
|
+
return side
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _product_key_factors(args: SparseDeltaMemoryArgs) -> tuple[int, ...]:
|
|
146
|
+
"""Validate the product-key geometry and return the per-head factor widths: those of
|
|
147
|
+
``product_key_factor_dims``, or two square factors when it is None."""
|
|
148
|
+
dims = args.product_key_factor_dims
|
|
149
|
+
if dims is None:
|
|
150
|
+
side = _product_key_side(args)
|
|
151
|
+
return (side, side)
|
|
152
|
+
sph = args.slots_per_head
|
|
153
|
+
if isinstance(sph, bool) or not isinstance(sph, int) or sph < 1:
|
|
154
|
+
raise ValueError(f"slots_per_head must be a positive int, got {sph!r}.")
|
|
155
|
+
try:
|
|
156
|
+
dims = check_product_key_factors(dims)
|
|
157
|
+
except ValueError as e:
|
|
158
|
+
raise ValueError(f"product_key_factor_dims: {e}") from None
|
|
159
|
+
if math.prod(dims) != sph:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"product_key_factor_dims={list(dims)} address {math.prod(dims)} slots per head, "
|
|
162
|
+
f"but slots_per_head={sph}; set slots_per_head to their product.")
|
|
163
|
+
return dims
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _check_args(args: SparseDeltaMemoryArgs) -> None:
|
|
167
|
+
"""Reject settings outside the supported range with the name of the config field."""
|
|
168
|
+
def is_int(n):
|
|
169
|
+
return isinstance(n, int) and not isinstance(n, bool)
|
|
170
|
+
|
|
171
|
+
for name in ("num_writes", "num_reads"):
|
|
172
|
+
n = getattr(args, name)
|
|
173
|
+
if not is_int(n) or not 1 <= n <= min(MAX_WIDTH, args.slots_per_head):
|
|
174
|
+
raise ValueError(f"{name} must be an int in 1..min({MAX_WIDTH}, slots_per_head="
|
|
175
|
+
f"{args.slots_per_head}), got {n!r}.")
|
|
176
|
+
c = args.chunk_size
|
|
177
|
+
if c is not None and (not is_int(c) or not MIN_CHUNK <= c <= MAX_CHUNK):
|
|
178
|
+
raise ValueError(f"chunk_size must be None or an int in {MIN_CHUNK}..{MAX_CHUNK}, "
|
|
179
|
+
f"got {c!r}.")
|
|
180
|
+
if args.snapshot_quant not in SNAPSHOT_QUANT_MODES:
|
|
181
|
+
raise ValueError(f"snapshot_quant must be one of {SNAPSHOT_QUANT_MODES}, "
|
|
182
|
+
f"got {args.snapshot_quant!r}.")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class SparseDeltaMemory(SDMStatsMixin, nn.Module):
|
|
186
|
+
"""Sparse memory with delta-rule (DeltaNet-style) gated updates.
|
|
187
|
+
|
|
188
|
+
- Forget gate g controls exponential decay of memory
|
|
189
|
+
- Input gate beta controls how much new information to integrate
|
|
190
|
+
- Gates only apply to memory slots touched by sparse keys
|
|
191
|
+
|
|
192
|
+
Per token:
|
|
193
|
+
- g = -A * softplus(a_proj(x) + dt_bias)
|
|
194
|
+
- beta = sigmoid(b_proj(x))
|
|
195
|
+
- memory[idx] *= exp(g)
|
|
196
|
+
- memory[idx] += beta * k @ (v - memory[idx] @ k).T
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, args: SparseDeltaMemoryArgs, layer_id: int):
|
|
200
|
+
super().__init__()
|
|
201
|
+
self.args = args
|
|
202
|
+
self.layer_id = layer_id
|
|
203
|
+
self.dim = args.dim
|
|
204
|
+
self.num_heads = args.num_heads
|
|
205
|
+
|
|
206
|
+
# Per-head dimensions
|
|
207
|
+
H = self.num_heads
|
|
208
|
+
if H < 1:
|
|
209
|
+
raise ValueError(f"num_heads={H} must be >= 1.")
|
|
210
|
+
if args.dim % H != 0:
|
|
211
|
+
raise ValueError(f"dim={args.dim} must be divisible by num_heads={H}.")
|
|
212
|
+
self.head_dim = args.dim // H
|
|
213
|
+
|
|
214
|
+
# Each head owns a bank of slots_per_head rows; the layer bank is
|
|
215
|
+
# num_heads * slots_per_head rows of head_dim.
|
|
216
|
+
self.slots_per_head = args.slots_per_head
|
|
217
|
+
self.product_key_factors = _product_key_factors(args)
|
|
218
|
+
self.key_size_per_head = sum(self.product_key_factors)
|
|
219
|
+
_check_args(args)
|
|
220
|
+
self.key_size = H * self.key_size_per_head
|
|
221
|
+
total_mem_slots = H * self.slots_per_head
|
|
222
|
+
# The kernels address bank rows with int32 slot ids; the op checks the batch
|
|
223
|
+
# multiple B * total_mem_slots at runtime.
|
|
224
|
+
if total_mem_slots >= 2**31:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"num_heads * slots_per_head = {total_mem_slots} must be below 2**31, the "
|
|
227
|
+
f"int32 slot-index range of the kernels.")
|
|
228
|
+
|
|
229
|
+
# Query and Key projections
|
|
230
|
+
self.Wq_read = nn.Linear(args.dim, self.key_size)
|
|
231
|
+
self.Wk_write = nn.Linear(args.dim, self.key_size)
|
|
232
|
+
self.Wv_write = nn.Linear(args.dim, args.dim)
|
|
233
|
+
|
|
234
|
+
# BatchNorm on PKM projections (prevents slot collapse by normalizing
|
|
235
|
+
# the query distribution across the batch, ensuring uniform key space usage)
|
|
236
|
+
self.query_bn = nn.BatchNorm1d(self.key_size) if args.query_batchnorm else None
|
|
237
|
+
self.key_bn = nn.BatchNorm1d(self.key_size) if args.query_batchnorm else None
|
|
238
|
+
|
|
239
|
+
# Gating projections (per-head when num_heads > 1)
|
|
240
|
+
self.a_proj = nn.Linear(args.dim, H) # For forget gate
|
|
241
|
+
self.b_proj = nn.Linear(args.dim, H) # For input gate
|
|
242
|
+
|
|
243
|
+
# Decay parameter (A in deltanet/GDN formulation)
|
|
244
|
+
# Initialized as uniform random in [A_min, A_max], stored in log space (matching FLA)
|
|
245
|
+
A_min, A_max = args.A_init_range
|
|
246
|
+
A_init = torch.empty(H, dtype=torch.float32).uniform_(A_min, A_max).clamp(min=1e-4)
|
|
247
|
+
self.A_log = nn.Parameter(torch.log(A_init))
|
|
248
|
+
self.A_log._no_weight_decay = True
|
|
249
|
+
|
|
250
|
+
# Time step bias, initialized from inv_softplus(uniform(dt_min, dt_max)) matching FLA/GDN,
|
|
251
|
+
# so softplus(a_proj(x) + dt_bias) is about 0.001-0.1 at init and g = -A * dt stays
|
|
252
|
+
# moderate (no exp overflow in WY).
|
|
253
|
+
dt_min, dt_max = 0.001, 0.1
|
|
254
|
+
dt_init_floor = 1e-4
|
|
255
|
+
dt = torch.exp(
|
|
256
|
+
torch.rand(H) * (math.log(dt_max) - math.log(dt_min)) + math.log(dt_min)
|
|
257
|
+
).clamp(min=dt_init_floor)
|
|
258
|
+
inv_dt = dt + torch.log(-torch.expm1(-dt)) # inv_softplus
|
|
259
|
+
self.dt_bias = nn.Parameter(inv_dt)
|
|
260
|
+
self.dt_bias._no_weight_decay = True
|
|
261
|
+
|
|
262
|
+
# Output projection
|
|
263
|
+
self.Wo = nn.Linear(args.dim, args.dim)
|
|
264
|
+
|
|
265
|
+
# Output gate (sigmoid gating)
|
|
266
|
+
self.Wog = nn.Linear(args.dim, args.dim) if args.output_gate else None
|
|
267
|
+
|
|
268
|
+
# Learnable initial memory (when backprop_on_memory=True): H memories of
|
|
269
|
+
# (slots_per_head, head_dim) stored as (H * slots_per_head, head_dim).
|
|
270
|
+
self.memory = (
|
|
271
|
+
nn.Parameter(torch.zeros(total_mem_slots, self.head_dim))
|
|
272
|
+
if args.backprop_on_memory
|
|
273
|
+
else None
|
|
274
|
+
)
|
|
275
|
+
# Tag the learned memory bank so model-size / param-count reporting can exclude it:
|
|
276
|
+
# it is SDM's *state* (read sparsely, negligible FLOPs), not an "active" parameter.
|
|
277
|
+
if self.memory is not None:
|
|
278
|
+
self.memory._sdm_memory_bank = True
|
|
279
|
+
|
|
280
|
+
# Optional per-head normalization of the readings (matching GDN convention).
|
|
281
|
+
self.readings_norm = (
|
|
282
|
+
nn.LayerNorm(self.head_dim, eps=args.norm_eps) if args.normalize_readings else None
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Activation functions
|
|
286
|
+
self.read_act = (
|
|
287
|
+
ScaledSoftmax(scale=args.read_act_scale, dim=-1)
|
|
288
|
+
if args.read_act == "Softmax"
|
|
289
|
+
else l2_normalize
|
|
290
|
+
if args.read_act == "l2_normalize"
|
|
291
|
+
else getattr(nn, args.read_act)()
|
|
292
|
+
)
|
|
293
|
+
self.write_act = (
|
|
294
|
+
ScaledSoftmax(scale=args.write_act_scale, dim=-1)
|
|
295
|
+
if args.write_act == "Softmax"
|
|
296
|
+
else l2_normalize
|
|
297
|
+
if args.write_act == "l2_normalize"
|
|
298
|
+
else getattr(nn, args.write_act)()
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Memory access statistics tracking (opt-in logging; see memory_stats.py).
|
|
302
|
+
self._init_stats(args, H)
|
|
303
|
+
self._stats_skip_warned = False
|
|
304
|
+
self._decode_key = None # the bank layout sdm_decode_step last checked (gated_write_read)
|
|
305
|
+
|
|
306
|
+
self.graph_runner = (CudaGraphRunner(f"SparseDeltaMemory(layer_id={layer_id})")
|
|
307
|
+
if args.cuda_graph else None)
|
|
308
|
+
|
|
309
|
+
def compute_gates(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
310
|
+
"""Compute forget gate g and input gate beta.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
x: Input tensor [B, T, D]
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
g: Forget gate in log space [B, T, H]
|
|
317
|
+
beta: Input gate [B, T, H]
|
|
318
|
+
"""
|
|
319
|
+
a = self.a_proj(x) # [B, T, H]
|
|
320
|
+
A = -torch.exp(self.A_log) # [H], negative for decay
|
|
321
|
+
g = A * F.softplus(a + self.dt_bias) # [B, T, H]
|
|
322
|
+
beta = torch.sigmoid(self.b_proj(x)) # [B, T, H]
|
|
323
|
+
return g, beta
|
|
324
|
+
|
|
325
|
+
def _product_keys(
|
|
326
|
+
self,
|
|
327
|
+
x: torch.Tensor, # [BH, T, key_size_per_head]
|
|
328
|
+
num_keys: int,
|
|
329
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
330
|
+
"""Per-head product-key top-k.
|
|
331
|
+
|
|
332
|
+
Returns (val, idx) of shape [BH, T, num_keys], idx local to the head's bank and
|
|
333
|
+
ascending: (..(c_0 * n_1 + c_1) * ..) * n_{F-1} + c_{F-1} for the columns c_f of the
|
|
334
|
+
factors of widths n_f (a * side + b for the default two square factors). Training,
|
|
335
|
+
prefill and decode all select with the same rule.
|
|
336
|
+
"""
|
|
337
|
+
return product_key_topk(x, self.product_key_factors, num_keys)
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def chunk_size(self) -> int:
|
|
341
|
+
"""The write/read op's chunk size: ``args.chunk_size``, or the op's default (64)."""
|
|
342
|
+
return DEFAULT_CHUNK if self.args.chunk_size is None else self.args.chunk_size
|
|
343
|
+
|
|
344
|
+
def gated_write_read(
|
|
345
|
+
self,
|
|
346
|
+
memory: torch.Tensor,
|
|
347
|
+
k_idx: torch.Tensor,
|
|
348
|
+
k_val: torch.Tensor,
|
|
349
|
+
v: torch.Tensor,
|
|
350
|
+
beta: torch.Tensor,
|
|
351
|
+
g: torch.Tensor,
|
|
352
|
+
q_idx: torch.Tensor,
|
|
353
|
+
q_val: torch.Tensor,
|
|
354
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
355
|
+
"""Gated write + read of all streams; returns (readings [BH, T, head_dim], memory).
|
|
356
|
+
|
|
357
|
+
Inputs are [BH, T, ...] with per-stream slot offsets already added to the
|
|
358
|
+
indices; ``memory`` is [BH * slots_per_head, head_dim] and is advanced in place.
|
|
359
|
+
A call that may need a backward (``needs_backward``: grad mode on and an input
|
|
360
|
+
requiring grad, in train and eval mode alike) saves the backward state, so run
|
|
361
|
+
inference under ``torch.no_grad()`` or ``torch.inference_mode()``. Any other call
|
|
362
|
+
saves nothing, and a single token then takes the fused decode step.
|
|
363
|
+
"""
|
|
364
|
+
B, T, _ = k_idx.shape
|
|
365
|
+
dim = v.shape[2]
|
|
366
|
+
if T == 1 and not needs_backward(memory, k_val, v, beta, g, q_val):
|
|
367
|
+
key = (B, memory.shape, memory.stride(), memory.dtype, memory.device, v.device)
|
|
368
|
+
if key == self._decode_key: # the bank sdm_decode_step checked last: no check per token
|
|
369
|
+
readings = _decode_launch(memory, k_idx, k_val, v, beta, g, q_idx, q_val)
|
|
370
|
+
else: # a new or swapped bank: the slot rows, the public step's checks, then the memo
|
|
371
|
+
if memory.shape[:1] != (B * self.slots_per_head,):
|
|
372
|
+
raise ValueError(f"SparseDeltaMemory: the cache memory has shape {tuple(memory.shape)}; this call "
|
|
373
|
+
f"needs {B * self.slots_per_head} rows ({B} streams x {self.slots_per_head} slots)")
|
|
374
|
+
readings = sdm_decode_step(memory, k_idx, k_val, v, beta, g, q_idx, q_val)
|
|
375
|
+
self._decode_key = key
|
|
376
|
+
else:
|
|
377
|
+
readings = sdm_write_read(
|
|
378
|
+
memory, k_idx, k_val, v, beta, g, q_idx, q_val,
|
|
379
|
+
chunk_size=self.chunk_size,
|
|
380
|
+
num_memory_slots=self.slots_per_head,
|
|
381
|
+
num_parallel=B,
|
|
382
|
+
snapshot_quant=self.args.snapshot_quant,
|
|
383
|
+
key_weighted_decay=self.args.key_weighted_decay,
|
|
384
|
+
grad_bank_fp32=self.args.grad_bank_fp32,
|
|
385
|
+
)
|
|
386
|
+
return readings.view(B, T, dim), memory
|
|
387
|
+
|
|
388
|
+
def _graphed_write_read(self, x, param, cache, inputs):
|
|
389
|
+
"""``gated_write_read`` of the initial bank through the layer's graph runner, or None
|
|
390
|
+
when the call runs plain eager: it needs no backward, or starts from a cache. The
|
|
391
|
+
runner replays the graphs, or runs eager inside a graph capture or
|
|
392
|
+
``suspend_cuda_graphs()``, for another call than the captured one, or after a failed
|
|
393
|
+
capture; either way the call saves the same tensors for backward. Returns the
|
|
394
|
+
readings and the call's state (a ``cuda_graph.ReplayedState`` for a replayed call)."""
|
|
395
|
+
bank = param if cache is None else cache.memory
|
|
396
|
+
if not needs_backward(*(inputs if bank is None else (bank, *inputs))):
|
|
397
|
+
return None
|
|
398
|
+
if cache is not None:
|
|
399
|
+
# The graphs start from the layer's own initial bank.
|
|
400
|
+
self.graph_runner.note("eager_cache",
|
|
401
|
+
"a differentiable call that starts from a cache runs eager.")
|
|
402
|
+
return None
|
|
403
|
+
BH, T, _ = inputs[0].shape
|
|
404
|
+
call = (param, (BH * self.slots_per_head, self.head_dim),
|
|
405
|
+
x.dtype if param is None else param.dtype, inputs,
|
|
406
|
+
(self.chunk_size, self.slots_per_head, BH,
|
|
407
|
+
self.args.snapshot_quant, self.args.grad_bank_fp32))
|
|
408
|
+
if _stream_is_capturing(x) or cuda_graphs_suspended():
|
|
409
|
+
readings, memory = self.graph_runner.eager(*call)
|
|
410
|
+
state = SDMLayerState(memory=memory, seq_len=T)
|
|
411
|
+
else:
|
|
412
|
+
readings, state = self.graph_runner(*call, seq_len=T)
|
|
413
|
+
return readings.view(BH, T, self.head_dim), state
|
|
414
|
+
|
|
415
|
+
# The kernels sit behind an autograd.Function with Triton/CUDA launches; dynamo
|
|
416
|
+
# tracing into them only adds compile time. The surrounding model still compiles.
|
|
417
|
+
@torch.compiler.disable
|
|
418
|
+
def forward(
|
|
419
|
+
self,
|
|
420
|
+
x: torch.Tensor,
|
|
421
|
+
freqs_cis: torch.Tensor | None = None,
|
|
422
|
+
mask: torch.Tensor | None = None,
|
|
423
|
+
cache: SDMLayerState | None = None,
|
|
424
|
+
) -> Tuple[torch.Tensor, SDMLayerState | None]:
|
|
425
|
+
"""Forward pass with gated memory updates.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
x: Input [B, T, D] or [B*T, D]
|
|
429
|
+
freqs_cis: Positional encoding tensor, used to extract seq_len when x is 2D
|
|
430
|
+
mask: Not used, for compatibility
|
|
431
|
+
cache: Optional memory state
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
output: Output [B, T, D] or [B*T, D] (matches input)
|
|
435
|
+
new_cache: Updated memory state
|
|
436
|
+
|
|
437
|
+
The bank is advanced in place. When the call may need a backward (see
|
|
438
|
+
``gated_write_read``), the backward restores it to its initial state (with
|
|
439
|
+
``snapshot_quant`` fp8/int4, the written rows to their dequantized snapshot), so the
|
|
440
|
+
returned state and a passed-in ``cache`` of an eager call hold the final bank only
|
|
441
|
+
until backward runs; clone ``new_cache.memory`` before backward to carry it into the
|
|
442
|
+
next segment. (With ``cuda_graph``, the option's eager fallback keeps its final bank,
|
|
443
|
+
and a replayed call's state returns a copy at its first read, see below.)
|
|
444
|
+
|
|
445
|
+
With ``cuda_graph``, a call that may need a backward replays CUDA graphs (see
|
|
446
|
+
cuda_graph.py): the layer's own, or under non-reentrant activation checkpointing a set
|
|
447
|
+
it may share with layers of its shape, whose first call captures the forward graph and
|
|
448
|
+
whose first backward the backward graph. A call of another shape, a
|
|
449
|
+
call with a ``cache``, a call inside ``suspend_cuda_graphs()`` or a capture, and every
|
|
450
|
+
call after a failed capture run eager (``reset_cuda_graphs()`` drops the graphs after
|
|
451
|
+
a permanent change). A replayed call's ``new_cache`` (``cuda_graph.ReplayedState``)
|
|
452
|
+
never exposes the graphs' static bank: the first read of its ``memory`` returns a copy
|
|
453
|
+
of the call's final bank, and raises if a later forward of this layer, of a layer
|
|
454
|
+
sharing its graphs or of a layer captured before it, or the backward, ran since.
|
|
455
|
+
"""
|
|
456
|
+
if x.ndim == 2:
|
|
457
|
+
# x is [B*T, D], extract seq_len from freqs_cis
|
|
458
|
+
seq_len, _ = freqs_cis.shape
|
|
459
|
+
bsz = x.shape[0] // seq_len
|
|
460
|
+
x = x.reshape(bsz, seq_len, self.args.dim)
|
|
461
|
+
input_was_2d = True
|
|
462
|
+
else:
|
|
463
|
+
input_was_2d = False
|
|
464
|
+
|
|
465
|
+
B, T, D = x.shape
|
|
466
|
+
H = self.num_heads
|
|
467
|
+
sph = self.slots_per_head
|
|
468
|
+
hd = self.head_dim
|
|
469
|
+
BH = B * H
|
|
470
|
+
|
|
471
|
+
# Statistics read tensor values on the host, so they are collected only in eval
|
|
472
|
+
# mode and never during CUDA-graph capture.
|
|
473
|
+
stats_on = (self.args.log_memory_access_stats and not self.training
|
|
474
|
+
and not _stream_is_capturing(x))
|
|
475
|
+
if self.args.log_memory_access_stats and self.training and not self._stats_skip_warned:
|
|
476
|
+
self._stats_skip_warned = True
|
|
477
|
+
logger.warning(
|
|
478
|
+
"SparseDeltaMemory(layer_id=%s): log_memory_access_stats is collected in "
|
|
479
|
+
"eval mode only; nothing is recorded while training.", self.layer_id)
|
|
480
|
+
|
|
481
|
+
g, beta = self.compute_gates(x) # [B, T, H] each
|
|
482
|
+
if stats_on:
|
|
483
|
+
self._record_gate_stats(g, beta)
|
|
484
|
+
|
|
485
|
+
v = self.Wv_write(x) # [B, T, D]
|
|
486
|
+
|
|
487
|
+
# Product keys: project, then split into heads.
|
|
488
|
+
raw_k = self.Wk_write(x) # [B, T, key_size]
|
|
489
|
+
raw_q = self.Wq_read(x) # [B, T, key_size]
|
|
490
|
+
if self.key_bn is not None:
|
|
491
|
+
raw_k = self.key_bn(raw_k.reshape(B * T, -1)).reshape(B, T, -1)
|
|
492
|
+
if self.query_bn is not None:
|
|
493
|
+
raw_q = self.query_bn(raw_q.reshape(B * T, -1)).reshape(B, T, -1)
|
|
494
|
+
raw_k = head_split(raw_k, H) # [BH, T, key_size_per_head]
|
|
495
|
+
raw_q = head_split(raw_q, H)
|
|
496
|
+
|
|
497
|
+
k_val, k_idx = self._product_keys(raw_k, self.args.num_writes)
|
|
498
|
+
q_val, q_idx = self._product_keys(raw_q, self.args.num_reads)
|
|
499
|
+
q_val = self.read_act(q_val)
|
|
500
|
+
k_val = self.write_act(k_val)
|
|
501
|
+
|
|
502
|
+
if stats_on:
|
|
503
|
+
self._record_read_stats(q_idx, q_val, BH, H, sph)
|
|
504
|
+
|
|
505
|
+
v = head_split(v, H) # [BH, T, hd]
|
|
506
|
+
g = rearrange(g, 'b t h -> (b h) t 1')
|
|
507
|
+
beta = rearrange(beta, 'b t h -> (b h) t 1')
|
|
508
|
+
|
|
509
|
+
# Key-weighted decay: exp(g * k_val) instead of exp(g), per write.
|
|
510
|
+
if self.args.key_weighted_decay:
|
|
511
|
+
g = g * k_val # [BH, T, num_writes]
|
|
512
|
+
|
|
513
|
+
# Stream offsets: stream (b, h) owns slots [(b*H + h) * sph, (b*H + h + 1) * sph).
|
|
514
|
+
offset = torch.arange(BH, device=x.device).view(BH, 1, 1) * sph
|
|
515
|
+
k_idx = k_idx + offset
|
|
516
|
+
q_idx = q_idx + offset
|
|
517
|
+
inputs = (k_idx, k_val, v, beta, g, q_idx, q_val)
|
|
518
|
+
|
|
519
|
+
param = None if self.memory is None else self.memory.reshape(self.memory.shape[0], -1)
|
|
520
|
+
graphed = new_cache = None
|
|
521
|
+
if self.graph_runner is not None:
|
|
522
|
+
graphed = self._graphed_write_read(x, param, cache, inputs)
|
|
523
|
+
if graphed is not None:
|
|
524
|
+
readings, new_cache = graphed
|
|
525
|
+
if stats_on: # norms of the initial bank: B copies of the parameter, or zeros
|
|
526
|
+
with torch.no_grad():
|
|
527
|
+
self._record_memory_norms_pre(
|
|
528
|
+
torch.zeros(BH * sph, hd, device=x.device, dtype=x.dtype)
|
|
529
|
+
if param is None else expand_memory_batch(param, B).reshape(BH * sph, hd))
|
|
530
|
+
else:
|
|
531
|
+
# Memory [BH * sph, hd]: private per-sequence copies, advanced in place.
|
|
532
|
+
if cache is not None:
|
|
533
|
+
memory = cache.memory
|
|
534
|
+
elif param is not None:
|
|
535
|
+
memory = expand_memory_batch(param, B).reshape(BH * sph, hd)
|
|
536
|
+
else:
|
|
537
|
+
memory = torch.zeros(BH * sph, hd, device=x.device, dtype=x.dtype)
|
|
538
|
+
if stats_on:
|
|
539
|
+
self._record_memory_norms_pre(memory)
|
|
540
|
+
readings, memory = self.gated_write_read(memory, *inputs)
|
|
541
|
+
# A float32 cache bank yields float32 readings; the rest runs in the compute dtype.
|
|
542
|
+
readings = readings.to(v.dtype)
|
|
543
|
+
|
|
544
|
+
# Per-head normalization of the readings (matching GDN convention)
|
|
545
|
+
if self.readings_norm is not None:
|
|
546
|
+
readings = self.readings_norm(readings)
|
|
547
|
+
|
|
548
|
+
readings = head_merge(readings, H) # [B, T, D]
|
|
549
|
+
|
|
550
|
+
# Output gate: sigmoid(Wog(x)) * readings
|
|
551
|
+
if self.Wog is not None:
|
|
552
|
+
readings = torch.sigmoid(self.Wog(x)) * readings
|
|
553
|
+
|
|
554
|
+
output = self.Wo(readings)
|
|
555
|
+
|
|
556
|
+
if stats_on:
|
|
557
|
+
self._record_write_stats(k_idx, k_val, memory if graphed is None else new_cache.memory,
|
|
558
|
+
output, BH, H, sph)
|
|
559
|
+
|
|
560
|
+
if input_was_2d:
|
|
561
|
+
output = output.reshape(B * T, D)
|
|
562
|
+
|
|
563
|
+
if cache is not None:
|
|
564
|
+
cache.update_(memory, seq_len=T)
|
|
565
|
+
new_cache = cache
|
|
566
|
+
elif graphed is None:
|
|
567
|
+
new_cache = SDMLayerState(memory=memory, seq_len=T)
|
|
568
|
+
|
|
569
|
+
return output, new_cache
|
|
570
|
+
|
|
571
|
+
def init_weights(
|
|
572
|
+
self, init_std: float | None = None, factor: float = 1.0, width_scaling: float | None = None
|
|
573
|
+
) -> None:
|
|
574
|
+
"""Initialize weights.
|
|
575
|
+
|
|
576
|
+
Must reinitialize ALL parameters including bare nn.Parameters: the training loop
|
|
577
|
+
calls model.to_empty(device='cuda'), which leaves them uninitialized.
|
|
578
|
+
"""
|
|
579
|
+
in_init_std = (init_std or (self.dim ** (-0.5))) / factor
|
|
580
|
+
|
|
581
|
+
for w in [self.Wq_read, self.Wk_write, self.Wv_write, self.Wo,
|
|
582
|
+
self.a_proj, self.b_proj]:
|
|
583
|
+
nn.init.trunc_normal_(
|
|
584
|
+
w.weight, mean=0.0, std=in_init_std,
|
|
585
|
+
a=-3 * in_init_std, b=3 * in_init_std
|
|
586
|
+
)
|
|
587
|
+
if w.bias is not None:
|
|
588
|
+
nn.init.zeros_(w.bias)
|
|
589
|
+
|
|
590
|
+
if self.Wog is not None:
|
|
591
|
+
nn.init.trunc_normal_(
|
|
592
|
+
self.Wog.weight, mean=0.0, std=in_init_std,
|
|
593
|
+
a=-3 * in_init_std, b=3 * in_init_std
|
|
594
|
+
)
|
|
595
|
+
if self.Wog.bias is not None:
|
|
596
|
+
nn.init.zeros_(self.Wog.bias)
|
|
597
|
+
|
|
598
|
+
A_min, A_max = self.args.A_init_range
|
|
599
|
+
with torch.no_grad():
|
|
600
|
+
self.A_log.uniform_(A_min, A_max).clamp_(min=1e-4).log_()
|
|
601
|
+
# dt_bias from inv_softplus(uniform(0.001, 0.1)) matching FLA
|
|
602
|
+
dt_min, dt_max, dt_init_floor = 0.001, 0.1, 1e-4
|
|
603
|
+
self.dt_bias.uniform_(math.log(dt_min), math.log(dt_max)).exp_().clamp_(min=dt_init_floor)
|
|
604
|
+
self.dt_bias.data.copy_(self.dt_bias.data + torch.log(-torch.expm1(-self.dt_bias.data)))
|
|
605
|
+
|
|
606
|
+
if self.memory is not None:
|
|
607
|
+
nn.init.trunc_normal_(
|
|
608
|
+
self.memory,
|
|
609
|
+
mean=0.0,
|
|
610
|
+
std=in_init_std,
|
|
611
|
+
a=-3 * in_init_std,
|
|
612
|
+
b=3 * in_init_std,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if self.readings_norm is not None:
|
|
616
|
+
self.readings_norm.reset_parameters()
|
|
617
|
+
|
|
618
|
+
# BatchNorm on queries/keys: standard init (queries must produce nonzero values).
|
|
619
|
+
for norm in [self.query_bn, self.key_bn]:
|
|
620
|
+
if norm is not None:
|
|
621
|
+
norm.reset_parameters()
|
|
622
|
+
|
|
623
|
+
def create_kv_cache(
|
|
624
|
+
self,
|
|
625
|
+
bsz: int,
|
|
626
|
+
seq_len: int,
|
|
627
|
+
dtype: torch.dtype,
|
|
628
|
+
device: str | torch.device | None = None,
|
|
629
|
+
initial_memory: torch.Tensor | None = None,
|
|
630
|
+
) -> SDMLayerState:
|
|
631
|
+
"""Create the inference state.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
initial_memory: optional [bsz * num_heads * slots_per_head, head_dim] bank to
|
|
635
|
+
start from (e.g. the state after a previous segment). It is copied.
|
|
636
|
+
Defaults to the learned initial memory, or zeros without one. A state
|
|
637
|
+
produced by a differentiable forward is valid only until backward (see
|
|
638
|
+
``forward``).
|
|
639
|
+
"""
|
|
640
|
+
H = self.num_heads
|
|
641
|
+
sph = self.slots_per_head
|
|
642
|
+
hd = self.head_dim
|
|
643
|
+
BH = bsz * H
|
|
644
|
+
if initial_memory is not None:
|
|
645
|
+
expected = (BH * sph, hd)
|
|
646
|
+
if tuple(initial_memory.shape) != expected:
|
|
647
|
+
raise ValueError(
|
|
648
|
+
f"initial_memory must have shape {expected}, got "
|
|
649
|
+
f"{tuple(initial_memory.shape)}.")
|
|
650
|
+
memory = initial_memory.to(dtype=dtype, device=device).clone()
|
|
651
|
+
elif self.memory is not None:
|
|
652
|
+
# Materialize a sharded (DTensor) parameter, then cast and clone in one step.
|
|
653
|
+
# The parameter may live on another device, so `device` is applied explicitly.
|
|
654
|
+
mem = self.memory
|
|
655
|
+
if hasattr(mem, 'full_tensor'):
|
|
656
|
+
mem = mem.full_tensor()
|
|
657
|
+
memory = mem.unsqueeze(0).expand(bsz, -1, -1).reshape(
|
|
658
|
+
BH * sph, hd
|
|
659
|
+
).to(dtype=dtype, device=device).clone()
|
|
660
|
+
del mem
|
|
661
|
+
else:
|
|
662
|
+
memory = torch.zeros(BH * sph, hd, dtype=dtype, device=device)
|
|
663
|
+
return SDMLayerState(memory=memory, seq_len=seq_len)
|