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,90 @@
1
+ """
2
+ file: LNN_controller/hybrid_controller.py -- v1
3
+
4
+ Factory for interleaved, per-timestep controller CHAINS built from any mix of
5
+ controller wrappers that follow the controller-wrapper protocol (see
6
+ cfc_controller.py's docstring). An rnn_type of the form "<kind>+<kind>[+...]",
7
+ kind in {"mamba", "mamba2", "mamba3", "cfc"}, e.g. "mamba+cfc", builds a
8
+ ChainedControllerWrapper: stage 0 maps in_dim -> d_model, every later stage
9
+ maps d_model -> d_model. Each kind uses its own block count (blocks_per_kind)
10
+ and its own hyperparameters (kwargs_per_kind).
11
+
12
+ Adding a new controller kind = one entry in STAGE_KINDS + one branch in
13
+ _make_stage(). Mamba wrappers are imported lazily inside _make_stage() to
14
+ avoid a circular import (mamba_controller.py imports this file).
15
+
16
+ New controller kind: add it to STAGE_KINDS and _make_stage() in hybrid_controller.py, plus one kwargs_per_kind entry in H6.
17
+ New parallel backbone kind: add it to _BACKBONE_KINDS and the loop in build_parallel_backbone().
18
+
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from initium.LNN_controller.cfc_controller import CfCControllerWrapper
24
+ from initium.LNN_controller.chained_controller import ChainedControllerWrapper
25
+
26
+ STAGE_KINDS = ("mamba", "mamba2", "mamba3", "cfc")
27
+
28
+
29
+ def parse_hybrid_spec(spec: str) -> list[str]:
30
+ return [k.strip().lower() for k in spec.split("+")]
31
+
32
+
33
+ def is_hybrid_rnn_type(rnn_type: str) -> bool:
34
+ kinds = parse_hybrid_spec(rnn_type)
35
+ return len(kinds) >= 2 and all(k in STAGE_KINDS for k in kinds)
36
+
37
+
38
+ def _make_stage(kind, in_dim, d_model, num_blocks, kw, device):
39
+ if kind == "cfc":
40
+ return CfCControllerWrapper(
41
+ in_dim=in_dim, d_model=d_model, num_blocks=num_blocks, device=device, **kw
42
+ )
43
+ if kind == "mamba":
44
+ from initium.mamba_controller.mamba_controller import MambaControllerWrapper as W
45
+ elif kind == "mamba2":
46
+ from initium.mamba_controller.mamba2_controller import Mamba2ControllerWrapper as W
47
+ elif kind == "mamba3":
48
+ from initium.mamba_controller.mamba3_controller import Mamba3ControllerWrapper as W
49
+ else:
50
+ raise ValueError(
51
+ f"hybrid_controller: unknown stage kind {kind!r}, expected one of {STAGE_KINDS}"
52
+ )
53
+ return W(in_dim=in_dim, d_model=d_model, num_blocks=num_blocks, device=device, **kw)
54
+
55
+
56
+ def build_hybrid_controller(spec, in_dim, d_model, blocks_per_kind, kwargs_per_kind, device=None):
57
+ kinds = parse_hybrid_spec(spec)
58
+ assert len(kinds) >= 2 and all(k in STAGE_KINDS for k in kinds), f"bad hybrid spec {spec!r}"
59
+ stages, cur = [], in_dim
60
+ for kind in kinds:
61
+ stages.append(
62
+ _make_stage(
63
+ kind, cur, d_model, blocks_per_kind[kind], kwargs_per_kind.get(kind, {}), device
64
+ )
65
+ )
66
+ cur = d_model
67
+ return ChainedControllerWrapper(stages, stage_kinds=kinds)
68
+
69
+
70
+ if (
71
+ __name__ == "__main__"
72
+ ): # smoke test: python -m LNN_controller.hybrid_controller (from project root)
73
+ import torch
74
+
75
+ B, T, in_dim, d = 4, 6, 40, 64
76
+ blocks = {"mamba": 1, "mamba2": 1, "mamba3": 1, "cfc": 1}
77
+ kws = {"cfc": dict(backbone_units=64)}
78
+ for spec in ("cfc+cfc", "mamba+cfc", "mamba2+cfc", "mamba3+cfc"):
79
+ try:
80
+ w = build_hybrid_controller(spec, in_dim, d, blocks, kws)
81
+ except ImportError as e:
82
+ print(f"{spec}: SKIPPED ({e})")
83
+ continue
84
+ hx, loss = w.init_state(B), torch.zeros(())
85
+ for _ in range(T):
86
+ out, hx = w(torch.randn(B, 1, in_dim), hx)
87
+ assert out.shape == (B, 1, d), out.shape
88
+ loss = loss + out.pow(2).mean()
89
+ loss.backward()
90
+ print(f"{spec}: OK (loss {float(loss):.4f})")
initium/MoE/README.md ADDED
@@ -0,0 +1 @@
1
+ MoE was implemented as a future-proof feature, when any of the models needs to be trained with 30B+ parameters, they will help out a lot. With smaller amount of parameters they are essentially redudant or even harmful(because of the overhead) - use them when you want to train a model with 30B+ parameters.
@@ -0,0 +1,552 @@
1
+ """
2
+ file: moe_layer.py
3
+
4
+ Reusable Switch-style (top-1) Mixture-of-Experts feed-forward layer,
5
+ architecture-agnostic controller "add-on" block. Designed per
6
+ Experiment-Roadmap.md, "Alternative Phase 3 - Step 2, Option 4" and the
7
+ corpus's MoE-Mamba-specific and general-MoE failure-mode tables:
8
+
9
+ - EXTERNAL placement only: this module is meant to be interleaved
10
+ BETWEEN controller blocks (Mamba blocks, or eventually Transformer
11
+ blocks), never spliced inside a block's own internal projections.
12
+ MoE-Mamba's own ablation (Dead-End #43) found every internal-placement
13
+ variant underperforms external interleaving -- this module's whole
14
+ API shape (a drop-in residual feed-forward sublayer operating on
15
+ (..., d_model) token representations) exists so it can ONLY be used
16
+ externally, by construction: it has no notion of any specific
17
+ controller's internal gates/projections, so there's nothing to splice
18
+ it into.
19
+ - Switch-style top-1 routing (Fedus, Zoph & Shazeer, "Switch
20
+ Transformers"), not Shazeer et al. 2017's noisy top-k -- MoE-Mamba
21
+ itself builds on Switch's k=1 simplification specifically because it
22
+ reduces router computation, at-least-halves expert capacity, and
23
+ simplifies communication, with no quality loss over top-k>1 in
24
+ Switch's own ablations. Reusing that choice here.
25
+ - Expert-count floor: default num_experts=8, with num_experts<4
26
+ rejected outright in __init__, per Dead-End #45 -- a single-expert
27
+ MoE layer measurably underperforms no-MoE-at-all in MoE-Mamba's own
28
+ ablation.
29
+ - Auxiliary load-balancing loss, Switch's single differentiable term
30
+ (Fedus et al. Eq. 4-6): loss = alpha * N * sum_i f_i * P_i. Switch
31
+ simplified Shazeer et al. 2017's separate importance + load losses
32
+ into this one term; MoE-Mamba follows Switch, so this file does too.
33
+ Default alpha=0.01, Switch's own tuned value (they swept 1e-1..1e-5
34
+ and found 1e-2 balanced load quickly without hurting the primary
35
+ objective).
36
+ - Controller-agnostic call signature: forward(x) where x is
37
+ (..., d_model) -- works whether the caller passes one timestep
38
+ (B, d_model), as mamba_controller.py's MambaControllerWrapper does
39
+ (DNC drives its controller one timestep at a time, interleaved with
40
+ memory read/write), or a whole sequence (B, T, d_model) at once, as a
41
+ future Transformer controller could (it doesn't need DNC's
42
+ per-timestep interleaving the same way). Token routing is computed
43
+ per-token regardless of T, so no controller-specific branching is
44
+ needed inside this module -- this is the concrete mechanism that
45
+ makes Option 4 reusable across controllers, per the roadmap's
46
+ explicit requirement.
47
+
48
+ Sized per the corpus's dexpert scaling note (MoE-Mamba, Section 3.4 /
49
+ Appendix B): to keep active-parameters-per-token roughly comparable to a
50
+ dense feed-forward layer at a given Mamba:MoE active-parameter ratio,
51
+ MoE-Mamba defaults to dexpert = 3 * d_model at their own "3:3" ratio
52
+ (their own reported sweet spot -- Figure 5 -- gains become marginal past
53
+ this ratio and higher ratios are impractical due to routing overhead).
54
+ This file mirrors that default (expert_dim = 3 * d_model) but leaves it
55
+ fully overridable.
56
+
57
+ capacity_factor default is 1.5, not Switch's own 1.0-2.0 range midpoint of
58
+ 1.25: mamba_controller.py's MambaControllerWrapper calls this module once
59
+ PER TIMESTEP (num_tokens == batch_size only, e.g. 16), not once per whole
60
+ padded sequence the way a Transformer FFN pass would. With that few
61
+ tokens per call, a low capacity factor drops a much larger fraction of
62
+ tokens on ordinary jitter than it would at Switch's own token-per-batch
63
+ scale -- 1.5 gives more headroom for that per-call regime while still
64
+ being a real (not effectively-infinite) capacity constraint.
65
+
66
+ Failure mode this file's docstrings exist specifically to flag but cannot
67
+ itself check (see Experiment-Roadmap.md, "Option 4" failure-mode table):
68
+ - ANOM-151 (MoE-Mamba's own unresolved anomaly): added MoE capacity does
69
+ NOT substitute for content-based retrieval/copying capacity -- lower
70
+ perplexity but lower accuracy was observed against dense
71
+ Transformer-MoE in the source paper, with the authors' own untested
72
+ conjecture being that a fixed-size state limits exactly the
73
+ induction-head-style copying capacity content-addressed retrieval
74
+ depends on. This module's diagnostics (below) cover routing/load
75
+ health only. The mandatory check is the CALLER's job: keep running
76
+ the training script's existing memory-dependency ablation
77
+ (evaluate_traversal(..., ablate_memory=True)) and hop-count breakdown
78
+ before/after enabling MoE, and treat a task-loss-only comparison as
79
+ insufficient.
80
+ """
81
+
82
+ from __future__ import annotations
83
+
84
+ from typing import cast
85
+
86
+ import torch
87
+ import torch.nn as nn
88
+ import torch.nn.functional as F
89
+
90
+
91
+ class Expert(nn.Module):
92
+ """One feed-forward expert: Linear -> ReLU -> Linear, matching
93
+ MoE-Mamba's/Switch's own expert architecture (a single hidden layer,
94
+ ReLU-activated) rather than anything more exotic -- keeping this
95
+ minimal is itself corpus-consistent (Dead-End #43: fancier internal
96
+ placements underperformed the plain external design)."""
97
+
98
+ w_in: nn.Linear
99
+ w_out: nn.Linear
100
+
101
+ def __init__(self, d_model: int, expert_dim: int):
102
+ super().__init__()
103
+ self.w_in = nn.Linear(d_model, expert_dim)
104
+ self.w_out = nn.Linear(expert_dim, d_model)
105
+
106
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
107
+ return self.w_out(F.relu(self.w_in(x)))
108
+
109
+
110
+ class SwitchMoE(nn.Module):
111
+ """Switch-style (top-1) sparsely-gated MoE feed-forward sublayer.
112
+ Controller-agnostic: operates on the last dimension of whatever shape
113
+ is passed in (..., d_model), and returns the same shape -- it does NOT
114
+ apply its own residual connection (see MoEBlock below for that), so it
115
+ composes cleanly regardless of what pre-norm/residual convention the
116
+ calling controller block already uses.
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ d_model: int,
122
+ num_experts: int = 8,
123
+ expert_dim: int | None = None,
124
+ capacity_factor: float = 1.5,
125
+ router_noise_eps: float = 1e-2,
126
+ load_balance_alpha: float = 0.01,
127
+ top_k: int = 1,
128
+ ):
129
+ super().__init__()
130
+ if num_experts < 4:
131
+ raise ValueError(
132
+ f"SwitchMoE: num_experts={num_experts} < 4 -- Dead-End #45 "
133
+ "found a single-expert (and, by the same logic, very-few-"
134
+ "expert) MoE layer measurably underperforms no-MoE-at-all. "
135
+ "This floor is enforced here rather than left as a silent "
136
+ "footgun; pass num_experts>=4 (8+ preferred, per the roadmap)."
137
+ )
138
+ self.d_model = d_model
139
+ self.num_experts = num_experts
140
+ expert_dim = expert_dim if expert_dim is not None else 3 * d_model
141
+ self.capacity_factor = capacity_factor
142
+ self.router_noise_eps = router_noise_eps
143
+ self.load_balance_alpha = load_balance_alpha
144
+ if not (1 <= top_k <= num_experts):
145
+ raise ValueError(f"SwitchMoE: top_k={top_k} must be in [1, num_experts={num_experts}]")
146
+ self.top_k = top_k
147
+
148
+ self.router = nn.Linear(d_model, num_experts, bias=False)
149
+ self.experts = nn.ModuleList([Expert(d_model, expert_dim) for _ in range(num_experts)])
150
+
151
+ self._aux_losses: list[torch.Tensor] = []
152
+ self._last_diag: dict = {}
153
+ self._last_topk_idx: torch.Tensor | None = (
154
+ None # (T, k), detached -- for per-source breakdowns
155
+ )
156
+
157
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
158
+ orig_shape = x.shape
159
+ d_model = orig_shape[-1]
160
+ assert d_model == self.d_model, (
161
+ f"SwitchMoE: input last dim {d_model} != configured d_model {self.d_model}"
162
+ )
163
+ flat = x.reshape(-1, d_model) # (num_tokens, d_model)
164
+ num_tokens = flat.shape[0]
165
+
166
+ logits32 = self.router(
167
+ flat
168
+ ).float() # router kept in fp32 (Switch's own selective-precision fix)
169
+ if self.training and self.router_noise_eps > 0:
170
+ # Switch's own exploration mechanism (Appendix C): multiplicative
171
+ # jitter noise. We apply it directly to the router logits here
172
+ # (a commonly-used equivalent of jittering the router input) for
173
+ # simplicity.
174
+ noise = torch.empty_like(logits32).uniform_(
175
+ 1.0 - self.router_noise_eps, 1.0 + self.router_noise_eps
176
+ )
177
+ logits32 = logits32 * noise
178
+ probs = torch.softmax(logits32, dim=-1) # (num_tokens, num_experts)
179
+
180
+ # Top-K routing (Shazeer et al. 2017 / GShard), generalizing Switch's
181
+ # own k=1 special case: each token selects its top_k highest-prob
182
+ # experts, gate weights renormalized to sum to 1 across just those
183
+ # k slots -- top_k=1 reduces to the original Switch gate exactly.
184
+ # Every expert is computed ONCE for the WHOLE token batch (dense,
185
+ # single pair of batched einsums below) regardless of top_k, so the
186
+ # k selected experts for a given token are genuinely active
187
+ # SIMULTANEOUSLY (one fused kernel launch covers all E experts x
188
+ # all T tokens), not run as k sequential passes -- this is what
189
+ # "multiple experts active at once" means at this project's scale.
190
+ top_k = self.top_k
191
+ topk_prob, topk_idx = probs.topk(top_k, dim=-1) # (T, k) each
192
+ gate_weights = topk_prob / topk_prob.sum(dim=-1, keepdim=True).clamp(min=1e-9)
193
+
194
+ # f_i/P_i are cheap to compute regardless of mode; only the aux LOSS
195
+ # accumulation (needed for backward()) is training-gated below.
196
+ # Computing diagnostics unconditionally is what lets inference
197
+ # (model.eval()) report routing health -- previously last_diagnostics()
198
+ # stayed empty forever outside training, which made it impossible to
199
+ # verify MoE routing on real held-out data at inference time.
200
+ one_hot_k = F.one_hot(topk_idx, num_classes=self.num_experts).float() # (T, k, E)
201
+ f_i = one_hot_k.sum(dim=(0, 1)) / max(num_tokens * top_k, 1)
202
+ P_i = probs.mean(dim=0)
203
+
204
+ if self.training:
205
+ aux_loss = self.load_balance_alpha * self.num_experts * (f_i * P_i).sum()
206
+ self._aux_losses.append(aux_loss)
207
+
208
+ with torch.no_grad():
209
+ self._last_diag = {
210
+ "cv_importance": (P_i.std() / P_i.mean().clamp(min=1e-8)).item(),
211
+ "cv_load": (f_i.std() / f_i.mean().clamp(min=1e-8)).item(),
212
+ "max_load_frac": f_i.max().item(),
213
+ }
214
+ self._last_topk_idx = topk_idx.detach()
215
+
216
+ # Expert capacity, scaled by top_k (each slot competes for the same
217
+ # per-expert buffer).
218
+ capacity = max(1, int((num_tokens * top_k / self.num_experts) * self.capacity_factor))
219
+
220
+ w_in = torch.stack(
221
+ [cast(nn.Linear, e.w_in).weight for e in self.experts], dim=0
222
+ ) # (E, expert_dim, d_model)
223
+ b_in = torch.stack([cast(nn.Linear, e.w_in).bias for e in self.experts], dim=0)
224
+ w_out = torch.stack(
225
+ [cast(nn.Linear, e.w_out).weight for e in self.experts], dim=0
226
+ ) # (E, d_model, expert_dim)
227
+ b_out = torch.stack([cast(nn.Linear, e.w_out).bias for e in self.experts], dim=0)
228
+
229
+ # Dense pass over EVERY expert for EVERY token -- the single fused
230
+ # computation that makes the top-k selected experts per token
231
+ # simultaneous rather than sequential.
232
+ hidden = torch.einsum("td,exd->tex", flat, w_in) + b_in # (T, E, expert_dim)
233
+ hidden = F.relu(hidden)
234
+ expert_out_all = torch.einsum("tex,edx->ted", hidden, w_out) + b_out # (T, E, d_model)
235
+
236
+ gathered = torch.gather(
237
+ expert_out_all, 1, topk_idx.unsqueeze(-1).expand(-1, -1, d_model)
238
+ ) # (T, k, d_model)
239
+
240
+ # Capacity mask: keep only the first `capacity` (token,slot) pairs,
241
+ # in original order, routed to each expert -- same drop semantics
242
+ # as the prior top-1 implementation, generalized over the
243
+ # flattened (T*k) slot order.
244
+ flat_idx = topk_idx.reshape(-1)
245
+ one_hot_flat = F.one_hot(flat_idx, num_classes=self.num_experts)
246
+ rank_in_expert = one_hot_flat.cumsum(dim=0).gather(1, flat_idx.unsqueeze(1)).squeeze(1) - 1
247
+ keep = (rank_in_expert < capacity).reshape(num_tokens, top_k)
248
+
249
+ gate = (gate_weights * keep.to(gate_weights.dtype)).unsqueeze(-1).to(gathered.dtype)
250
+ output = (gathered * gate).sum(dim=1).to(flat.dtype) # (T, d_model)
251
+
252
+ return output.reshape(orig_shape)
253
+
254
+ def pop_aux_loss(self) -> torch.Tensor:
255
+ """Consume and clear the accumulated load-balancing loss (mirrors
256
+ StochasticWriteHead.pop_kl()'s accumulate-then-pop convention in
257
+ stochastic_write_head_v2.py, so the training loop's pattern for
258
+ combining an auxiliary loss with the task loss stays consistent
259
+ across both mechanisms)."""
260
+ if not self._aux_losses:
261
+ return torch.zeros((), device=self.router.weight.device)
262
+ total = torch.stack(self._aux_losses).sum()
263
+ self._aux_losses = []
264
+ return total
265
+
266
+ def last_diagnostics(self) -> dict:
267
+ """Read-only routing-health diagnostics from the most recent
268
+ training forward() call: CV(Importance), CV(Load) (Shazeer et al.
269
+ 2017 Appendix A's own balance metrics -- lower is better balanced),
270
+ and the single most-loaded expert's fraction of tokens. Does NOT
271
+ include anything about retrieval/copying accuracy -- ANOM-151
272
+ requires the training script's own memory-ablation check for that,
273
+ not a routing-internal metric."""
274
+ return dict(self._last_diag)
275
+
276
+ def last_routing(self) -> torch.Tensor | None:
277
+ """(num_tokens, top_k) expert indices chosen on the most recent
278
+ forward() call, or None before any call. Callers that know the
279
+ token-batch's composition (e.g. MultiSourceMoEBlock, which knows
280
+ which token rows came from which source) slice this to compute
281
+ per-slice routing breakdowns that this pooled-over-all-tokens
282
+ class has no notion of on its own."""
283
+ return self._last_topk_idx
284
+
285
+
286
+ class MoEBlock(nn.Module):
287
+ """Pre-norm residual wrapper around a SwitchMoE sublayer -- the same
288
+ Add->LN->Mixer residual pattern any Transformer/Mamba sub-block uses
289
+ (see mamba_controller.py's MambaControllerBlock for the mixer-side
290
+ version of the identical pattern), so this can be interleaved after
291
+ ANY controller's per-block output, Mamba or (future) Transformer,
292
+ without either controller needing its own bespoke MoE wrapper. This is
293
+ the concrete reuse point: a future Transformer controller instantiates
294
+ this exact class the same way mamba_controller.py does.
295
+ """
296
+
297
+ def __init__(
298
+ self,
299
+ d_model: int,
300
+ num_experts: int = 8,
301
+ expert_dim: int | None = None,
302
+ capacity_factor: float = 1.5,
303
+ router_noise_eps: float = 1e-2,
304
+ load_balance_alpha: float = 0.01,
305
+ top_k: int = 1,
306
+ device: torch.device | None = None,
307
+ dtype: torch.dtype | None = None,
308
+ ):
309
+ super().__init__()
310
+ self.norm = nn.LayerNorm(d_model, device=device, dtype=dtype)
311
+ self.moe = SwitchMoE(
312
+ d_model,
313
+ num_experts=num_experts,
314
+ expert_dim=expert_dim,
315
+ capacity_factor=capacity_factor,
316
+ router_noise_eps=router_noise_eps,
317
+ load_balance_alpha=load_balance_alpha,
318
+ top_k=top_k,
319
+ )
320
+ self._cumulative_source_counts: list[torch.Tensor] | None = (
321
+ None # per-source, (num_experts,)
322
+ )
323
+ if device is not None and getattr(device, "type", None) == "cuda":
324
+ self.to(device)
325
+
326
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
327
+ return x + self.moe(self.norm(x))
328
+
329
+ def pop_aux_loss(self) -> torch.Tensor:
330
+ return self.moe.pop_aux_loss()
331
+
332
+ def last_diagnostics(self) -> dict:
333
+ return self.moe.last_diagnostics()
334
+
335
+
336
+ class SourceEmbedding(nn.Module):
337
+ """Learned per-source bias added to a token's representation before
338
+ routing, so a Top-K router can specialize by SOURCE identity (e.g.
339
+ "parallel backbone output" vs "previous memory read vector" in
340
+ SplitGraphDNC's controller combiner) in addition to content. One
341
+ embedding row per declared source; ADDED, not concatenated, so it never
342
+ changes d_model. Zero-init: routing starts purely content-based and the
343
+ model has to learn any source specialization, matching this project's
344
+ "start equal to the simpler baseline" convention (see
345
+ stochastic_write_head_v2.py's zero-init logvar head)."""
346
+
347
+ def __init__(self, num_sources: int, d_model: int, device=None, dtype=None):
348
+ super().__init__()
349
+ self.embedding = nn.Embedding(num_sources, d_model, device=device, dtype=dtype)
350
+ nn.init.zeros_(self.embedding.weight)
351
+
352
+ def forward(self, x: torch.Tensor, source_id: int) -> torch.Tensor:
353
+ idx = torch.full((x.shape[0],), source_id, dtype=torch.long, device=x.device)
354
+ return x + self.embedding(idx).to(x.dtype)
355
+
356
+
357
+ class MultiSourceMoEBlock(nn.Module):
358
+ """CfC-oriented MoE sublayer: accepts a VARIABLE-length list of
359
+ per-source token tensors -- each (B, d_model) -- and returns the same
360
+ number of outputs, one per source, each individually routed and gated
361
+ through ONE shared bank of Top-K experts (SwitchMoE.forward -- every
362
+ expert still runs exactly once per call, dense and simultaneous).
363
+
364
+ This is what lets a CfC controller/combiner keep its "many inputs in,
365
+ many outputs out, with per-input specialization" property once MoE
366
+ sits in front of/inside it: the router sees BOTH a token's content AND
367
+ a learned source embedding, so it can genuinely learn "route source A's
368
+ tokens toward experts {2,5}, source B's toward {1,7}" -- a per-source
369
+ specialization on top of ordinary within-source content routing -- the
370
+ concrete mechanism behind "an internal router analyzes the type of
371
+ input and allocates it to specific experts."
372
+
373
+ All sources are concatenated into one (sum(B_i), d_model) token batch
374
+ and pushed through a SINGLE SwitchMoE call, so adding sources costs
375
+ router/gather overhead only, never extra kernel launches.
376
+ """
377
+
378
+ _cumulative_source_counts: list[torch.Tensor] | None
379
+
380
+ def __init__(
381
+ self,
382
+ d_model: int,
383
+ num_sources: int,
384
+ num_experts: int = 8,
385
+ expert_dim: int | None = None,
386
+ top_k: int = 1,
387
+ capacity_factor: float = 1.5,
388
+ router_noise_eps: float = 1e-2,
389
+ load_balance_alpha: float = 0.01,
390
+ device=None,
391
+ dtype=None,
392
+ ):
393
+ super().__init__()
394
+ if num_sources < 1:
395
+ raise ValueError(f"MultiSourceMoEBlock: num_sources must be >= 1, got {num_sources}")
396
+ self.num_sources = num_sources
397
+ self.norm = nn.LayerNorm(d_model, device=device, dtype=dtype)
398
+ self.source_embed = SourceEmbedding(num_sources, d_model, device=device, dtype=dtype)
399
+ self.moe = SwitchMoE(
400
+ d_model,
401
+ num_experts=num_experts,
402
+ expert_dim=expert_dim,
403
+ top_k=top_k,
404
+ capacity_factor=capacity_factor,
405
+ router_noise_eps=router_noise_eps,
406
+ load_balance_alpha=load_balance_alpha,
407
+ )
408
+ self._cumulative_source_counts = None
409
+ if device is not None and getattr(device, "type", None) == "cuda":
410
+ self.to(device)
411
+
412
+ def forward(self, sources: list[torch.Tensor]) -> list[torch.Tensor]:
413
+ if len(sources) != self.num_sources:
414
+ raise ValueError(
415
+ f"MultiSourceMoEBlock: expected {self.num_sources} source tensors, "
416
+ f"got {len(sources)}"
417
+ )
418
+ batch_sizes = [s.shape[0] for s in sources]
419
+ tagged = torch.cat(
420
+ [self.source_embed(self.norm(s), i) for i, s in enumerate(sources)], dim=0
421
+ )
422
+ routed = self.moe(tagged)
423
+
424
+ # Per-source routing breakdown: which experts did THIS source's rows
425
+ # actually go to, independent of the pooled cv_load/cv_importance
426
+ # SwitchMoE.last_diagnostics() reports over the whole concatenated
427
+ # batch. This is the direct evidence for (or against) genuine
428
+ # per-source specialization -- e.g. "source 0 (backbone output)
429
+ # concentrates on experts {2,5}, source 1 (read vector) on {1,7}"
430
+ # -- rather than every source routing near-identically by chance.
431
+ topk_idx = self.moe.last_routing() # (sum(batch_sizes), top_k) or None
432
+ if topk_idx is not None:
433
+ num_experts = self.moe.num_experts
434
+ if self._cumulative_source_counts is None:
435
+ self._cumulative_source_counts = [torch.zeros(num_experts) for _ in batch_sizes]
436
+ source_diag, offset = [], 0
437
+ for i, b in enumerate(batch_sizes):
438
+ idx_slice = topk_idx[offset : offset + b].reshape(-1).cpu()
439
+ counts = torch.bincount(idx_slice, minlength=num_experts).float()
440
+ self._cumulative_source_counts[i] += counts # never reset -- spans the whole run
441
+ cum = self._cumulative_source_counts[i]
442
+ frac = (cum / cum.sum().clamp(min=1)).tolist()
443
+ top_expert = int(cum.argmax().item())
444
+ source_diag.append(
445
+ {
446
+ "expert_frac": frac,
447
+ "top_expert": top_expert,
448
+ "top_expert_frac": frac[top_expert],
449
+ "n_calls_accumulated": int(cum.sum().item()),
450
+ }
451
+ )
452
+ offset += b
453
+ self._last_source_diag = source_diag
454
+
455
+ outs, offset = [], 0
456
+ for i, b in enumerate(batch_sizes):
457
+ outs.append(sources[i] + routed[offset : offset + b]) # per-source residual
458
+ offset += b
459
+ return outs
460
+
461
+ def pop_aux_loss(self) -> torch.Tensor:
462
+ return self.moe.pop_aux_loss()
463
+
464
+ def last_diagnostics(self) -> dict:
465
+ return self.moe.last_diagnostics()
466
+
467
+ def last_source_diagnostics(self) -> list[dict] | None:
468
+ """Per-source expert-usage breakdown from the most recent forward()
469
+ call: one dict per source (same order as the `sources` list passed
470
+ in), each with `expert_frac` (this source's rows' distribution over
471
+ experts), `top_expert`, and `top_expert_frac`. None before any call.
472
+ Comparing this ACROSS sources is the concrete test of whether the
473
+ router is genuinely specializing by input identity rather than
474
+ routing every source near-identically."""
475
+ return getattr(self, "_last_source_diag", None)
476
+
477
+
478
+ class MoERNNWrapper(nn.Module):
479
+ """Minimal external-MoE add-on for the stock nn.LSTM/GRU/RNN controller
480
+ path (dnc.DNC's own rnn_type in {'lstm','gru','rnn'}). The plain
481
+ baseline controller doesn't need per-block/per-source specialization --
482
+ only a single Top-K MoE sublayer applied to its own per-step output --
483
+ so this stays deliberately simpler than the Mamba/CfC wrappers'
484
+ external-per-block interleaving.
485
+
486
+ Preserves the exact `module(x.unsqueeze(1), hx) -> (out.unsqueeze(1),
487
+ new_hx)` calling convention pytorch-dnc's `DNC._layer_forward` expects,
488
+ so it substitutes for `self.rnns[layer]` with zero changes anywhere
489
+ else in dnc.DNC's forward path.
490
+ """
491
+
492
+ def __init__(
493
+ self,
494
+ rnn: nn.Module,
495
+ d_model: int,
496
+ num_experts: int = 8,
497
+ expert_dim: int | None = None,
498
+ top_k: int = 1,
499
+ capacity_factor: float = 1.5,
500
+ load_balance_alpha: float = 0.01,
501
+ device=None,
502
+ dtype=None,
503
+ ):
504
+ super().__init__()
505
+ self.rnn = rnn
506
+ self.d_model = d_model
507
+ self.moe_enabled = True
508
+ self.moe_blocks = nn.ModuleList(
509
+ [
510
+ MoEBlock(
511
+ d_model,
512
+ num_experts=num_experts,
513
+ expert_dim=expert_dim,
514
+ top_k=top_k,
515
+ capacity_factor=capacity_factor,
516
+ load_balance_alpha=load_balance_alpha,
517
+ device=device,
518
+ dtype=dtype,
519
+ )
520
+ ]
521
+ )
522
+
523
+ def forward(self, input: torch.Tensor, hx):
524
+ out, new_hx = self.rnn(input, hx)
525
+ out = self.moe_blocks[0](out)
526
+ return out, new_hx
527
+
528
+
529
+ def pop_total_moe_aux_loss(moe_layers: list) -> tuple:
530
+ """Sum pop_aux_loss() across all installed MoEBlock/SwitchMoE layers and
531
+ merge their diagnostics (mean across layers for CV metrics, max for the
532
+ worst-case load fraction), mirroring stochastic_write_head_v2's
533
+ pop_total_kl() pattern exactly, so the training loop combines this
534
+ auxiliary loss the same way it already combines the KL loss."""
535
+ total = None
536
+ cv_imp, cv_load, max_load = [], [], []
537
+ for layer in moe_layers:
538
+ loss = layer.pop_aux_loss()
539
+ total = loss if total is None else total + loss
540
+ diag = layer.last_diagnostics()
541
+ if diag:
542
+ cv_imp.append(diag["cv_importance"])
543
+ cv_load.append(diag["cv_load"])
544
+ max_load.append(diag["max_load_frac"])
545
+ if total is None:
546
+ total = torch.zeros(())
547
+ merged = {
548
+ "moe_cv_importance": sum(cv_imp) / len(cv_imp) if cv_imp else 0.0,
549
+ "moe_cv_load": sum(cv_load) / len(cv_load) if cv_load else 0.0,
550
+ "moe_max_load_frac": max(max_load) if max_load else 0.0,
551
+ }
552
+ return total, merged
initium/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Initium differentiable neural computer research framework."""