initium-deeplearning 0.1.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 (71) hide show
  1. initium/LNN_controller/cfc_backbone_parallel.py +265 -0
  2. initium/LNN_controller/cfc_controller.py +325 -0
  3. initium/LNN_controller/chained_controller.py +83 -0
  4. initium/LNN_controller/hybrid_controller.py +90 -0
  5. initium/MoE/README.md +1 -0
  6. initium/MoE/moe_layer.py +552 -0
  7. initium/__init__.py +1 -0
  8. initium/config/controller_config.py +86 -0
  9. initium/config/train_config.py +64 -0
  10. initium/core_training.py +2182 -0
  11. initium/data/__init__.py +0 -0
  12. initium/data/audio/__init__.py +0 -0
  13. initium/data/audio/audio_dataset.py +96 -0
  14. initium/data/base_dataset.py +75 -0
  15. initium/data/common/__init__.py +0 -0
  16. initium/data/common/chain_task.py +542 -0
  17. initium/data/common/digit_codec.py +85 -0
  18. initium/data/common/graph_io.py +123 -0
  19. initium/data/common/real_data.py +747 -0
  20. initium/data/dataset_registry.py +59 -0
  21. initium/data/graph_traversal/__init__.py +0 -0
  22. initium/data/graph_traversal/graph_traversal.py +746 -0
  23. initium/data/multimodal/__init__.py +0 -0
  24. initium/data/multimodal/multimodal_dataset.py +401 -0
  25. initium/data/text/__init__.py +0 -0
  26. initium/data/text/text_dataset.py +79 -0
  27. initium/data/video/__init__.py +0 -0
  28. initium/data/video/video_dataset.py +85 -0
  29. initium/inference/__init__.py +1 -0
  30. initium/inference/cache/__init__.py +1 -0
  31. initium/inference/cache/base_cache.py +112 -0
  32. initium/inference/cache/cache_config.py +21 -0
  33. initium/inference/cache/cache_registry.py +99 -0
  34. initium/inference/cache/cached_engine.py +264 -0
  35. initium/inference/cache/keys.py +77 -0
  36. initium/inference/cache/prefix_cache.py +59 -0
  37. initium/inference/cache/result_cache.py +57 -0
  38. initium/inference/cache/state_utils.py +74 -0
  39. initium/inference/cache/stores.py +167 -0
  40. initium/inference/capabilities.py +86 -0
  41. initium/inference/checkpoint_io.py +63 -0
  42. initium/inference/engine.py +88 -0
  43. initium/inference/inference_config.py +22 -0
  44. initium/inference/metrics.py +103 -0
  45. initium/inference/model_loader.py +270 -0
  46. initium/inference/run_inference.py +526 -0
  47. initium/inference/run_logging.py +173 -0
  48. initium/inference/tasks/__init__.py +1 -0
  49. initium/inference/tasks/audio_task.py +114 -0
  50. initium/inference/tasks/base_task.py +60 -0
  51. initium/inference/tasks/graph_traversal_task.py +235 -0
  52. initium/inference/tasks/multimodal_task.py +74 -0
  53. initium/inference/tasks/task_registry.py +68 -0
  54. initium/inference/tasks/text_task.py +114 -0
  55. initium/inference/tasks/video_task.py +114 -0
  56. initium/mamba_controller/mamba2_controller.py +433 -0
  57. initium/mamba_controller/mamba3_controller.py +242 -0
  58. initium/mamba_controller/mamba_backbone_parallel.py +291 -0
  59. initium/mamba_controller/mamba_controller.py +983 -0
  60. initium/mamba_controller/split_graph_dnc.py +623 -0
  61. initium/memory_manipulation/dynamic_memory_resize.py +157 -0
  62. initium/memory_manipulation/dynamic_n_controller.py +152 -0
  63. initium/memory_manipulation/link_matrix_ablation.py +130 -0
  64. initium/memory_manipulation/nvrtc_compat.py +143 -0
  65. initium/memory_manipulation/stochastic_write_head_v2.py +545 -0
  66. initium/setup_wheels.py +175 -0
  67. initium_deeplearning-0.1.0.dist-info/METADATA +253 -0
  68. initium_deeplearning-0.1.0.dist-info/RECORD +71 -0
  69. initium_deeplearning-0.1.0.dist-info/WHEEL +4 -0
  70. initium_deeplearning-0.1.0.dist-info/entry_points.txt +3 -0
  71. initium_deeplearning-0.1.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,545 @@
