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