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,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.
|
initium/MoE/moe_layer.py
ADDED
|
@@ -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."""
|