1
+ """
2
+ file: stochastic_write_head_v2.py
3
+ Phase 1 patch: stochastic (Gaussian) write vector for pytorch-dnc.
4
+
5
+ Design, per Phase 1 instructions:
6
+ - Do NOT touch the controller, the write mechanism's addressing
7
+ (content/allocation-based), or any other component.
8
+ - The write head emits distribution parameters (mu_t, sigma_t) of a
9
+ diagonal Gaussian q(v_t) = N(mu_t, diag(sigma_t^2)) instead of a
10
+ deterministic write vector.
11
+ - The value actually written to memory is sampled via the
12
+ reparameterization trick: v_t = mu_t + sigma_t * eps, eps ~ N(0, I).
13
+ - L_KL(t) = KL(q(v_t) || p0) is computed in closed form, using ONLY this
14
+ timestep's write-head output (no scan of the memory matrix, no other
15
+ timestep, no other component) -> unambiguously local.
16
+ - Prior p0 = N(0, I), fixed, non-learned, isotropic, in the memory-row
17
+ (= write-vector) embedding space.
18
+
19
+ How the patch works
20
+ --------------------
21
+ dnc.memory.Memory (independent_linears=True, the pytorch-dnc default) builds
22
+ the write vector as:
23
+
24
+ write_vector = self.write_vector_transform(interface_input) # nn.Linear
25
+
26
+ where `interface_input` is the same per-timestep tensor fed to every other
27
+ head transform (read keys, erase vector, gates, ...). `write_vector_transform`
28
+ is called with a single (BATCH, input_size) tensor and returns a single
29
+ (BATCH, cell_size) tensor -- nothing more.
30
+
31
+ StochasticWriteHead has the exact same call signature: forward(x) -> tensor
32
+ of shape (BATCH, cell_size). We install it by literally replacing the
33
+ `write_vector_transform` attribute on the Memory submodule(s) found on the
34
+ model, after model construction and before optimizer construction. Because
35
+ it's just a submodule swap, DNC's forward pass, `_layer_forward`, and
36
+ `Memory.write()` need zero modification -- they keep calling
37
+ `self.write_vector_transform(x)` exactly as before, unaware that the return
38
+ value is now sampled rather than deterministic.
39
+
40
+ Nothing about content-addressing, allocation, erase vectors, gates, or the
41
+ controller is touched. Only the mapping from interface_input -> write_vector
42
+ changes.
43
+
44
+ --- v2: learned, periodically-snapshotted prior (Phase 2) ------------------
45
+ Phase 1's prior p0 = N(0,I) was fixed and non-stateful, so there was nothing
46
+ to log or checkpoint about it. Phase 2 replaces p0 with p0 = N(mu_g, Sigma_g)
47
+ (diagonal), a global prior fit from the write head's own recent output and
48
+ updated on a slow, periodic schedule -- per the roadmap's Phase 2 design.
49
+ Everything below is additive; nothing about the Phase 1 code path changes
50
+ if the new snapshot-update method is never called (mu_g/logvar_g buffers
51
+ stay at their zero-init, i.e. N(0,I), reproducing Phase 1 bit-for-bit).
52
+
53
+ 1. StochasticWriteHead now owns two extra registered buffers, `prior_mu` and
54
+ `prior_logvar` (shape (cell_size,), zero-initialized). Buffers, not
55
+ Parameters: they must NOT receive a gradient from the per-write KL term
56
+ (that would make the "prior" just another thing being learned by the
57
+ same backward pass the KL is supposed to regularize against, which is
58
+ exactly the abandon-locality-via-continuous-coupling failure mode the
59
+ roadmap's snapshot design exists to avoid). They are written to only by
60
+ `update_prior_snapshot()`, under `torch.no_grad()`, on the training
61
+ script's own periodic cadence -- never by `.backward()`.
62
+ 2. `forward()` now also accumulates a detached copy of every sampled write
63
+ v_t into `self._recent_writes` (only while `self.training and
64
+ self.sample`, matching the existing KL-accumulation guard). This is the
65
+ "recent writes" buffer `update_prior_snapshot()` fits mu_g/Sigma_g from.
66
+ 3. The per-write KL closed form is generalized from KL(q(v_t)||N(0,I)) to
67
+ KL(q(v_t)||N(mu_g,Sigma_g)) -- reduces to the exact Phase 1 formula when
68
+ mu_g=0, Sigma_g=I (the buffers' initial state), so this is a strict
69
+ generalization, not a behavior change, for any run that never calls
70
+ update_prior_snapshot(). The computation still reads only this call's
71
+ (mu_t, logvar_t) plus the current frozen (prior_mu, prior_logvar)
72
+ buffers -- no scan of accumulated writes, no other timestep -- so the
73
+ per-write locality claim is unchanged from Phase 1: the only non-local
74
+ computation is the periodic snapshot update itself, isolated to
75
+ `update_prior_snapshot()`.
76
+ 4. `update_prior_snapshot(step, ...)`: computes mu_g <- mean and Sigma_g
77
+ (diagonal) <- variance of the accumulated `_recent_writes` samples,
78
+ clamps the resulting log-variance to [min_logvar, max_logvar] as a
79
+ numerical floor/ceiling (prevents a silent Sigma_g -> 0 collapse, which
80
+ would not show up in the existing q(v_t) KL-collapse diagnostic at all,
81
+ since that diagnostic only ever looked at q, never at the prior), copies
82
+ the result into the `prior_mu`/`prior_logvar` buffers under
83
+ `torch.no_grad()`, records `last_snapshot_step = step`, clears the
84
+ buffer, and returns a diagnostics dict (mu_g norm, diag(Sigma_g)
85
+ mean/min/max, trace, sample count) for the training script to log.
86
+ 5. `snapshot_diagnostics()`: same summary dict as above, read-only, for
87
+ checkpointing / ad-hoc logging without mutating anything.
88
+ 6. `pop_kl()`'s diagnostics dict now also carries `snapshot_step` (=
89
+ `last_snapshot_step`), so every per-write-KL-window log line can be
90
+ tagged with exactly which frozen (mu_g, Sigma_g) snapshot that window's
91
+ KL was computed against -- the audit trail the roadmap's Phase 2 spec
92
+ calls for.
93
+ 7. `pop_total_kl()` now also returns the (min, across heads -- normally a
94
+ single head) `snapshot_step` in its merged diagnostics.
95
+ 8. New module-level helpers `get_prior_state()` / `load_prior_state()` for
96
+ the training script's checkpoint save/resume path, so `mu_g, Sigma_g`
97
+ (and the step they were last updated) are captured in the checkpoint
98
+ itself rather than living only in the in-memory module buffers -- see
99
+ the training script's v5 header for why this matters.
100
+ """
101
+
102
+ from __future__ import annotations
103
+
104
+ from typing import Any
105
+
106
+ import torch
107
+ import torch.nn as nn
108
+
109
+
110
+ class StochasticWriteHead(nn.Module):
111
+ """Drop-in replacement for `Memory.write_vector_transform`.
112
+
113
+ Emits a sampled write vector v_t = mu_t + sigma_t * eps (reparameterization
114
+ trick) instead of a deterministic vector, and accumulates the closed-form
115
+ per-timestep KL(q(v_t) || p0) -- p0 = N(0,I) in Phase 1, or the
116
+ periodically-snapshotted N(prior_mu, diag(exp(prior_logvar))) once
117
+ `update_prior_snapshot()` has been called at least once (Phase 2) -- so
118
+ the training loop can retrieve it after the sequence forward pass and
119
+ add beta * L_KL to the task loss.
120
+
121
+ Parameters
122
+ ----------
123
+ in_features, out_features:
124
+ Same as the nn.Linear it replaces (in_features = Memory.input_size,
125
+ out_features = Memory.cell_size).
126
+ device:
127
+ Passed through so the new parameters end up on the right device;
128
+ mirrors Memory's own `.to(device)` handling.
129
+ sample:
130
+ If False, forward() always returns mu (no noise added, no KL signal
131
+ needed) -- used for the beta=0 sanity-check run, which should
132
+ recover Phase 0 (deterministic write vector) exactly rather than
133
+ merely "on average" as beta -> 0 pressure removes noise over
134
+ training. mu_transform is initialized from the original Linear's
135
+ weights (see `install_stochastic_write_heads`), so beta=0 with
136
+ sample=False is bit-for-bit Phase 0's write head at step 0, and
137
+ stays deterministic throughout that run.
138
+ """
139
+
140
+ prior_mu: torch.Tensor
141
+ prior_logvar: torch.Tensor
142
+
143
+ def __init__(
144
+ self,
145
+ in_features: int,
146
+ out_features: int,
147
+ device: torch.device | None = None,
148
+ sample: bool = True,
149
+ ):
150
+ super().__init__()
151
+ self.mu_transform = nn.Linear(in_features, out_features)
152
+ self.logvar_transform = nn.Linear(in_features, out_features)
153
+
154
+ torch.nn.init.kaiming_uniform_(self.mu_transform.weight)
155
+ # Zero-init the logvar head so sigma_t starts at 1 everywhere, i.e.
156
+ # q(v_t) starts equal to the prior p0 = N(0,I) -- standard VAE-style
157
+ # init that avoids an immediate large KL spike / early instability.
158
+ nn.init.zeros_(self.logvar_transform.weight)
159
+ nn.init.constant_(self.logvar_transform.bias, -4.0) # sigma ~0.135 at init
160
+
161
+ self.sample = sample
162
+ self._kl_terms: list[torch.Tensor] = [] # per-call (BATCH, cell_size) KL, fp32
163
+ self._clamp_terms: list[torch.Tensor] = [] # per-call bool mask, fp32
164
+
165
+ # v2 (Phase 2): stateful, periodically-snapshotted prior. Buffers
166
+ # (not Parameters) -- see module docstring point 1 for why: they
167
+ # must never receive a gradient from the per-write KL term, only
168
+ # ever be written by update_prior_snapshot() under no_grad().
169
+ # Zero-init reproduces Phase 1's fixed p0 = N(0,I) exactly for any
170
+ # run that never calls update_prior_snapshot().
171
+ self.register_buffer("prior_mu", torch.zeros(out_features))
172
+ self.register_buffer("prior_logvar", torch.zeros(out_features))
173
+ self.last_snapshot_step: int = 0
174
+ self._recent_writes: list[torch.Tensor] = [] # detached v_t samples since last snapshot
175
+
176
+ if device is not None and getattr(device, "type", None) == "cuda":
177
+ self.to(device)
178
+
179
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
180
+ mu = self.mu_transform(x)
181
+ logvar = self.logvar_transform(x)
182
+
183
+ # [NaN-TRACE] Bisects "this head's input was already corrupted"
184
+ # from "this head's own Linear layers are where it starts" --
185
+ # can't tell these apart from outside the module. Silent unless
186
+ # something's actually wrong.
187
+ if self.training and self.sample:
188
+ x_ok = torch.isfinite(x).all()
189
+ mu_ok = torch.isfinite(mu).all()
190
+ logvar_ok = torch.isfinite(logvar).all()
191
+ if not (x_ok and mu_ok and logvar_ok):
192
+ print(
193
+ f"[NaN-TRACE] StochasticWriteHead.forward: "
194
+ f"x_finite={bool(x_ok)} mu_finite={bool(mu_ok)} "
195
+ f"logvar_finite={bool(logvar_ok)} | "
196
+ f"x.abs().max()={x.abs().max().item() if x_ok else float('nan')} "
197
+ f"mu.abs().max()={mu.abs().max().item() if mu_ok else float('nan')} "
198
+ f"logvar.abs().max()={logvar.abs().max().item() if logvar_ok else float('nan')}"
199
+ )
200
+
201
+ if self.sample:
202
+ # Clamp for numerical stability (matters under AMP/fp16 autocast).
203
+ logvar_c = torch.clamp(logvar, min=-10.0, max=10.0)
204
+ if self.training:
205
+ self._clamp_terms.append((logvar != logvar_c).float())
206
+ std = torch.exp(0.5 * logvar_c)
207
+ eps = torch.randn_like(std)
208
+ v = mu + std * eps
209
+ else:
210
+ logvar_c = logvar
211
+ v = mu
212
+
213
+ if self.training and self.sample:
214
+ # Closed-form per-dimension KL(q(v_t)||p0), computed in fp32
215
+ # regardless of the ambient autocast dtype. p0 is either the
216
+ # fixed N(0,I) (Phase 1: prior_mu/prior_logvar buffers still at
217
+ # their zero-init) or the current frozen snapshot
218
+ # N(prior_mu, diag(exp(prior_logvar))) (Phase 2). Either way
219
+ # this uses ONLY this call's (mu, logvar) plus the *frozen*
220
+ # prior buffers -- i.e. only this timestep's write-head output
221
+ # and a constant -- nothing else is read, so the per-write
222
+ # locality claim is identical to Phase 1's.
223
+ # Keep double precision for numerical gradient checks and
224
+ # scientific callers; use fp32 for lower-precision/autocast
225
+ # inputs so the KL remains stable under mixed precision.
226
+ kl_dtype = torch.float64 if mu.dtype == torch.float64 else torch.float32
227
+ mu32 = mu.to(dtype=kl_dtype)
228
+ logvar32 = logvar_c.to(dtype=kl_dtype)
229
+ prior_mu32 = self.prior_mu.to(dtype=kl_dtype)
230
+ prior_logvar32 = self.prior_logvar.to(dtype=kl_dtype)
231
+ # General diagonal-Gaussian KL(N(mu,var) || N(prior_mu,prior_var)):
232
+ # 0.5 * [ prior_logvar - logvar + (exp(logvar) + (mu-prior_mu)^2)/exp(prior_logvar) - 1 ]
233
+ # Reduces exactly to Phase 1's 0.5*(exp(logvar)+mu^2-1-logvar) when
234
+ # prior_mu=0, prior_logvar=0.
235
+ kl_per_dim = 0.5 * (
236
+ prior_logvar32
237
+ - logvar32
238
+ + (logvar32.exp() + (mu32 - prior_mu32).pow(2)) / prior_logvar32.exp()
239
+ - 1.0
240
+ )
241
+ self._kl_terms.append(kl_per_dim)
242
+ # v2: accumulate the actual sampled write for the next periodic
243
+ # prior-snapshot fit. Detached -- this must never carry a graph
244
+ # edge back into this step's backward pass; it is read only by
245
+ # update_prior_snapshot(), itself always called under no_grad().
246
+ self._recent_writes.append(v.detach().float())
247
+
248
+ return v
249
+
250
+ def pop_kl(self, free_bits: float = 0.0) -> tuple[torch.Tensor, dict]:
251
+ """Consume and clear accumulated per-timestep KL terms.
252
+
253
+ Returns (loss, diagnostics):
254
+ loss: scalar tensor, mean over (timesteps * batch) of the
255
+ free-bits-floored, dim-summed per-timestep KL. This is
256
+ L_KL for one sequence forward pass, "summed over dims,
257
+ averaged over timesteps" per the Phase 1 spec.
258
+ diagnostics: dict of detached scalars (pre-free-bits) describing
259
+ the raw KL magnitude distribution this call, for collapse
260
+ monitoring (posterior_collapse if mean/max ~ 0 early and
261
+ stays there). Also carries `snapshot_step` (v2/Phase 2):
262
+ the step at which the (prior_mu, prior_logvar) snapshot
263
+ this KL was computed against was last updated (0 if the
264
+ prior has never been snapshotted, i.e. still N(0,I)).
265
+ """
266
+ if not self._kl_terms:
267
+ zero = torch.zeros((), device=self.mu_transform.weight.device)
268
+ return zero, {
269
+ "kl_mean": 0.0,
270
+ "kl_max": 0.0,
271
+ "kl_min": 0.0,
272
+ "kl_std": 0.0,
273
+ "clamp_frac": 0.0,
274
+ "floor_frac": 0.0,
275
+ "snapshot_step": self.last_snapshot_step,
276
+ }
277
+
278
+ kl_stack = torch.cat(
279
+ [t.reshape(-1, t.shape[-1]) for t in self._kl_terms], dim=0
280
+ ) # (T*B, cell_size), fp32, raw (pre-free-bits)
281
+
282
+ diagnostics = {
283
+ "kl_mean": kl_stack.mean().item(),
284
+ "kl_max": kl_stack.max().item(),
285
+ "kl_min": kl_stack.min().item(),
286
+ "kl_std": kl_stack.std().item(),
287
+ "clamp_frac": torch.cat([t.flatten() for t in self._clamp_terms]).mean().item()
288
+ if self._clamp_terms
289
+ else 0.0,
290
+ "floor_frac": (kl_stack <= free_bits).float().mean().item() if free_bits > 0 else 0.0,
291
+ "snapshot_step": self.last_snapshot_step, # v2: audit tag, see module docstring point 6
292
+ }
293
+
294
+ if free_bits > 0:
295
+ per_step_kl = kl_stack.sum(dim=-1) # (T*B,) — sum over dims, pre-clamp
296
+ free_bits_total = (
297
+ free_bits * kl_stack.shape[-1]
298
+ ) # scale per-dim threshold to the summed budget
299
+ per_step_kl = torch.clamp(per_step_kl, min=free_bits_total)
300
+ loss = per_step_kl.mean()
301
+ else:
302
+ loss = kl_stack.sum(dim=-1).mean()
303
+
304
+ self._kl_terms = []
305
+ self._clamp_terms = []
306
+ return loss, diagnostics
307
+
308
+ def reset_kl(self) -> None:
309
+ self._kl_terms = []
310
+
311
+ # ---- v2 (Phase 2): periodic prior snapshot -----------------------------
312
+ def update_prior_snapshot(
313
+ self,
314
+ step: int,
315
+ min_logvar: float = -6.0,
316
+ max_logvar: float = 6.0,
317
+ eps: float = 1e-6,
318
+ ) -> dict:
319
+ """Refit (prior_mu, prior_logvar) from the writes accumulated in
320
+ `self._recent_writes` since the last call, then clear that buffer.
321
+
322
+ This is the ONE place in this module where the prior's non-locality
323
+ lives: it looks at (up to) every write since the last snapshot, not
324
+ just one timestep's output. It is meant to be called on a slow,
325
+ periodic cadence (every K training steps) by the training script,
326
+ under no_grad (enforced here regardless of caller), and NEVER from
327
+ inside the per-step backward pass -- that separation is what keeps
328
+ the per-write KL term itself local (see forward()).
329
+
330
+ min_logvar/max_logvar: numerical floor/ceiling on the fitted
331
+ log-variance. The floor exists specifically so a collapsing
332
+ Sigma_g -> 0 (a real failure mode a naive fit-from-samples estimator
333
+ can hit, e.g. if the write head briefly degenerates to a near-
334
+ constant output) produces a clamped, loggable, non-degenerate value
335
+ instead of silently propagating a near-zero variance into the next
336
+ K steps' KL denominator. This is the prior-collapse check the
337
+ roadmap flags as invisible to the existing q(v_t) KL diagnostic.
338
+
339
+ Returns a diagnostics dict (see snapshot_diagnostics()); additionally
340
+ includes `n_samples`, the number of write-vector samples the fit was
341
+ computed from (0 if `_recent_writes` was empty, in which case the
342
+ previous snapshot is left unchanged and only `n_samples=0` is
343
+ returned -- this can legitimately happen for e.g. sample=False heads,
344
+ or a snapshot cadence shorter than one training step's writes).
345
+ """
346
+ if not self._recent_writes:
347
+ diag = self.snapshot_diagnostics()
348
+ diag["n_samples"] = 0
349
+ diag["raw_var_mean"] = diag["raw_var_max"] = diag["raw_hi_frac"] = 0.0
350
+ return diag
351
+
352
+ with torch.no_grad():
353
+ stacked = torch.cat(self._recent_writes, dim=0) # (N, cell_size)
354
+ n_samples = stacked.shape[0]
355
+ new_mu = stacked.mean(dim=0)
356
+ new_var = stacked.var(dim=0, unbiased=False).clamp(min=eps)
357
+ raw_var_mean = new_var.mean().item()
358
+ raw_var_max = new_var.max().item()
359
+ raw_hi_frac = (new_var.log() > max_logvar).float().mean().item()
360
+ new_logvar = new_var.log().clamp(min=min_logvar, max=max_logvar)
361
+ self.prior_mu.copy_(new_mu)
362
+ self.prior_logvar.copy_(new_logvar)
363
+
364
+ self.last_snapshot_step = step
365
+ self._recent_writes = []
366
+
367
+ diag = self.snapshot_diagnostics()
368
+ diag["n_samples"] = n_samples
369
+ diag["raw_var_mean"] = raw_var_mean
370
+ diag["raw_var_max"] = raw_var_max
371
+ diag["raw_hi_frac"] = raw_hi_frac
372
+ return diag
373
+
374
+ def snapshot_diagnostics(self) -> dict:
375
+ """Read-only summary of the current (prior_mu, prior_logvar)
376
+ snapshot: ||mu_g||, diag(Sigma_g) mean/min/max, trace(Sigma_g), and
377
+ the step it was last updated. Does not mutate anything, does not
378
+ touch `_recent_writes` -- safe to call from checkpointing or ad-hoc
379
+ logging without interfering with the next scheduled snapshot.
380
+ """
381
+ with torch.no_grad():
382
+ sigma_g = self.prior_logvar.exp()
383
+ return {
384
+ "mu_g_norm": self.prior_mu.norm().item(),
385
+ "sigma_g_mean": sigma_g.mean().item(),
386
+ "sigma_g_min": sigma_g.min().item(),
387
+ "sigma_g_max": sigma_g.max().item(),
388
+ "trace_sigma_g": sigma_g.sum().item(),
389
+ "snapshot_step": self.last_snapshot_step,
390
+ }
391
+
392
+
393
+ def install_stochastic_write_heads(
394
+ model: nn.Module,
395
+ device: torch.device | None = None,
396
+ sample: bool = True,
397
+ ) -> list[StochasticWriteHead]:
398
+ """Replace every Memory.write_vector_transform found on `model` with a
399
+ StochasticWriteHead, initializing mu_transform from the original Linear's
400
+ weights (so, before any training, the sampled mean matches Phase 0's
401
+ deterministic write vector exactly).
402
+
403
+ Call this AFTER model construction (and after model.to(device)) and
404
+ BEFORE constructing the optimizer, so the new parameters are included in
405
+ optimizer.parameters(). (v2 note: the new prior_mu/prior_logvar buffers
406
+ are buffers, not parameters, so this ordering requirement is unaffected
407
+ by them -- they're never in optimizer.parameters() regardless.)
408
+
409
+ Raises if no dnc.memory.Memory submodule is found, or if a Memory module
410
+ doesn't have write_vector_transform (i.e. was built with
411
+ independent_linears=False) -- this patch specifically targets the
412
+ independent_linears=True path, which is pytorch-dnc's default and what
413
+ the training script uses.
414
+ """
415
+ from dnc.memory import Memory # local import: only required when patching
416
+
417
+ heads: list[StochasticWriteHead] = []
418
+ for name, module in model.named_modules():
419
+ if isinstance(module, Memory):
420
+ if not hasattr(module, "write_vector_transform"):
421
+ raise RuntimeError(
422
+ f"Memory submodule '{name}' has no write_vector_transform "
423
+ "(likely built with independent_linears=False). This patch "
424
+ "requires independent_linears=True, the DNC default."
425
+ )
426
+ orig: nn.Linear = module.write_vector_transform
427
+ stochastic = StochasticWriteHead(
428
+ orig.in_features, orig.out_features, device=device, sample=sample
429
+ )
430
+ with torch.no_grad():
431
+ stochastic.mu_transform.weight.copy_(orig.weight)
432
+ stochastic.mu_transform.bias.copy_(orig.bias)
433
+ module.write_vector_transform = stochastic # submodule swap
434
+ heads.append(stochastic)
435
+
436
+ if not heads:
437
+ raise RuntimeError(
438
+ "No dnc.memory.Memory submodule found on model; cannot install "
439
+ "stochastic write head. Is `model` really a dnc.DNC instance?"
440
+ )
441
+ return heads
442
+
443
+
444
+ def pop_total_kl(heads: list[StochasticWriteHead], free_bits: float = 0.0):
445
+ """Sum pop_kl() across all installed heads (normally just one, since
446
+ num_layers=1 / share_memory_between_layers means a single Memory
447
+ instance). Returns (total_loss, merged_diagnostics).
448
+
449
+ v2: merged_diagnostics also carries `snapshot_step` -- the minimum
450
+ across heads (normally there is exactly one head, so this is simply
451
+ that head's last_snapshot_step; the min is a defensive choice for the
452
+ multi-head case so the reported tag never overstates how current every
453
+ head's snapshot is).
454
+ """
455
+ total = None
456
+ merged: dict[str, list[Any]] = {
457
+ "kl_mean": [],
458
+ "kl_max": [],
459
+ "kl_min": [],
460
+ "kl_std": [],
461
+ "clamp_frac": [],
462
+ "floor_frac": [],
463
+ }
464
+ snapshot_steps = []
465
+ for h in heads:
466
+ loss, diag = h.pop_kl(free_bits=free_bits)
467
+ total = loss if total is None else total + loss
468
+ for k in merged:
469
+ merged[k].append(diag[k])
470
+ snapshot_steps.append(diag["snapshot_step"])
471
+ averaged: dict[str, Any] = {k: (sum(v) / len(v) if v else 0.0) for k, v in merged.items()}
472
+ averaged["snapshot_step"] = min(snapshot_steps) if snapshot_steps else 0
473
+ if total is None:
474
+ total = torch.zeros(())
475
+ return total, averaged
476
+
477
+
478
+ # ---- v2 (Phase 2): prior snapshot update / checkpoint helpers --------------
479
+ def update_all_prior_snapshots(
480
+ heads: list[StochasticWriteHead],
481
+ step: int,
482
+ min_logvar: float = -6.0,
483
+ max_logvar: float = 6.0,
484
+ ) -> dict:
485
+ """Call update_prior_snapshot(step, ...) on every head and return a
486
+ single merged diagnostics dict (mean across heads for the summary
487
+ scalars; normally there is exactly one head, so this is just that
488
+ head's own diagnostics). Intended to be called by the training script
489
+ on its PRIOR_SNAPSHOT_EVERY cadence, outside of any per-step backward
490
+ pass.
491
+ """
492
+ per_head = [
493
+ h.update_prior_snapshot(step, min_logvar=min_logvar, max_logvar=max_logvar) for h in heads
494
+ ]
495
+ keys = [
496
+ "mu_g_norm",
497
+ "sigma_g_mean",
498
+ "sigma_g_min",
499
+ "sigma_g_max",
500
+ "trace_sigma_g",
501
+ "raw_var_mean",
502
+ "raw_var_max",
503
+ "raw_hi_frac",
504
+ ]
505
+ merged = {k: sum(d[k] for d in per_head) / len(per_head) for k in keys}
506
+ merged["n_samples"] = sum(d["n_samples"] for d in per_head)
507
+ merged["snapshot_step"] = step
508
+ return merged
509
+
510
+
511
+ def get_prior_state(heads: list[StochasticWriteHead]) -> list[dict]:
512
+ """Snapshot (prior_mu, prior_logvar, last_snapshot_step) for every head,
513
+ as plain CPU tensors/ints, suitable for `torch.save`. This is what lets
514
+ a checkpoint be resumed or re-evaluated against the EXACT prior state it
515
+ was trained/evaluated with, rather than whatever happens to be sitting
516
+ in the (freshly re-initialized, zero) buffers of a newly-constructed
517
+ head -- the same class of bug the write_mode deterministic/sampled
518
+ mismatch check in eval_from_checkpoint.py already exists to catch.
519
+ """
520
+ return [
521
+ {
522
+ "prior_mu": h.prior_mu.detach().cpu().clone(),
523
+ "prior_logvar": h.prior_logvar.detach().cpu().clone(),
524
+ "last_snapshot_step": h.last_snapshot_step,
525
+ }
526
+ for h in heads
527
+ ]
528
+
529
+
530
+ def load_prior_state(heads: list[StochasticWriteHead], state: list[dict]) -> None:
531
+ """Inverse of get_prior_state(): restore (prior_mu, prior_logvar,
532
+ last_snapshot_step) onto each head in-place, in order. Raises if the
533
+ number of heads doesn't match the saved state, since a mismatch here
534
+ means the checkpoint doesn't belong to this model config at all.
535
+ """
536
+ if len(heads) != len(state):
537
+ raise RuntimeError(
538
+ f"load_prior_state: {len(heads)} heads on the model but "
539
+ f"{len(state)} entries in the saved prior state."
540
+ )
541
+ with torch.no_grad():
542
+ for h, s in zip(heads, state):
543
+ h.prior_mu.copy_(s["prior_mu"].to(h.prior_mu.device))
544
+ h.prior_logvar.copy_(s["prior_logvar"].to(h.prior_logvar.device))
545
+ h.last_snapshot_step = s["last_snapshot_step"]