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.
- initium/LNN_controller/cfc_backbone_parallel.py +265 -0
- initium/LNN_controller/cfc_controller.py +325 -0
- initium/LNN_controller/chained_controller.py +83 -0
- initium/LNN_controller/hybrid_controller.py +90 -0
- initium/MoE/README.md +1 -0
- initium/MoE/moe_layer.py +552 -0
- initium/__init__.py +1 -0
- initium/config/controller_config.py +86 -0
- initium/config/train_config.py +64 -0
- initium/core_training.py +2182 -0
- initium/data/__init__.py +0 -0
- initium/data/audio/__init__.py +0 -0
- initium/data/audio/audio_dataset.py +96 -0
- initium/data/base_dataset.py +75 -0
- initium/data/common/__init__.py +0 -0
- initium/data/common/chain_task.py +542 -0
- initium/data/common/digit_codec.py +85 -0
- initium/data/common/graph_io.py +123 -0
- initium/data/common/real_data.py +747 -0
- initium/data/dataset_registry.py +59 -0
- initium/data/graph_traversal/__init__.py +0 -0
- initium/data/graph_traversal/graph_traversal.py +746 -0
- initium/data/multimodal/__init__.py +0 -0
- initium/data/multimodal/multimodal_dataset.py +401 -0
- initium/data/text/__init__.py +0 -0
- initium/data/text/text_dataset.py +79 -0
- initium/data/video/__init__.py +0 -0
- initium/data/video/video_dataset.py +85 -0
- initium/inference/__init__.py +1 -0
- initium/inference/cache/__init__.py +1 -0
- initium/inference/cache/base_cache.py +112 -0
- initium/inference/cache/cache_config.py +21 -0
- initium/inference/cache/cache_registry.py +99 -0
- initium/inference/cache/cached_engine.py +264 -0
- initium/inference/cache/keys.py +77 -0
- initium/inference/cache/prefix_cache.py +59 -0
- initium/inference/cache/result_cache.py +57 -0
- initium/inference/cache/state_utils.py +74 -0
- initium/inference/cache/stores.py +167 -0
- initium/inference/capabilities.py +86 -0
- initium/inference/checkpoint_io.py +63 -0
- initium/inference/engine.py +88 -0
- initium/inference/inference_config.py +22 -0
- initium/inference/metrics.py +103 -0
- initium/inference/model_loader.py +270 -0
- initium/inference/run_inference.py +526 -0
- initium/inference/run_logging.py +173 -0
- initium/inference/tasks/__init__.py +1 -0
- initium/inference/tasks/audio_task.py +114 -0
- initium/inference/tasks/base_task.py +60 -0
- initium/inference/tasks/graph_traversal_task.py +235 -0
- initium/inference/tasks/multimodal_task.py +74 -0
- initium/inference/tasks/task_registry.py +68 -0
- initium/inference/tasks/text_task.py +114 -0
- initium/inference/tasks/video_task.py +114 -0
- initium/mamba_controller/mamba2_controller.py +433 -0
- initium/mamba_controller/mamba3_controller.py +242 -0
- initium/mamba_controller/mamba_backbone_parallel.py +291 -0
- initium/mamba_controller/mamba_controller.py +983 -0
- initium/mamba_controller/split_graph_dnc.py +623 -0
- initium/memory_manipulation/dynamic_memory_resize.py +157 -0
- initium/memory_manipulation/dynamic_n_controller.py +152 -0
- initium/memory_manipulation/link_matrix_ablation.py +130 -0
- initium/memory_manipulation/nvrtc_compat.py +143 -0
- initium/memory_manipulation/stochastic_write_head_v2.py +545 -0
- initium/setup_wheels.py +175 -0
- initium_deeplearning-0.1.0.dist-info/METADATA +253 -0
- initium_deeplearning-0.1.0.dist-info/RECORD +71 -0
- initium_deeplearning-0.1.0.dist-info/WHEEL +4 -0
- initium_deeplearning-0.1.0.dist-info/entry_points.txt +3 -0
- 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"]
|