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