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,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: dynamic_memory_resize.py
|
|
3
|
+
|
|
4
|
+
Static Option 2: rebuild a dnc.DNC's Memory submodule at a new nr_cells,
|
|
5
|
+
transplanting every N-independent learned sublayer so no trained weight is
|
|
6
|
+
lost. Safe because Memory's per-episode state (memory/link_matrix/
|
|
7
|
+
precedence/usage_vector/read_weights/write_weights) is rebuilt from scratch
|
|
8
|
+
by Memory.reset() every forward call regardless -- there is no cross-episode
|
|
9
|
+
memory *content* to preserve, only trained *weights*, and none of those are
|
|
10
|
+
sized by nr_cells.
|
|
11
|
+
|
|
12
|
+
Also transplants Option 1's link-matrix patch (class + mode) onto the
|
|
13
|
+
rebuilt Memory when the old one was patched -- see the isinstance check
|
|
14
|
+
below and link_matrix_ablation.py's module docstring for why this is
|
|
15
|
+
needed: without it, a resize event mid-run would silently revert an active
|
|
16
|
+
link-matrix ablation/sparsification back to dense behavior.
|
|
17
|
+
|
|
18
|
+
--- optimizer resync (added for Dynamic-N) --------------------------------
|
|
19
|
+
`model.memories[layer] = new_memory` swaps the module tree, but the
|
|
20
|
+
training-loop optimizer (`torch.optim.Adam(list(rnn.parameters()) + ...)`)
|
|
21
|
+
was built ONCE, before this function was ever called, from the Parameter
|
|
22
|
+
OBJECTS live at that time. load_state_dict() above only copies tensor
|
|
23
|
+
*values* into new_memory's freshly-constructed Parameters -- it never makes
|
|
24
|
+
optimizer.param_groups aware of them. So without the `optimizer=` arg
|
|
25
|
+
below, every Memory sublayer except write_vector_transform (moved over as
|
|
26
|
+
the same object, so already tracked) silently stops receiving Adam updates
|
|
27
|
+
for the rest of the run after ANY live mid-run call to this function:
|
|
28
|
+
gradients still compute (they're in the forward graph), but
|
|
29
|
+
optimizer.step() only updates params actually present in a param_group,
|
|
30
|
+
and the newly-constructed ones never are.
|
|
31
|
+
|
|
32
|
+
This was already true for static Option 2's curriculum-indexed resize
|
|
33
|
+
(TraversalCurriculum.maybe_advance() calls this function without
|
|
34
|
+
`optimizer=`, ~13 times over a 120k-step run) -- it just wasn't obvious in
|
|
35
|
+
aggregate metrics from a handful of one-time freezes near a lesson
|
|
36
|
+
boundary. It's NOT a problem on a checkpoint *resume*, because
|
|
37
|
+
`optimizer.load_state_dict(ckpt["optimizer_state_dict"])` is called right
|
|
38
|
+
after resize_memory() there and rebuilds `state` keyed to the CURRENT
|
|
39
|
+
param objects, matched positionally against the checkpoint's saved
|
|
40
|
+
param_groups (same architecture => same order => this just works). It's
|
|
41
|
+
specifically live, same-process, mid-run resize calls that orphan params --
|
|
42
|
+
which is exactly what Dynamic-N's whole premise is: resizing far more
|
|
43
|
+
often, live, mid-run. So this fix ships as part of Dynamic-N; the existing
|
|
44
|
+
static-Option-2 call site can opt in by passing `optimizer=optimizer` too
|
|
45
|
+
(not changed automatically here, since that changes reproducibility of
|
|
46
|
+
already-running static-Option-2 experiments).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import torch
|
|
50
|
+
import torch.nn as nn
|
|
51
|
+
from dnc.memory import Memory
|
|
52
|
+
|
|
53
|
+
from initium.memory_manipulation.link_matrix_ablation import AblatableSparseLinkMemory
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _resync_optimizer_after_resize(optimizer, old_named_params: dict, new_memory) -> None:
|
|
57
|
+
"""Repoint optimizer.param_groups at new_memory's Parameter objects
|
|
58
|
+
wherever a same-named old parameter existed, and migrate that
|
|
59
|
+
parameter's Adam state (exp_avg, exp_avg_sq, step) onto the new object
|
|
60
|
+
so momentum isn't reset by a resize. Safe to do unconditionally here
|
|
61
|
+
because no Memory Linear sublayer's shape depends on nr_cells (see
|
|
62
|
+
module docstring) -- every migrated tensor is the same shape before and
|
|
63
|
+
after.
|
|
64
|
+
|
|
65
|
+
write_vector_transform is a no-op under this function: `new_p is
|
|
66
|
+
old_p` for its parameters (the module object itself was moved, not
|
|
67
|
+
rebuilt -- see resize_memory() below), so it's skipped by the `is not`
|
|
68
|
+
check and stays exactly as-is in both param_groups and optimizer.state.
|
|
69
|
+
"""
|
|
70
|
+
new_named_params = dict(new_memory.named_parameters())
|
|
71
|
+
replace_map = {} # old Parameter object -> new Parameter object
|
|
72
|
+
for name, old_p in old_named_params.items():
|
|
73
|
+
new_p = new_named_params.get(name)
|
|
74
|
+
if new_p is not None and new_p is not old_p:
|
|
75
|
+
replace_map[old_p] = new_p
|
|
76
|
+
|
|
77
|
+
for old_p, new_p in replace_map.items():
|
|
78
|
+
if old_p in optimizer.state:
|
|
79
|
+
optimizer.state[new_p] = optimizer.state.pop(old_p)
|
|
80
|
+
|
|
81
|
+
replace_ids = {id(old_p): new_p for old_p, new_p in replace_map.items()}
|
|
82
|
+
for group in optimizer.param_groups:
|
|
83
|
+
group["params"] = [replace_ids.get(id(p), p) for p in group["params"]]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resize_memory(model, new_nr_cells: int, device=None, layer: int = 0, optimizer=None):
|
|
87
|
+
if device is not None:
|
|
88
|
+
device = torch.device(device)
|
|
89
|
+
old_memory: Memory = model.memories[layer] # VERIFY attribute name
|
|
90
|
+
if old_memory.nr_cells == new_nr_cells: # VERIFY kwarg/attr name
|
|
91
|
+
return old_memory
|
|
92
|
+
|
|
93
|
+
# Captured before write_vector_transform is swapped to a placeholder
|
|
94
|
+
# below, so this reflects old_memory's real (pre-mutation) structure --
|
|
95
|
+
# used by the optimizer resync at the very end of this function.
|
|
96
|
+
old_named_params = dict(old_memory.named_parameters())
|
|
97
|
+
|
|
98
|
+
new_memory = Memory(
|
|
99
|
+
input_size=old_memory.input_size,
|
|
100
|
+
nr_cells=new_nr_cells, # VERIFY kwarg name
|
|
101
|
+
cell_size=old_memory.cell_size,
|
|
102
|
+
read_heads=old_memory.read_heads,
|
|
103
|
+
independent_linears=True,
|
|
104
|
+
device=device, # VERIFY kwarg name -- see note below
|
|
105
|
+
).to(device)
|
|
106
|
+
|
|
107
|
+
# Transplant every N-independent learned sublayer except
|
|
108
|
+
# write_vector_transform (that one is a StochasticWriteHead, not a
|
|
109
|
+
# plain Linear, on old_memory -- move the module object itself instead
|
|
110
|
+
# of trying to load_state_dict it).
|
|
111
|
+
old_write_head = old_memory.write_vector_transform
|
|
112
|
+
old_memory.write_vector_transform = nn.Identity() # placeholder so it's excluded below
|
|
113
|
+
new_memory.load_state_dict(old_memory.state_dict(), strict=False)
|
|
114
|
+
new_memory.write_vector_transform = old_write_head # move the actual module
|
|
115
|
+
|
|
116
|
+
# Static Option 1 interaction: if old_memory was patched by
|
|
117
|
+
# link_matrix_ablation.patch_link_matrix (its __class__ reassigned to
|
|
118
|
+
# AblatableSparseLinkMemory, carrying link_matrix_mode / optionally
|
|
119
|
+
# link_matrix_topk as plain instance attributes), new_memory above is a
|
|
120
|
+
# *different* freshly-constructed plain dnc.memory.Memory object --
|
|
121
|
+
# load_state_dict only copies tensor weights, never the Python class or
|
|
122
|
+
# plain instance attributes, so the patch would silently be dropped
|
|
123
|
+
# right here, reverting new_memory to baseline dense link-matrix
|
|
124
|
+
# behavior with no error or warning. Re-apply it explicitly so a resize
|
|
125
|
+
# event (Option 2) can never silently undo an active link-matrix
|
|
126
|
+
# ablation/sparsification (Option 1) mid-run.
|
|
127
|
+
if isinstance(old_memory, AblatableSparseLinkMemory):
|
|
128
|
+
new_memory.__class__ = AblatableSparseLinkMemory
|
|
129
|
+
new_memory.link_matrix_mode = old_memory.link_matrix_mode
|
|
130
|
+
if hasattr(old_memory, "link_matrix_topk"):
|
|
131
|
+
new_memory.link_matrix_topk = old_memory.link_matrix_topk
|
|
132
|
+
|
|
133
|
+
model.memories[layer] = new_memory
|
|
134
|
+
# model.memories is a plain python list, not an nn.ModuleList -- the
|
|
135
|
+
# line above rebinds the list slot but never touches whatever attribute
|
|
136
|
+
# PyTorch's own submodule registry (_modules) actually points at
|
|
137
|
+
# (rnn_layer_memory_shared, or rnn_layer_memory_<layer> when memories
|
|
138
|
+
# aren't shared -- set once via setattr() at model construction, see
|
|
139
|
+
# mamba_controller.py / dnc.DNC.__init__). Without this, that attribute
|
|
140
|
+
# stays pointed at old_memory forever -- the same object whose
|
|
141
|
+
# write_vector_transform was just swapped to nn.Identity() above as a
|
|
142
|
+
# transplant placeholder -- so rnn.state_dict()/rnn.parameters()/
|
|
143
|
+
# rnn.load_state_dict() silently keep walking a stale, Identity-headed
|
|
144
|
+
# module while forward() (which reads self.memories[layer] directly)
|
|
145
|
+
# correctly uses new_memory. Found via a resume-time crash: dynamic-N's
|
|
146
|
+
# floor-start construction forces a resize on resume that a static-only
|
|
147
|
+
# workflow never used to trigger, which is what surfaced this -- but
|
|
148
|
+
# the same corruption happens on any LIVE resize, including every
|
|
149
|
+
# static-Option-2 lesson advance that ever fired.
|
|
150
|
+
for attr_name, submodule in list(model._modules.items()):
|
|
151
|
+
if submodule is old_memory:
|
|
152
|
+
setattr(model, attr_name, new_memory)
|
|
153
|
+
|
|
154
|
+
if optimizer is not None:
|
|
155
|
+
_resync_optimizer_after_resize(optimizer, old_named_params, new_memory)
|
|
156
|
+
|
|
157
|
+
return new_memory
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: dynamic_n_controller.py
|
|
3
|
+
|
|
4
|
+
Dynamic-N, macro-scale (between episodes): grow nr_cells when the model's
|
|
5
|
+
own usage_vector says it needs more capacity, instead of reading a fixed
|
|
6
|
+
per-lesson lookup table (LESSON_NR_CELLS / static Option 2). Per the
|
|
7
|
+
strategy doc ("Whether Option 1 can be made dynamic the same way"), the
|
|
8
|
+
macro-scale case is mechanically just static Option 2's resize mechanism
|
|
9
|
+
(dynamic_memory_resize.resize_memory) with a different *trigger* -- no new
|
|
10
|
+
tensor-resizing logic is needed here, only the decision of *when* to call
|
|
11
|
+
it. This module owns only that decision.
|
|
12
|
+
|
|
13
|
+
--- Why this is split into record() / should_grow() / mark_grown() --------
|
|
14
|
+
The eventual mid-episode (micro-scale) variant needs the exact same
|
|
15
|
+
"sustained saturation, not a single-step spike, with a cooldown after each
|
|
16
|
+
growth event" decision logic (see the strategy doc's point 1 and point 3),
|
|
17
|
+
just fed from a per-timestep usage reading instead of a per-episode one,
|
|
18
|
+
and executing a live in-place pad instead of resize_memory()'s
|
|
19
|
+
rebuild-and-swap. Keeping the decision core (this class) ignorant of *when*
|
|
20
|
+
it's called and *how* growth is physically applied means that future
|
|
21
|
+
variant only has to replace the call site (what feeds record(), and what
|
|
22
|
+
apply_growth does), not re-derive the EMA/cooldown/ceiling logic:
|
|
23
|
+
|
|
24
|
+
- record(frac_saturated): update the state used by should_grow(). Caller
|
|
25
|
+
decides the cadence (once per episode here; once per timestep for the
|
|
26
|
+
micro-scale variant later).
|
|
27
|
+
- should_grow(): pure decision, no side effects, no knowledge of *how*
|
|
28
|
+
growth is applied. Returns the next nr_cells or None.
|
|
29
|
+
- mark_grown(step, old_n, new_n): call AFTER growth has actually been
|
|
30
|
+
applied (by whatever mechanism), to reset the cooldown/EMA and append
|
|
31
|
+
to growth_history. Decoupled from resize_memory() specifically, so a
|
|
32
|
+
future in-place-pad apply function can call it too.
|
|
33
|
+
|
|
34
|
+
The actual resize call (dynamic_memory_resize.resize_memory) happens at the
|
|
35
|
+
call site in Alter_PHASE3_mamba.py's training loop, not in this file --
|
|
36
|
+
same "this module doesn't do the tensor work" split link_matrix_ablation.py
|
|
37
|
+
uses relative to dynamic_memory_resize.py.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DynamicNController:
|
|
42
|
+
"""Usage-triggered nr_cells growth, macro-scale (between episodes).
|
|
43
|
+
|
|
44
|
+
Trigger: an EMA of "fraction of memory cells with usage_vector above
|
|
45
|
+
usage_high", computed fresh each time record() is called. EMA (not the
|
|
46
|
+
raw per-step value) is the sustaining filter the strategy doc's point 1
|
|
47
|
+
calls for -- a single saturated episode doesn't trigger growth, only a
|
|
48
|
+
persistently saturated one does, which also naturally damps against
|
|
49
|
+
growing too eagerly (the strategy doc's other flagged failure mode:
|
|
50
|
+
re-importing Option 2's original "excess capacity is an active cost"
|
|
51
|
+
problem, Concept 17).
|
|
52
|
+
|
|
53
|
+
Growth: multiplicative (nr_cells *= growth_factor each event, clamped
|
|
54
|
+
to ceiling), matching the strategy doc's own 128->256->512 example.
|
|
55
|
+
Floor is enforced only at construction (the starting nr_cells passed to
|
|
56
|
+
the model) -- this controller only ever grows, never shrinks; shrinking
|
|
57
|
+
an active run is a different, harder problem (would need to reconcile
|
|
58
|
+
with live content) and isn't part of this task.
|
|
59
|
+
|
|
60
|
+
Cooldown: after every growth event, should_grow() returns None for
|
|
61
|
+
cooldown_steps worth of record() calls, regardless of EMA, so the
|
|
62
|
+
controller doesn't fire again while the model's addressing policy is
|
|
63
|
+
still re-settling from the last resize (strategy doc point 3,
|
|
64
|
+
distribution-shift-at-growth risk). The EMA is also reset to 0 on
|
|
65
|
+
growth so the post-growth cooldown window starts from a clean read
|
|
66
|
+
rather than the already-saturated value that just triggered it.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
floor: int,
|
|
72
|
+
ceiling: int,
|
|
73
|
+
growth_factor: float = 2.0,
|
|
74
|
+
trigger_frac: float = 0.75,
|
|
75
|
+
ema_decay: float = 0.98,
|
|
76
|
+
cooldown_steps: int = 2000,
|
|
77
|
+
):
|
|
78
|
+
if floor > ceiling:
|
|
79
|
+
raise ValueError(f"DynamicNController: floor ({floor}) > ceiling ({ceiling})")
|
|
80
|
+
self.floor = floor
|
|
81
|
+
self.ceiling = ceiling
|
|
82
|
+
self.growth_factor = growth_factor
|
|
83
|
+
self.trigger_frac = trigger_frac
|
|
84
|
+
self.ema_decay = ema_decay
|
|
85
|
+
self.cooldown_steps = cooldown_steps
|
|
86
|
+
|
|
87
|
+
self.ema: float = 0.0
|
|
88
|
+
self.cooldown_remaining: int = 0
|
|
89
|
+
# (step, old_n, new_n) for every growth event this run has applied
|
|
90
|
+
# (restored from checkpoint on resume -- see state_dict/load_state_dict).
|
|
91
|
+
self.growth_history: list[tuple[int, int, int]] = []
|
|
92
|
+
|
|
93
|
+
def record(self, frac_saturated: float) -> None:
|
|
94
|
+
"""Update the sustained-saturation EMA. Call once per decision
|
|
95
|
+
cadence (once per episode/training-step for macro-scale) with the
|
|
96
|
+
current episode's fraction of memory cells above the usage-high
|
|
97
|
+
threshold -- see the training-loop call site for how that fraction
|
|
98
|
+
is computed from usage_vector.
|
|
99
|
+
"""
|
|
100
|
+
self.ema = self.ema_decay * self.ema + (1.0 - self.ema_decay) * frac_saturated
|
|
101
|
+
if self.cooldown_remaining > 0:
|
|
102
|
+
self.cooldown_remaining -= 1
|
|
103
|
+
|
|
104
|
+
def should_grow(self, current_nr_cells: int) -> int | None:
|
|
105
|
+
"""Returns the next nr_cells to resize to, or None if no growth
|
|
106
|
+
should happen right now. Pure decision -- does not itself resize
|
|
107
|
+
anything or mutate controller state (mark_grown does that, and only
|
|
108
|
+
after the caller has actually applied the growth).
|
|
109
|
+
"""
|
|
110
|
+
if self.cooldown_remaining > 0:
|
|
111
|
+
return None
|
|
112
|
+
if current_nr_cells >= self.ceiling:
|
|
113
|
+
return None
|
|
114
|
+
if self.ema < self.trigger_frac:
|
|
115
|
+
return None
|
|
116
|
+
proposed = int(min(current_nr_cells * self.growth_factor, self.ceiling))
|
|
117
|
+
if proposed <= current_nr_cells:
|
|
118
|
+
return None
|
|
119
|
+
return proposed
|
|
120
|
+
|
|
121
|
+
def mark_grown(self, step: int, old_n: int, new_n: int) -> None:
|
|
122
|
+
"""Call immediately after growth has actually been applied (by
|
|
123
|
+
resize_memory() at the macro-scale call site, or by whatever
|
|
124
|
+
mid-episode mechanism the micro-scale variant eventually uses).
|
|
125
|
+
Resets the EMA and starts the post-growth cooldown.
|
|
126
|
+
"""
|
|
127
|
+
self.growth_history.append((step, old_n, new_n))
|
|
128
|
+
self.ema = 0.0
|
|
129
|
+
self.cooldown_remaining = self.cooldown_steps
|
|
130
|
+
|
|
131
|
+
def state_dict(self) -> dict:
|
|
132
|
+
"""For checkpointing -- see save_checkpoint()'s dynamic_n_state key.
|
|
133
|
+
Deliberately does NOT include floor/ceiling/growth_factor/
|
|
134
|
+
trigger_frac/ema_decay/cooldown_steps: those are run configuration
|
|
135
|
+
(re-supplied from CLI args / module constants on resume, same
|
|
136
|
+
convention as link_matrix_mode/topk are re-passed rather than
|
|
137
|
+
read back out of the checkpoint), not run *state*. Current
|
|
138
|
+
nr_cells is also deliberately NOT stored here -- it's already
|
|
139
|
+
self-describing via model_config["nr_cells"] (read live from
|
|
140
|
+
rnn.memories[0].nr_cells), the same single-source-of-truth
|
|
141
|
+
convention static Option 2's checkpoint format already uses.
|
|
142
|
+
"""
|
|
143
|
+
return {
|
|
144
|
+
"ema": self.ema,
|
|
145
|
+
"cooldown_remaining": self.cooldown_remaining,
|
|
146
|
+
"growth_history": list(self.growth_history),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def load_state_dict(self, state: dict) -> None:
|
|
150
|
+
self.ema = state.get("ema", 0.0)
|
|
151
|
+
self.cooldown_remaining = state.get("cooldown_remaining", 0)
|
|
152
|
+
self.growth_history = list(state.get("growth_history", []))
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: link_matrix_ablation.py
|
|
3
|
+
|
|
4
|
+
Static Option 1: ablate or sparsify the DNC's temporal link matrix at fixed
|
|
5
|
+
N=256, per Session 009's synthesis finding that temporal-linkage addressing
|
|
6
|
+
is near-unused in QA-style (single-hop, content-addressed) tasks. Graph
|
|
7
|
+
traversal chains multiple hops per episode, which is exactly the case that
|
|
8
|
+
finding doesn't directly cover -- see the hop-breakdown logging in
|
|
9
|
+
Alter_PHASE3_mamba.py (TraversalCurriculum.maybe_advance, plus the periodic
|
|
10
|
+
and terminal OOD evaluate_traversal calls) for the diagnostic that watches
|
|
11
|
+
for a hop-count-correlated accuracy collapse this synthesis finding
|
|
12
|
+
wouldn't predict.
|
|
13
|
+
|
|
14
|
+
This does NOT touch nr_cells (that's Option 2 / dynamic_memory_resize.py)
|
|
15
|
+
and does NOT resize any tensor -- it only changes what get_link_matrix()
|
|
16
|
+
computes at whatever N the Memory was already built at. Applied by
|
|
17
|
+
reassigning __class__ on an already-constructed dnc.memory.Memory instance
|
|
18
|
+
(see patch_link_matrix() below), not by editing the installed dnc package --
|
|
19
|
+
same external-library-as-black-box precedent as dynamic_memory_resize.py's
|
|
20
|
+
weight-transplant approach, just simpler here since no weights move.
|
|
21
|
+
|
|
22
|
+
INTERACTION WITH OPTION 2: dynamic_memory_resize.resize_memory() builds a
|
|
23
|
+
brand-new plain dnc.memory.Memory instance and swaps it into
|
|
24
|
+
model.memories[layer]. Its own weight-transplant (load_state_dict) has no
|
|
25
|
+
notion of link-matrix mode, so without extra handling a resize event would
|
|
26
|
+
silently drop back to "dense" mid-run. resize_memory() now checks
|
|
27
|
+
isinstance(old_memory, AblatableSparseLinkMemory) and re-applies the class
|
|
28
|
+
and link_matrix_mode/link_matrix_topk onto the freshly-built new_memory --
|
|
29
|
+
see that file. This module doesn't need to do anything for that to work;
|
|
30
|
+
it's noted here so the coupling isn't a surprise when reading either file
|
|
31
|
+
in isolation.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import torch
|
|
35
|
+
from dnc.memory import Memory
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AblatableSparseLinkMemory(Memory):
|
|
39
|
+
"""Drop-in replacement for get_link_matrix() only. Every other Memory
|
|
40
|
+
method (write, read, content_weightings, directional_weightings, ...)
|
|
41
|
+
is inherited unchanged, and this class adds no new nn.Module state --
|
|
42
|
+
so it's safe to attach to an already-constructed Memory instance via
|
|
43
|
+
__class__ reassignment (see patch_link_matrix) instead of
|
|
44
|
+
reconstructing/reloading weights.
|
|
45
|
+
|
|
46
|
+
Mode is read from instance attributes (set by patch_link_matrix, not
|
|
47
|
+
passed as get_link_matrix() args), so this overrides cleanly against
|
|
48
|
+
the existing call signature in Memory.write() -- no caller changes
|
|
49
|
+
needed anywhere else in dnc.py or memory.py.
|
|
50
|
+
|
|
51
|
+
link_matrix_mode:
|
|
52
|
+
"dense" -- unchanged Memory behavior (baseline; same as not
|
|
53
|
+
patching at all).
|
|
54
|
+
"ablated" -- link matrix is never updated; it stays at its initial
|
|
55
|
+
zero value (from Memory.new()/reset()) for the whole
|
|
56
|
+
episode. directional_weightings() (unchanged, inherited)
|
|
57
|
+
then always returns zero forward/backward weightings, so
|
|
58
|
+
read_weightings' content_mode term is the only nonzero
|
|
59
|
+
contribution -- content-based addressing continues to
|
|
60
|
+
work exactly as before, temporal addressing contributes
|
|
61
|
+
nothing. This is the "ablate" half of the roadmap's
|
|
62
|
+
"ablate or aggressively sparsify."
|
|
63
|
+
"sparse_topk" -- link matrix is updated as normal every write() call,
|
|
64
|
+
but immediately after each update, every row keeps only
|
|
65
|
+
its link_matrix_topk largest-magnitude entries and zeros
|
|
66
|
+
the rest. This is the "sparsify" half -- reduces the
|
|
67
|
+
O(N^2) footprint the roadmap's citation flags, while
|
|
68
|
+
still letting the model use *some* temporal signal,
|
|
69
|
+
unlike full ablation. Recomputed fresh every step (not a
|
|
70
|
+
one-time structural prune), so it tracks whichever links
|
|
71
|
+
the model is actually relying on at that point in the
|
|
72
|
+
episode rather than freezing an initial guess.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def get_link_matrix(
|
|
76
|
+
self, link_matrix: torch.Tensor, write_weights: torch.Tensor, precedence: torch.Tensor
|
|
77
|
+
) -> torch.Tensor:
|
|
78
|
+
mode = getattr(self, "link_matrix_mode", "dense")
|
|
79
|
+
|
|
80
|
+
if mode == "dense":
|
|
81
|
+
return super().get_link_matrix(link_matrix, write_weights, precedence)
|
|
82
|
+
|
|
83
|
+
if mode == "ablated":
|
|
84
|
+
return link_matrix # never updated -- stays at reset()'s zero value
|
|
85
|
+
|
|
86
|
+
if mode == "sparse_topk":
|
|
87
|
+
updated = super().get_link_matrix(link_matrix, write_weights, precedence)
|
|
88
|
+
k = getattr(self, "link_matrix_topk", None)
|
|
89
|
+
if k is None or k >= self.nr_cells:
|
|
90
|
+
return updated # no-op if topk isn't set or wouldn't actually sparsify anything
|
|
91
|
+
# updated: (batch, 1, N, N). Keep each row's top-k
|
|
92
|
+
# largest-magnitude entries, zero the rest.
|
|
93
|
+
magnitudes = updated.abs()
|
|
94
|
+
_, topk_idx = magnitudes.topk(k, dim=-1)
|
|
95
|
+
mask = torch.zeros_like(updated)
|
|
96
|
+
mask.scatter_(-1, topk_idx, 1.0)
|
|
97
|
+
return updated * mask
|
|
98
|
+
|
|
99
|
+
raise ValueError(f"Unknown link_matrix_mode: {mode!r}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def patch_link_matrix(model, mode: str, topk: int | None = None, layer: int | None = None):
|
|
103
|
+
"""Apply Static Option 1 to an already-constructed dnc.DNC (or MambaDNC,
|
|
104
|
+
which subclasses DNC and builds model.memories identically -- see
|
|
105
|
+
mamba_controller.py) instance, right after model construction.
|
|
106
|
+
|
|
107
|
+
mode: "dense" | "ablated" | "sparse_topk" -- see AblatableSparseLinkMemory
|
|
108
|
+
docstring. "dense" is accepted (as a no-op) so a run can be flipped
|
|
109
|
+
back to baseline behavior via the same config/CLI path, for the
|
|
110
|
+
roadmap's required baseline-vs-isolated-ablation comparison,
|
|
111
|
+
without touching any other run() plumbing.
|
|
112
|
+
topk: required (and only used) when mode == "sparse_topk".
|
|
113
|
+
layer: which model.memories[i] to patch. None (default) patches every
|
|
114
|
+
entry in model.memories -- correct for share_memory_between_layers
|
|
115
|
+
=True (this project's setup: a single shared Memory, so memories
|
|
116
|
+
has exactly one entry) and still correct if share_memory_between_
|
|
117
|
+
layers is ever False (patches every per-layer Memory identically).
|
|
118
|
+
Pass an int to patch only one layer's Memory when memories are NOT
|
|
119
|
+
shared and you want just one layer's link matrix affected.
|
|
120
|
+
"""
|
|
121
|
+
if mode == "sparse_topk" and topk is None:
|
|
122
|
+
raise ValueError("patch_link_matrix: topk is required when mode='sparse_topk'")
|
|
123
|
+
|
|
124
|
+
targets = model.memories if layer is None else [model.memories[layer]]
|
|
125
|
+
for memory in targets:
|
|
126
|
+
memory.__class__ = AblatableSparseLinkMemory
|
|
127
|
+
memory.link_matrix_mode = mode
|
|
128
|
+
if topk is not None:
|
|
129
|
+
memory.link_matrix_topk = topk
|
|
130
|
+
return model
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: memory_manipulation/nvrtc_compat.py
|
|
3
|
+
|
|
4
|
+
Environment-compatibility shim, NOT a modeling change. Works around a
|
|
5
|
+
CUDA/NVRTC library mismatch observed in this environment ("nvrtc: error:
|
|
6
|
+
failed to open libnvrtc-builtins.so.13.0" - the installed torch build's
|
|
7
|
+
CUDA minor version has no matching nvrtc-builtins shared library present,
|
|
8
|
+
a known failure mode after a partial torch/CUDA reinstall or a fresh
|
|
9
|
+
Colab runtime whose nvidia-cuda-nvrtc-cuXX package doesn't match the
|
|
10
|
+
torch wheel's CUDA build).
|
|
11
|
+
|
|
12
|
+
Root cause: on CUDA, `torch.prod(x, dim=...)` and `torch.cumprod(x,
|
|
13
|
+
dim=...)` (reductions/scans along a non-trivial dim) dispatch through
|
|
14
|
+
PyTorch's Jiterator -- a kernel compiled at PROCESS RUNTIME via NVRTC --
|
|
15
|
+
rather than a statically pre-compiled kernel. Any environment where NVRTC
|
|
16
|
+
can't find its builtins library crashes the FIRST time either op runs,
|
|
17
|
+
regardless of model architecture, controller type, or MoE config.
|
|
18
|
+
|
|
19
|
+
`dnc.memory.Memory` (third-party, installed via pip -- not a project
|
|
20
|
+
file) calls both of these on the hot path of every single write():
|
|
21
|
+
- get_usage_vector: torch.prod(1 - free_gates*read_weights, dim=1) and
|
|
22
|
+
torch.prod(1 - write_weights, dim=1)
|
|
23
|
+
- allocation weighting: torch.cumprod(sorted_usage, dim=1) (Graves et
|
|
24
|
+
al. 2016's free-list product term, `prod_{i<j} u[phi[i]]`)
|
|
25
|
+
|
|
26
|
+
Rather than reimplementing each of dnc.memory's internal methods by hand
|
|
27
|
+
(fragile: depends on exactly matching an undocumented third-party
|
|
28
|
+
formula, and any future call site is invisible until it crashes), this
|
|
29
|
+
module patches `torch.prod` / `torch.cumprod` / `Tensor.prod` /
|
|
30
|
+
`Tensor.cumprod` THEMSELVES. Every call inside dnc/memory.py - known or
|
|
31
|
+
not yet discovered - is transparently redirected, with zero changes
|
|
32
|
+
needed to the third-party package or to any call site in this project.
|
|
33
|
+
|
|
34
|
+
Fix mechanism: for a CUDA tensor with an explicit `dim`, replace the
|
|
35
|
+
product/cumulative-product with the equivalent log-space reduction
|
|
36
|
+
exp(sum(log(clamp(x, min=EPS)), dim)) or exp(cumsum(log(clamp(x)), dim)).
|
|
37
|
+
log, sum/cumsum, and exp are ordinary elementwise/reduction ops with
|
|
38
|
+
statically pre-compiled CUDA kernels for every dtype PyTorch ships - no
|
|
39
|
+
Jiterator involved - so this sidesteps the missing-nvrtc-builtins
|
|
40
|
+
failure without requiring a CUDA toolkit reinstall inside the notebook.
|
|
41
|
+
|
|
42
|
+
Safety net: the log-space substitution is only mathematically valid for
|
|
43
|
+
NON-NEGATIVE inputs (log of a negative number is NaN). Every known call
|
|
44
|
+
site in dnc.memory operates on gates/usages/weights, which are always in
|
|
45
|
+
[0,1] - but to stay correct even for an unknown future call site (some
|
|
46
|
+
other library, or an edit to dnc.memory) with genuinely negative values,
|
|
47
|
+
the patched functions check `torch.isnan(result).any()` after the
|
|
48
|
+
log-space computation and, on a hit, recompute via the ORIGINAL op on a
|
|
49
|
+
CPU copy of the input (never re-invoking the broken CUDA op) before
|
|
50
|
+
moving the result back to the original device. This costs a device sync
|
|
51
|
+
+ small copy only in that (expected-never, for this project) fallback
|
|
52
|
+
path - the common case never sees it.
|
|
53
|
+
|
|
54
|
+
Call patch_prod_jiterator() once, before running any forward pass -
|
|
55
|
+
idempotent, so importing this from multiple entry points (core_training.py,
|
|
56
|
+
run_inference.py) is safe. If the underlying environment issue is fixed
|
|
57
|
+
instead (matching nvidia-cuda-nvrtc-cuXX package reinstalled), this patch
|
|
58
|
+
is a harmless no-op difference in float rounding at the ~1e-6 level, not
|
|
59
|
+
a correctness regression - there is no reason to remove it once applied.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import torch
|
|
65
|
+
|
|
66
|
+
_EPS = 1e-6
|
|
67
|
+
_PATCHED_ATTR = "_nvrtc_compat_patched"
|
|
68
|
+
|
|
69
|
+
_orig_prod = torch.prod
|
|
70
|
+
_orig_cumprod = torch.cumprod
|
|
71
|
+
_orig_tensor_prod = torch.Tensor.prod
|
|
72
|
+
_orig_tensor_cumprod = torch.Tensor.cumprod
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _log_space(input: torch.Tensor, dim, keepdim: bool, cumulative: bool) -> torch.Tensor:
|
|
76
|
+
orig_dtype = input.dtype
|
|
77
|
+
work = input if input.dtype.is_floating_point else input.float()
|
|
78
|
+
logged = torch.log(work.clamp(min=_EPS))
|
|
79
|
+
reduced = (
|
|
80
|
+
torch.cumsum(logged, dim=dim) if cumulative else torch.sum(logged, dim=dim, keepdim=keepdim)
|
|
81
|
+
)
|
|
82
|
+
return torch.exp(reduced).to(orig_dtype)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _patched_prod(input, dim=None, keepdim=False, *, dtype=None):
|
|
86
|
+
if isinstance(input, torch.Tensor) and input.is_cuda and dim is not None:
|
|
87
|
+
result = _log_space(input, dim, keepdim, cumulative=False)
|
|
88
|
+
if torch.isnan(result).any():
|
|
89
|
+
# Negative-valued input -- log-space isn't valid here. Fall back
|
|
90
|
+
# to the ORIGINAL op on CPU (never re-run the broken CUDA path).
|
|
91
|
+
cpu_out = _orig_prod(
|
|
92
|
+
input.detach().cpu(),
|
|
93
|
+
dim,
|
|
94
|
+
keepdim=keepdim,
|
|
95
|
+
**({"dtype": dtype} if dtype is not None else {}),
|
|
96
|
+
)
|
|
97
|
+
return cpu_out.to(input.device)
|
|
98
|
+
return result.to(dtype) if dtype is not None else result
|
|
99
|
+
if dim is None:
|
|
100
|
+
return _orig_prod(input, **({"dtype": dtype} if dtype is not None else {}))
|
|
101
|
+
return _orig_prod(
|
|
102
|
+
input, dim, keepdim=keepdim, **({"dtype": dtype} if dtype is not None else {})
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _patched_cumprod(input, dim, *, dtype=None):
|
|
107
|
+
if isinstance(input, torch.Tensor) and input.is_cuda:
|
|
108
|
+
result = _log_space(input, dim, keepdim=False, cumulative=True)
|
|
109
|
+
if torch.isnan(result).any():
|
|
110
|
+
cpu_out = _orig_cumprod(
|
|
111
|
+
input.detach().cpu(), dim, **({"dtype": dtype} if dtype is not None else {})
|
|
112
|
+
)
|
|
113
|
+
return cpu_out.to(input.device)
|
|
114
|
+
return result.to(dtype) if dtype is not None else result
|
|
115
|
+
return _orig_cumprod(input, dim, **({"dtype": dtype} if dtype is not None else {}))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _patched_tensor_prod(self, dim=None, keepdim=False, *, dtype=None):
|
|
119
|
+
return _patched_prod(self, dim, keepdim, dtype=dtype)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _patched_tensor_cumprod(self, dim, *, dtype=None):
|
|
123
|
+
return _patched_cumprod(self, dim, dtype=dtype)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def patch_prod_jiterator() -> None:
|
|
127
|
+
"""Idempotent: safe to call from every entry point (training, inference,
|
|
128
|
+
a notebook cell re-running setup) without double-patching."""
|
|
129
|
+
if getattr(torch, _PATCHED_ATTR, False):
|
|
130
|
+
return
|
|
131
|
+
torch.prod = _patched_prod
|
|
132
|
+
torch.cumprod = _patched_cumprod
|
|
133
|
+
torch.Tensor.prod = _patched_tensor_prod
|
|
134
|
+
torch.Tensor.cumprod = _patched_tensor_cumprod
|
|
135
|
+
setattr(torch, _PATCHED_ATTR, True)
|
|
136
|
+
print(
|
|
137
|
+
"[nvrtc_compat] patched torch.prod/cumprod (module- and Tensor-level) "
|
|
138
|
+
"to avoid PyTorch's Jiterator on CUDA, working around this "
|
|
139
|
+
"environment's missing/mismatched libnvrtc-builtins. Every "
|
|
140
|
+
"dnc.memory.Memory call site (get_usage_vector, allocation "
|
|
141
|
+
"weighting, and any other prod/cumprod use) is covered "
|
|
142
|
+
"automatically - no per-method patching needed."
|
|
143
|
+
)
|