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,983 @@
1
+ """
2
+ mamba_controller.py -- v1
3
+
4
+ Alternate Phase 3, Step 1 (see Experiment-Roadmap.md, "Alternative Phase 3 -
5
+ fits better to the programs architecture"): wire a Mamba-1 (selective SSM)
6
+ controller into the pytorch-dnc `DNC` in place of the LSTM controller used
7
+ by Phase 2 (`PHASE_2_graph_traversal_KL_prior.py`), while leaving memory
8
+ addressing (content lookup, allocation, temporal link matrix, read modes)
9
+ and the Phase 1/2 stochastic-write-head KL machinery completely untouched.
10
+
11
+ Library used: `mamba-ssm` (per the roadmap's explicit instruction, Mamba-1
12
+ is tried first, not Mamba-2/S4, because it's the simplest option). This
13
+ file reuses `mamba_ssm.modules.mamba_simple.Mamba` for its parameters and
14
+ initialization (in_proj, conv1d, x_proj, dt_proj, A_log, D, out_proj -- the
15
+ exact S6 parameterization from the Mamba paper, Algorithm 2 / Section 3.6),
16
+ but does NOT use that class's own `.forward()` or `.step()` methods. Why,
17
+ and what this file does instead, is the one piece of non-obvious design in
18
+ here -- see "Why not just call Mamba.step()?" below before editing the
19
+ step() math in MambaControllerCell.
20
+
21
+ --------------------------------------------------------------------------
22
+ Why a separate file (as requested)
23
+ --------------------------------------------------------------------------
24
+ pytorch-dnc's `DNC.__init__` builds `self.rnns[layer]` as a `nn.LSTM` /
25
+ `nn.GRU` / `nn.RNN` directly inline (see `dnc/dnc.py` in
26
+ https://github.com/ixaxaar/pytorch-dnc), and `DNC._init_hidden` has an
27
+ LSTM-specific branch for constructing the initial controller hidden state.
28
+ Neither `rnn_type='mamba'` nor a `nn.Module` with a fundamentally different
29
+ hidden-state shape (Mamba needs a `(conv_state, ssm_state)` pair per block,
30
+ not an `(h, c)` LSTM pair) is supported by the stock class. Rather than
31
+ monkey-patching or forking pytorch-dnc, this file defines:
32
+
33
+ 1. `MambaControllerCell` -- one Mamba-1 block, single-timestep, BPTT-safe.
34
+ 2. `MambaControllerBlock` -- pre-norm residual wrapper around (1), mirroring
35
+ mamba_ssm's own `Block` (Add -> LN -> Mixer).
36
+ 3. `MambaControllerWrapper`-- stacks N `MambaControllerBlock`s and exposes
37
+ the exact calling convention pytorch-dnc's
38
+ `DNC._layer_forward` expects from
39
+ `self.rnns[layer]`: callable as
40
+ `module(x_unsqueezed_at_dim1, hx) -> (out_unsqueezed, new_hx)`,
41
+ the same shape contract as `nn.LSTM(...,
42
+ batch_first=True)` called one timestep at a
43
+ time (`x.unsqueeze(1)`).
44
+ 4. `MambaDNC` -- subclasses `dnc.DNC`. For
45
+ `rnn_type != 'mamba'` it defers to the stock
46
+ `DNC.__init__`/`_init_hidden` untouched (so
47
+ this class is a strict drop-in superset --
48
+ `rnn_type='lstm'` behaves byte-identically
49
+ to plain `dnc.DNC`, which is what makes an
50
+ LSTM-vs-Mamba A/B comparison a one-line
51
+ change in the training script). For
52
+ `rnn_type == 'mamba'` it builds
53
+ `self.rnns[layer]` as a `MambaControllerWrapper`
54
+ instead, and overrides `_init_hidden`'s
55
+ controller-state branch to build
56
+ `(conv_state, ssm_state)` pairs instead of
57
+ `(h, c)`. Memory construction
58
+ (`dnc.memory.Memory`), the output projection,
59
+ and `forward()`/`_layer_forward()` are all
60
+ inherited from `dnc.DNC` UNCHANGED -- this is
61
+ what guarantees addressing stays untouched
62
+ and that `install_stochastic_write_heads()`
63
+ from `stochastic_write_head_v2.py` keeps
64
+ working with zero modification (it only ever
65
+ looks for `dnc.memory.Memory` submodules and
66
+ patches `write_vector_transform`; it never
67
+ looks at the controller).
68
+
69
+ --------------------------------------------------------------------------
70
+ Why not just call Mamba.step()? (the one thing worth reading carefully)
71
+ --------------------------------------------------------------------------
72
+ `mamba_ssm.modules.mamba_simple.Mamba` ships two ways to run the model:
73
+
74
+ - `.forward(hidden_states)`: takes the WHOLE sequence at once (B, L, D)
75
+ and is only fast because it uses `causal_conv1d_fn` / `selective_scan_fn`
76
+ CUDA kernels (or an FFT-free full-sequence scan) that assume every
77
+ timestep's input is already available. This doesn't fit here: DNC has
78
+ to interleave one controller step with one memory read/write at a time
79
+ (the read vector from step t feeds the controller's input at step t+1),
80
+ so the whole point of wiring Mamba in as *DNC's controller* is defeated
81
+ if we can't get the model one token at a time.
82
+
83
+ - `.step(hidden_states, conv_state, ssm_state)`: the autoregressive
84
+ decoding path, exactly one token at a time -- this is the right
85
+ granularity. BUT it is written for inference, and mutates its own state
86
+ tensors IN PLACE via `conv_state.copy_(...)` / `ssm_state.copy_(...)`
87
+ (see mamba_ssm/modules/mamba_simple.py). That's fine under
88
+ `torch.no_grad()` for text generation, but it is NOT safe for
89
+ backprop-through-time: the in-place `.copy_()` overwrites the exact
90
+ tensor version autograd needs to compute the backward pass through the
91
+ *previous* timestep's `ssm_state * dA` multiply. Concretely, PyTorch
92
+ raises:
93
+
94
+ RuntimeError: one of the variables needed for gradient computation
95
+ has been modified by an inplace operation ...
96
+
97
+ the moment `.backward()` is called on a loss that was accumulated over
98
+ more than one `.step()` call chained through the same state tensors --
99
+ which is exactly what a T-step DNC unroll does. (Verified directly: a
100
+ minimal repro of this exact `state.copy_(new_state)`-across-a-python-
101
+ loop pattern throws this RuntimeError on `.backward()`, while the
102
+ identical recurrence written out-of-place -- `state = state * dA + ...`,
103
+ no `.copy_()` -- backpropagates cleanly through an arbitrary number of
104
+ chained steps.)
105
+
106
+ So: `MambaControllerCell.step()` below reimplements the SAME math as
107
+ `Mamba.step()`'s non-fast-path branch (the branch it falls back to when
108
+ `causal_conv1d_update`/`selective_state_update` aren't available -- i.e.
109
+ the portable, dependency-light path, matching the roadmap's "Mamba-1
110
+ first because it's simplest"), line-for-line equivalent, EXCEPT every
111
+ in-place `.copy_()` is replaced with a plain out-of-place tensor
112
+ expression that returns a NEW state tensor each call, exactly the same
113
+ pattern `nn.LSTM` already uses (a step returns new `(h, c)`, it doesn't
114
+ mutate the old `(h, c)` in place). This is what makes it safe to chain
115
+ inside pytorch-dnc's own per-timestep Python loop under full BPTT.
116
+
117
+ We still reuse the actual `mamba_ssm.modules.mamba_simple.Mamba` class
118
+ for parameter construction (`in_proj`, `conv1d`, `x_proj`, `dt_proj`,
119
+ `A_log`, `D`, `out_proj`) and all of its initialization logic (S4D-real
120
+ `A_log` init, the dt-bias inverse-softplus init, etc.) -- we only bypass
121
+ its `forward`/`step` methods, never its `__init__`.
122
+
123
+ --------------------------------------------------------------------------
124
+ Design choices specific to this wiring (Step 1 scope)
125
+ --------------------------------------------------------------------------
126
+ - `d_model` for every Mamba block is fixed to DNC's `hidden_size`
127
+ (`output_size` in pytorch-dnc's naming), since the controller's output
128
+ (post-clip) is fed directly to `Memory.forward()` as the interface vector
129
+ `ξ`, which must have exactly that width. A small `nn.Linear` "in_adapter"
130
+ handles the one dimension mismatch pytorch-dnc's own LSTM absorbs
131
+ implicitly in its first-layer weight matrix: `nn_input_size` (=
132
+ `input_size + read_vectors_size`) -> `hidden_size`, applied once before
133
+ the Mamba block stack. Every subsequent stacked block already operates at
134
+ `hidden_size`, so no further adapter is needed (identical in spirit to
135
+ how `nn.LSTM(num_layers=N)`'s layer-0 weight differs in shape from
136
+ layers 1..N-1).
137
+ - `num_hidden_layers` (pytorch-dnc's existing knob, previously "how many
138
+ stacked LSTM layers form this DNC layer's controller", default 2) is
139
+ reused, unchanged in meaning, as "how many stacked Mamba blocks form this
140
+ DNC layer's controller" -- so Phase 2's existing model-capacity constants
141
+ don't need a new knob to keep meaning "the controller has some depth".
142
+ - `mamba_d_state` / `mamba_d_conv` / `mamba_expand` are new, Mamba-specific
143
+ hyperparameters (defaults 16 / 4 / 2, the Mamba-1 paper's own defaults),
144
+ exposed as constructor kwargs on `MambaDNC` with no analogue in the LSTM
145
+ path -- there's nothing to keep in sync with, since LSTM has no such
146
+ parameters.
147
+ - Initial `(conv_state, ssm_state)` are zero-initialized (matching
148
+ `Mamba.allocate_inference_cache`'s own convention), NOT
149
+ `xavier_uniform_`-initialized the way pytorch-dnc seeds its LSTM `h0`.
150
+ This is a deliberate divergence, not an oversight: `xavier_uniform_` on a
151
+ conv/ssm state tensor has no principled meaning in this parameterization
152
+ (Mamba's own reference implementation always starts both at exactly
153
+ zero), so keeping the library's own convention here is more faithful
154
+ than forcing LSTM's init convention onto a structurally different state.
155
+ - Everything here targets Mamba-1 (`mamba_ssm.modules.mamba_simple.Mamba`)
156
+ specifically, not Mamba-2/S4, per the roadmap's explicit sequencing.
157
+ """
158
+
159
+ from __future__ import annotations
160
+
161
+ from collections.abc import Iterable
162
+ from typing import Any, cast
163
+
164
+ import torch
165
+ import torch.nn as nn
166
+ import torch.nn.functional as F
167
+ from dnc import DNC
168
+ from dnc.memory import Memory
169
+ from dnc.util import cuda
170
+
171
+ from initium.MoE.moe_layer import MoEBlock, MoERNNWrapper
172
+
173
+ _MAMBA_IMPORT_ERROR: str | None
174
+
175
+ try:
176
+ from mamba_ssm.modules.mamba_simple import Mamba
177
+ except ImportError as _mamba_import_error: # pragma: no cover - environment-dependent
178
+ Mamba = None
179
+ _MAMBA_IMPORT_ERROR = (
180
+ "mamba_controller.py requires the `mamba-ssm` package "
181
+ "(`pip install mamba-ssm`, which needs a CUDA build toolchain / "
182
+ "nvcc available at install time -- see mamba-ssm's own install "
183
+ "instructions). Original import error: "
184
+ f"{_mamba_import_error}"
185
+ )
186
+ else:
187
+ _MAMBA_IMPORT_ERROR = None
188
+
189
+
190
+ def _require_mamba_ssm() -> None:
191
+ if Mamba is None:
192
+ raise ImportError(_MAMBA_IMPORT_ERROR)
193
+
194
+
195
+ from initium.LNN_controller.cfc_controller import ( # v13 (CfC / LNN controller)
196
+ CfCControllerWrapper,
197
+ _require_ncps,
198
+ )
199
+ from initium.LNN_controller.hybrid_controller import ( # v14 (Mamba -> CfC chains)
200
+ build_hybrid_controller,
201
+ is_hybrid_rnn_type,
202
+ )
203
+ from initium.mamba_controller.mamba2_controller import ( # v10 (Mamba-2 standalone controller)
204
+ Mamba2ControllerWrapper,
205
+ _require_mamba2_ssm,
206
+ )
207
+ from initium.mamba_controller.mamba3_controller import ( # v12 (Mamba-3 controller)
208
+ Mamba3ControllerWrapper,
209
+ _require_mamba3_ssm,
210
+ )
211
+
212
+
213
+ # ==========================================================================
214
+ # 1. MambaControllerCell -- one Mamba-1 block, single-timestep, BPTT-safe
215
+ # ==========================================================================
216
+ class MambaControllerCell(nn.Module):
217
+ """One Mamba-1 (selective SSM) block, driven one timestep at a time.
218
+
219
+ Reuses `mamba_ssm.modules.mamba_simple.Mamba` purely as a parameter
220
+ container (correct shapes + the library's own initialization), and
221
+ reimplements the single-step S6 recurrence out-of-place so it can be
222
+ backpropagated through an arbitrary number of chained calls -- see the
223
+ module docstring's "Why not just call Mamba.step()?" section for why
224
+ the library's own `.step()` cannot be used here as-is.
225
+
226
+ State: `(conv_state, ssm_state)`, shapes `(B, d_inner, d_conv)` and
227
+ `(B, d_inner, d_state)` respectively -- same shapes as
228
+ `Mamba.allocate_inference_cache()` produces, so this stays a drop-in
229
+ match for the library's own state convention even though the state is
230
+ threaded functionally here rather than mutated in place.
231
+ """
232
+
233
+ def __init__(
234
+ self,
235
+ d_model: int,
236
+ d_state: int = 16,
237
+ d_conv: int = 4,
238
+ expand: int = 2,
239
+ layer_idx: int | None = None,
240
+ device: torch.device | None = None,
241
+ dtype: torch.dtype | None = None,
242
+ ):
243
+ super().__init__()
244
+ _require_mamba_ssm()
245
+
246
+ # use_fast_path=False: purely defensive/documentary. We never call
247
+ # `self.mamba.forward()` or `self.mamba.step()` on this instance --
248
+ # only its submodules (in_proj, conv1d, ...) are used, by `step()`
249
+ # below -- so this flag has no effect on us either way. Set False so
250
+ # nothing accidentally routes through the causal_conv1d/
251
+ # mamba_inner_fn CUDA fast path if some other code ever calls
252
+ # `.forward()` on `self.mamba` directly.
253
+ self.mamba = Mamba(
254
+ d_model=d_model,
255
+ d_state=d_state,
256
+ d_conv=d_conv,
257
+ expand=expand,
258
+ layer_idx=layer_idx,
259
+ use_fast_path=False,
260
+ device=device,
261
+ dtype=dtype,
262
+ )
263
+ self.d_model = d_model
264
+ self.d_inner = self.mamba.d_inner
265
+ self.d_state = self.mamba.d_state
266
+ self.d_conv = self.mamba.d_conv
267
+ self.dt_rank = self.mamba.dt_rank
268
+
269
+ def init_state(
270
+ self, batch_size: int, device: torch.device | None = None, dtype: torch.dtype | None = None
271
+ ) -> tuple[torch.Tensor, torch.Tensor]:
272
+ """Zero-initialized (conv_state, ssm_state), matching
273
+ `Mamba.allocate_inference_cache`'s shapes/convention (see module
274
+ docstring for why this stays zero-init rather than xavier-init)."""
275
+ conv_state = torch.zeros(batch_size, self.d_inner, self.d_conv, device=device, dtype=dtype)
276
+ ssm_state = torch.zeros(batch_size, self.d_inner, self.d_state, device=device, dtype=dtype)
277
+ return conv_state, ssm_state
278
+
279
+ def step(
280
+ self,
281
+ hidden_states: torch.Tensor,
282
+ conv_state: torch.Tensor,
283
+ ssm_state: torch.Tensor,
284
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
285
+ """Single-timestep S6 update. `hidden_states`: (B, d_model).
286
+
287
+ Line-for-line equivalent to `Mamba.step()`'s non-fast-path branch
288
+ (mamba_ssm/modules/mamba_simple.py), except every state update is
289
+ out-of-place (`new_state = ...`, no `.copy_()`), so this function is
290
+ safe to call repeatedly inside a Python loop under full
291
+ backprop-through-time -- see module docstring.
292
+ """
293
+ m = self.mamba
294
+ dtype = hidden_states.dtype
295
+
296
+ # ---- input/gate projection --------------------------------------
297
+ xz = m.in_proj(hidden_states) # (B, 2*d_inner)
298
+ x, z = xz.chunk(2, dim=-1) # (B, d_inner) each
299
+
300
+ # ---- causal depthwise conv, functional (rolling window) ---------
301
+ # conv_state holds the last (d_conv - 1) x's; append the new x and
302
+ # drop the oldest column -- out-of-place shift, mirrors what
303
+ # Mamba.step() does via conv_state.copy_(roll(...)) but without
304
+ # mutating the input tensor.
305
+ new_conv_state = torch.cat([conv_state[:, :, 1:], x.unsqueeze(-1)], dim=-1)
306
+ conv_weight = m.conv1d.weight.squeeze(1) # (d_inner, 1, d_conv) -> (d_inner, d_conv)
307
+ x_conv = torch.sum(new_conv_state * conv_weight, dim=-1) # (B, d_inner)
308
+ if m.conv1d.bias is not None:
309
+ x_conv = x_conv + m.conv1d.bias
310
+ x_conv = m.act(x_conv).to(dtype=dtype)
311
+
312
+ # ---- input-dependent selection parameters (Delta, B, C) ---------
313
+ # FIX (chronic grad_norm nan in Mamba only, never in LSTM runs):
314
+ # x_proj/dt_proj/softplus/out_proj are plain nn.Linear calls, so
315
+ # under ambient autocast they still run (and can overflow) in fp16
316
+ # even though the scan below was already forced to fp32. Because
317
+ # ssm_state/conv_state persist across an entire episode (up to
318
+ # ~250+ timesteps, reset only at episode start), a single fp16
319
+ # overflow anywhere in this chain poisons every later step of that
320
+ # episode -- unlike the LSTM, whose gated hidden state is bounded
321
+ # by construction every step. Fix: run x_proj/dt_proj/softplus and
322
+ # the scan in real fp32, clamp dt's upper bound (its own dynamics
323
+ # only ever need ~0.001-0.1) and A_log's lower bound (a very
324
+ # negative A_log drives A -> 0, turning the SSM into an undamped
325
+ # accumulator), and hard-clamp the recurrent state and y before
326
+ # they're allowed to re-enter out_proj's autocast fp16 matmul.
327
+ with torch.autocast(device_type=hidden_states.device.type, enabled=False):
328
+ x_conv32 = x_conv.float()
329
+ x_db = m.x_proj(x_conv32) # (B, dt_rank + 2*d_state), real fp32
330
+ dt, B, C = torch.split(x_db, [self.dt_rank, self.d_state, self.d_state], dim=-1)
331
+ dt = F.linear(dt, m.dt_proj.weight.float()) # bias added below, inside softplus
332
+ dt = F.softplus(dt + m.dt_proj.bias.float()) # (B, d_inner), real fp32
333
+ dt32 = dt.clamp(min=1e-6, max=100.0) # upper bound: normal dt is 0.001-0.1
334
+ B32 = B.float()
335
+ C32 = C.float()
336
+ A_log_c = m.A_log.float().clamp(
337
+ min=-20.0, max=20.0
338
+ ) # lower bound stops A -> 0 (undamped accumulator)
339
+ A32 = -torch.exp(A_log_c) # (d_inner, d_state)
340
+ dA = torch.exp(torch.einsum("bd,dn->bdn", dt32, A32))
341
+ dB = torch.einsum("bd,bn->bdn", dt32, B32)
342
+ new_ssm_state32 = (
343
+ ssm_state.float() * dA + x_conv32.unsqueeze(-1) * dB
344
+ ) # (B, d_inner, d_state)
345
+ new_ssm_state32 = new_ssm_state32.clamp(
346
+ min=-1e4, max=1e4
347
+ ) # hard stop on undamped growth
348
+ if new_ssm_state32.abs().max() > 9999.0:
349
+ print(
350
+ "[clamp-hit] ssm_state clamped, pre-clamp max would have exceeded bound"
351
+ ) # If you see [clamp-hit] printed and NaNs still occur afterward in the same run, that proves the overflow is happening in the backward pass, not the forward activations — i.e., gradients blowing up through the dA = exp(dt·A) term over the ~250-step unroll even though forward values are now bounded. clamp() zeroes the gradient outside its range, so that specific tensor can't be the source once clamped; the remaining unclamped path (particularly dt32 and A32 before the einsum) is the next suspect, since gradients through exp() compounded across hundreds of BPTT steps can still explode independent of forward-value magnitude.
352
+ y32 = torch.einsum("bdn,bn->bd", new_ssm_state32, C32)
353
+ y32 = y32 + m.D.float() * x_conv32
354
+ y32 = y32 * m.act(z).float() # gated output
355
+ y32 = y32.clamp(
356
+ min=-1e4, max=1e4
357
+ ) # stay well inside fp16 range before out_proj re-enters autocast
358
+ new_ssm_state = new_ssm_state32.to(dtype)
359
+ y = y32.to(dtype)
360
+
361
+ out = m.out_proj(
362
+ y
363
+ ) # (B, d_model) -- back under ambient autocast, safe now that y is bounded
364
+ return out, new_conv_state, new_ssm_state
365
+
366
+
367
+ # ==========================================================================
368
+ # 2. MambaControllerBlock -- pre-norm residual wrapper around one cell
369
+ # ==========================================================================
370
+ class MambaControllerBlock(nn.Module):
371
+ """Add -> LN -> Mixer residual block around one `MambaControllerCell`,
372
+ mirroring `mamba_ssm.modules.block.Block`'s own pattern (pre-norm,
373
+ residual returned separately upstream for fused-kernel reasons that
374
+ don't apply to us; here we just add it back directly since we're not
375
+ using the fused add+norm kernel)."""
376
+
377
+ def __init__(
378
+ self,
379
+ d_model: int,
380
+ d_state: int = 16,
381
+ d_conv: int = 4,
382
+ expand: int = 2,
383
+ layer_idx: int | None = None,
384
+ device: torch.device | None = None,
385
+ dtype: torch.dtype | None = None,
386
+ ):
387
+ super().__init__()
388
+ self.norm = nn.LayerNorm(d_model, device=device, dtype=dtype)
389
+ self.cell = MambaControllerCell(
390
+ d_model,
391
+ d_state=d_state,
392
+ d_conv=d_conv,
393
+ expand=expand,
394
+ layer_idx=layer_idx,
395
+ device=device,
396
+ dtype=dtype,
397
+ )
398
+
399
+ def init_state(
400
+ self, batch_size: int, device: torch.device | None = None, dtype: torch.dtype | None = None
401
+ ):
402
+ return self.cell.init_state(batch_size, device=device, dtype=dtype)
403
+
404
+ def step(self, x: torch.Tensor, state: tuple[torch.Tensor, torch.Tensor]):
405
+ conv_state, ssm_state = state
406
+ out, new_conv_state, new_ssm_state = self.cell.step(self.norm(x), conv_state, ssm_state)
407
+ return x + out, (new_conv_state, new_ssm_state)
408
+
409
+
410
+ # ==========================================================================
411
+ # 3. MambaControllerWrapper -- stack of blocks, nn.LSTM-compatible call API
412
+ # ==========================================================================
413
+ class MambaControllerWrapper(nn.Module):
414
+ """Stacks `num_blocks` `MambaControllerBlock`s and exposes the exact
415
+ calling convention `dnc.dnc.DNC._layer_forward` expects from
416
+ `self.rnns[layer]`:
417
+
418
+ out_unsq, new_hx = wrapper(x_unsq, hx)
419
+
420
+ where `x_unsq` has shape (B, 1, in_dim) (DNC always calls with
421
+ `input.unsqueeze(1)`, one timestep at a time) and `out_unsq` has shape
422
+ (B, 1, d_model). `hx` is a list of `(conv_state, ssm_state)` tuples, one
423
+ per stacked block, or `None` for a fresh zero state.
424
+ """
425
+
426
+ def __init__(
427
+ self,
428
+ in_dim: int,
429
+ d_model: int,
430
+ num_blocks: int = 2,
431
+ moe_enabled: bool = False,
432
+ moe_num_experts: int = 8,
433
+ moe_expert_dim: int | None = None,
434
+ moe_capacity_factor: float = 1.5,
435
+ moe_load_balance_alpha: float = 0.01,
436
+ moe_top_k: int = 1,
437
+ d_state: int = 16,
438
+ d_conv: int = 4,
439
+ expand: int = 2,
440
+ device: torch.device | None = None,
441
+ dtype: torch.dtype | None = None,
442
+ ):
443
+ super().__init__()
444
+ self.d_model = d_model
445
+ self.num_blocks = num_blocks
446
+ # Dimension-matching adapter for the first block only -- mirrors how
447
+ # nn.LSTM(num_layers=N)'s layer-0 weight matrix differs in shape
448
+ # from layers 1..N-1 (layer 0 maps in_dim -> hidden, the rest map
449
+ # hidden -> hidden). Identity when already matched (in_dim == d_model),
450
+ # e.g. for DNC layers beyond the first when num_layers > 1.
451
+ self.in_adapter: nn.Module = (
452
+ nn.Identity()
453
+ if in_dim == d_model
454
+ else nn.Linear(in_dim, d_model, device=device, dtype=dtype)
455
+ )
456
+ self.blocks: nn.ModuleList = nn.ModuleList(
457
+ [
458
+ MambaControllerBlock(
459
+ d_model,
460
+ d_state=d_state,
461
+ d_conv=d_conv,
462
+ expand=expand,
463
+ layer_idx=i,
464
+ device=device,
465
+ dtype=dtype,
466
+ )
467
+ for i in range(num_blocks)
468
+ ]
469
+ )
470
+ self.moe_enabled = moe_enabled
471
+ self.moe_blocks: nn.ModuleList | None = None
472
+ if moe_enabled:
473
+ # External interleaving only (Dead-End #43): one MoEBlock per
474
+ # MambaControllerBlock, applied AFTER that block's own output.
475
+ # Every block gets its own MoE sublayer (matches MoE-Mamba's own
476
+ # architecture, Figure 2: Mamba+MoE repeated N times) rather than
477
+ # every-other-layer -- their own Table 3 shows both converge to
478
+ # the same final perplexity for their best (16-expert) config,
479
+ # so defaulting to every layer costs nothing and is simpler.
480
+ self.moe_blocks = nn.ModuleList(
481
+ [
482
+ MoEBlock(
483
+ d_model,
484
+ num_experts=moe_num_experts,
485
+ expert_dim=moe_expert_dim,
486
+ capacity_factor=moe_capacity_factor,
487
+ load_balance_alpha=moe_load_balance_alpha,
488
+ top_k=moe_top_k,
489
+ device=device,
490
+ dtype=dtype,
491
+ )
492
+ for _ in range(num_blocks)
493
+ ]
494
+ )
495
+
496
+ def init_state(
497
+ self, batch_size: int, device: torch.device | None = None, dtype: torch.dtype | None = None
498
+ ):
499
+ if device is None or dtype is None:
500
+ p = next(self.parameters())
501
+ device = device if device is not None else p.device
502
+ dtype = dtype if dtype is not None else p.dtype
503
+ blocks = cast(Iterable[MambaControllerBlock], self.blocks)
504
+ return [blk.init_state(batch_size, device=device, dtype=dtype) for blk in blocks]
505
+
506
+ def forward(self, input: torch.Tensor, hx):
507
+ # input: (B, 1, in_dim) -- DNC's `_layer_forward` always calls with
508
+ # `input.unsqueeze(1)`, a single timestep at a time, since memory
509
+ # read/write must be interleaved between controller steps.
510
+ assert input.dim() == 3 and input.size(1) == 1, (
511
+ "MambaControllerWrapper only supports single-timestep calls "
512
+ f"(got shape {tuple(input.shape)}); this mirrors how DNC drives "
513
+ "nn.LSTM one step at a time, never a full sequence at once."
514
+ )
515
+ x = input.squeeze(1)
516
+ x = self.in_adapter(x)
517
+
518
+ if hx is None:
519
+ hx = self.init_state(x.size(0), device=x.device, dtype=x.dtype)
520
+
521
+ new_hx = []
522
+ blocks = cast(Iterable[MambaControllerBlock], self.blocks)
523
+ for i, (block, state) in enumerate(zip(blocks, hx)):
524
+ x, new_state = block.step(x, state)
525
+ if self.moe_enabled:
526
+ assert self.moe_blocks is not None
527
+ x = self.moe_blocks[i](x)
528
+ new_hx.append(new_state)
529
+
530
+ return x.unsqueeze(1), new_hx
531
+
532
+
533
+ # ==========================================================================
534
+ # 4. MambaDNC -- dnc.DNC subclass that can build a Mamba-1 controller
535
+ # ==========================================================================
536
+ class MambaDNC(DNC):
537
+ """`dnc.DNC` subclass that adds `rnn_type='mamba'` as a third controller
538
+ option alongside pytorch-dnc's existing `'rnn'`/`'gru'`/`'lstm'`.
539
+
540
+ For any `rnn_type` other than `'mamba'`, `__init__`/`_init_hidden` defer
541
+ entirely to `dnc.DNC`'s own implementation -- this class is a strict
542
+ drop-in superset, so switching the Phase 2 training script between LSTM
543
+ and Mamba controllers is a one-line change (`rnn_type='lstm'` vs.
544
+ `rnn_type='mamba'`), not a code fork. All memory/addressing construction
545
+ (`dnc.memory.Memory`, the output projection) is copied verbatim from
546
+ `dnc.DNC.__init__` for the `'mamba'` branch below, to guarantee it can
547
+ never silently drift from upstream's addressing logic.
548
+ """
549
+
550
+ def __init__(
551
+ self,
552
+ input_size: int,
553
+ hidden_size: int,
554
+ rnn_type: str = "lstm",
555
+ num_layers: int = 1,
556
+ num_hidden_layers: int = 2,
557
+ bias: bool = True,
558
+ batch_first: bool = True,
559
+ dropout: float = 0,
560
+ nr_cells: int = 5,
561
+ read_heads: int = 2,
562
+ cell_size: int = 10,
563
+ nonlinearity: str = "tanh",
564
+ independent_linears: bool = False,
565
+ share_memory_between_layers: bool = True,
566
+ debug: bool = False,
567
+ clip: float = 20,
568
+ device: torch.device | None = None,
569
+ # Mamba-specific hyperparameters (no LSTM/GRU/RNN analogue -- see
570
+ # module docstring's "Design choices" section). Defaults are
571
+ # Mamba-1's own paper defaults.
572
+ mamba_d_state: int = 16,
573
+ mamba_d_conv: int = 4,
574
+ mamba_expand: int = 2,
575
+ # Mamba-2-specific hyperparameters (rnn_type='mamba2' only -- see
576
+ # mamba2_controller.py's module docstring for how these differ from
577
+ # Mamba-1's mamba_d_state/mamba_d_conv/mamba_expand above). Defaults
578
+ # are Mamba-2's own paper defaults (Dao & Gu 2024, Section 7.2:
579
+ # head_dim in {64,128}); ngroups=1 matches every other Mamba-2 use in
580
+ # this project, e.g. mamba_backbone_parallel.py.
581
+ mamba2_d_state: int = 64,
582
+ mamba2_d_conv: int = 4,
583
+ mamba2_expand: int = 2,
584
+ mamba2_headdim: int = 64,
585
+ mamba2_ngroups: int = 1,
586
+ mamba3_d_state: int = 64,
587
+ mamba3_expand: int = 2,
588
+ mamba3_headdim: int = 64,
589
+ mamba3_rope_fraction: float = 0.5,
590
+ # v13: CfC (LNN) controller hyperparameters -- rnn_type='cfc' only.
591
+ cfc_mode: str = "default", # "default" | "pure" | "no_gate"
592
+ cfc_backbone_units: int = 512,
593
+ cfc_backbone_layers: int = 1,
594
+ cfc_backbone_dropout: float = 0.0,
595
+ cfc_activation: str = "lecun_tanh",
596
+ cfc_mixed_memory: bool = False, # True = CfC-mmRNN variant
597
+ cfc_residual: bool = True,
598
+ hybrid_cfc_num_blocks: int = 1, # v14: depth of the CfC stage in "<mamba*>+cfc" rnn_types
599
+ # (the Mamba stage's depth is num_hidden_layers)
600
+ moe_enabled: bool = False,
601
+ moe_num_experts: int = 8,
602
+ moe_expert_dim: int | None = None,
603
+ moe_capacity_factor: float = 1.5,
604
+ moe_load_balance_alpha: float = 0.01,
605
+ moe_top_k: int = 1,
606
+ ):
607
+ if rnn_type.lower() not in ("mamba", "mamba2", "mamba3", "cfc") and not is_hybrid_rnn_type(
608
+ rnn_type
609
+ ):
610
+ super().__init__(
611
+ input_size=input_size,
612
+ hidden_size=hidden_size,
613
+ rnn_type=rnn_type,
614
+ num_layers=num_layers,
615
+ num_hidden_layers=num_hidden_layers,
616
+ bias=bias,
617
+ batch_first=batch_first,
618
+ dropout=dropout,
619
+ nr_cells=nr_cells,
620
+ read_heads=read_heads,
621
+ cell_size=cell_size,
622
+ nonlinearity=nonlinearity,
623
+ independent_linears=independent_linears,
624
+ share_memory_between_layers=share_memory_between_layers,
625
+ debug=debug,
626
+ clip=clip,
627
+ device=device,
628
+ )
629
+ if moe_enabled:
630
+ # Basic MoE for the plain LSTM/GRU/RNN baseline (per the
631
+ # roadmap: the shallow baseline controller only needs a
632
+ # single Top-K MoE sublayer wrapped around its own output,
633
+ # not per-block/per-source specialization -- see
634
+ # moe_layer.MoERNNWrapper). ASSUMPTION FLAGGED FOR
635
+ # VERIFICATION: this assumes dnc.DNC registers each
636
+ # self.rnns[layer] as a genuine submodule under SOME
637
+ # attribute on `self` (every other external-library
638
+ # assumption in this project, e.g. split_graph_dnc.py's
639
+ # Memory-return-shape note, is flagged the same way) -- the
640
+ # sweep below finds and rebinds whichever attribute that is,
641
+ # the same technique dynamic_memory_resize.resize_memory()
642
+ # already uses for Memory swaps.
643
+ for layer in range(num_layers):
644
+ old_rnn = self.rnns[layer]
645
+ wrapped = MoERNNWrapper(
646
+ old_rnn,
647
+ d_model=hidden_size,
648
+ num_experts=moe_num_experts,
649
+ expert_dim=moe_expert_dim,
650
+ top_k=moe_top_k,
651
+ capacity_factor=moe_capacity_factor,
652
+ load_balance_alpha=moe_load_balance_alpha,
653
+ device=device,
654
+ )
655
+ self.rnns[layer] = wrapped
656
+ for attr_name, submodule in list(self._modules.items()):
657
+ if submodule is old_rnn:
658
+ setattr(self, attr_name, wrapped)
659
+ self.moe_layers: list[nn.Module] = []
660
+ for controller in self.rnns:
661
+ self.moe_layers.extend(cast(Any, controller).moe_blocks)
662
+ return
663
+
664
+ if rnn_type.lower() == "mamba2":
665
+ _require_mamba2_ssm()
666
+ elif rnn_type.lower() == "mamba3":
667
+ _require_mamba3_ssm()
668
+ elif is_hybrid_rnn_type(rnn_type):
669
+ _require_ncps()
670
+ elif rnn_type.lower() == "cfc":
671
+ _require_ncps()
672
+ else:
673
+ _require_mamba_ssm()
674
+
675
+ # ---- rnn_type in ("mamba", "mamba2"): custom construction --------
676
+ # Deliberately NOT calling DNC.__init__ (it hardcodes nn.RNN/GRU/LSTM
677
+ # construction inline with no extension point) -- instead, replicate
678
+ # its non-controller-specific bookkeeping verbatim (same attribute
679
+ # names, same Memory/output construction) and only swap in the
680
+ # Mamba controller for `self.rnns`.
681
+ nn.Module.__init__(self)
682
+
683
+ self.input_size = input_size
684
+ self.hidden_size = hidden_size
685
+ self.rnn_type = rnn_type
686
+ self.num_layers = num_layers
687
+ self.num_hidden_layers = num_hidden_layers
688
+ self.bias = bias
689
+ self.batch_first = batch_first
690
+ self.dropout = dropout
691
+ self.nr_cells = nr_cells
692
+ self.read_heads = read_heads
693
+ self.cell_size = cell_size
694
+ self.nonlinearity = nonlinearity
695
+ self.independent_linears = independent_linears
696
+ self.share_memory_between_layers = share_memory_between_layers
697
+ self.debug = debug
698
+ self.clip = clip
699
+ self.device = device
700
+
701
+ # Mamba-only bookkeeping, stored for __repr__ / checkpoint metadata.
702
+ self.mamba_d_state = mamba_d_state
703
+ self.mamba_d_conv = mamba_d_conv
704
+ self.mamba_expand = mamba_expand
705
+
706
+ # Mamba-2-only bookkeeping (rnn_type='mamba2'), same checkpoint-
707
+ # metadata rationale as the Mamba-1 fields above.
708
+ self.mamba2_d_state = mamba2_d_state
709
+ self.mamba2_d_conv = mamba2_d_conv
710
+ self.mamba2_expand = mamba2_expand
711
+ self.mamba2_headdim = mamba2_headdim
712
+ self.mamba2_ngroups = mamba2_ngroups
713
+
714
+ self.mamba3_d_state = mamba3_d_state
715
+ self.mamba3_expand = mamba3_expand
716
+ self.mamba3_headdim = mamba3_headdim
717
+ self.mamba3_rope_fraction = mamba3_rope_fraction
718
+
719
+ self.cfc_mode = cfc_mode
720
+ self.cfc_backbone_units = cfc_backbone_units
721
+ self.cfc_backbone_layers = cfc_backbone_layers
722
+ self.cfc_backbone_dropout = cfc_backbone_dropout
723
+ self.cfc_activation = cfc_activation
724
+ self.cfc_mixed_memory = cfc_mixed_memory
725
+ self.cfc_residual = cfc_residual
726
+ self.hybrid_cfc_num_blocks = hybrid_cfc_num_blocks
727
+
728
+ # Option 4 (MoE) bookkeeping -- stored for checkpoint metadata,
729
+ # same convention as the mamba_d_state/d_conv/expand fields above.
730
+ self.moe_enabled = moe_enabled
731
+ self.moe_num_experts = moe_num_experts
732
+ self.moe_expert_dim = moe_expert_dim
733
+ self.moe_capacity_factor = moe_capacity_factor
734
+ self.moe_load_balance_alpha = moe_load_balance_alpha
735
+ self.moe_top_k = moe_top_k
736
+
737
+ self.w = self.cell_size
738
+ self.r = self.read_heads
739
+ self.read_vectors_size = self.read_heads * self.cell_size
740
+ self.output_size = self.hidden_size
741
+ self.nn_input_size = self.input_size + self.read_vectors_size
742
+ self.nn_output_size = self.output_size + self.read_vectors_size
743
+
744
+ self.rnns: list[nn.Module] = []
745
+ self.memories: list[Memory] = []
746
+
747
+ for layer in range(self.num_layers):
748
+ in_dim = self.nn_input_size if layer == 0 else self.nn_output_size
749
+ if is_hybrid_rnn_type(rnn_type):
750
+ # Every kind in the chain gets the SAME moe_* settings
751
+ # (its own per-block MoE sublayers -- ChainedControllerWrapper
752
+ # already aggregates every stage's moe_blocks generically).
753
+ _moe_per_kind = (
754
+ dict(
755
+ moe_enabled=moe_enabled,
756
+ moe_num_experts=moe_num_experts,
757
+ moe_expert_dim=moe_expert_dim,
758
+ moe_capacity_factor=moe_capacity_factor,
759
+ moe_load_balance_alpha=moe_load_balance_alpha,
760
+ moe_top_k=moe_top_k,
761
+ )
762
+ if moe_enabled
763
+ else {}
764
+ )
765
+ controller = build_hybrid_controller(
766
+ rnn_type.lower(),
767
+ in_dim=in_dim,
768
+ d_model=self.output_size,
769
+ blocks_per_kind={
770
+ "mamba": self.num_hidden_layers,
771
+ "mamba2": self.num_hidden_layers,
772
+ "mamba3": self.num_hidden_layers,
773
+ "cfc": hybrid_cfc_num_blocks,
774
+ },
775
+ kwargs_per_kind={
776
+ "mamba": dict(
777
+ d_state=mamba_d_state,
778
+ d_conv=mamba_d_conv,
779
+ expand=mamba_expand,
780
+ **_moe_per_kind,
781
+ ),
782
+ "mamba2": dict(
783
+ d_state=mamba2_d_state,
784
+ d_conv=mamba2_d_conv,
785
+ expand=mamba2_expand,
786
+ headdim=mamba2_headdim,
787
+ ngroups=mamba2_ngroups,
788
+ **_moe_per_kind,
789
+ ),
790
+ "mamba3": dict(
791
+ d_state=mamba3_d_state,
792
+ expand=mamba3_expand,
793
+ headdim=mamba3_headdim,
794
+ rope_fraction=mamba3_rope_fraction,
795
+ **_moe_per_kind,
796
+ ),
797
+ "cfc": dict(
798
+ mode=cfc_mode,
799
+ backbone_units=cfc_backbone_units,
800
+ backbone_layers=cfc_backbone_layers,
801
+ backbone_dropout=cfc_backbone_dropout,
802
+ activation=cfc_activation,
803
+ mixed_memory=cfc_mixed_memory,
804
+ residual=cfc_residual,
805
+ **_moe_per_kind,
806
+ ),
807
+ },
808
+ device=device,
809
+ )
810
+ elif rnn_type.lower() == "cfc":
811
+ controller = CfCControllerWrapper(
812
+ in_dim=in_dim,
813
+ d_model=self.output_size,
814
+ num_blocks=self.num_hidden_layers,
815
+ mode=cfc_mode,
816
+ backbone_units=cfc_backbone_units,
817
+ backbone_layers=cfc_backbone_layers,
818
+ backbone_dropout=cfc_backbone_dropout,
819
+ activation=cfc_activation,
820
+ mixed_memory=cfc_mixed_memory,
821
+ residual=cfc_residual,
822
+ moe_enabled=moe_enabled,
823
+ moe_num_experts=moe_num_experts,
824
+ moe_expert_dim=moe_expert_dim,
825
+ moe_top_k=moe_top_k,
826
+ moe_capacity_factor=moe_capacity_factor,
827
+ moe_load_balance_alpha=moe_load_balance_alpha,
828
+ device=device,
829
+ )
830
+ elif rnn_type.lower() == "mamba3":
831
+ controller = Mamba3ControllerWrapper(
832
+ in_dim=in_dim,
833
+ d_model=self.output_size,
834
+ num_blocks=self.num_hidden_layers,
835
+ d_state=mamba3_d_state,
836
+ expand=mamba3_expand,
837
+ headdim=mamba3_headdim,
838
+ rope_fraction=mamba3_rope_fraction,
839
+ moe_enabled=moe_enabled,
840
+ moe_num_experts=moe_num_experts,
841
+ moe_expert_dim=moe_expert_dim,
842
+ moe_capacity_factor=moe_capacity_factor,
843
+ moe_load_balance_alpha=moe_load_balance_alpha,
844
+ moe_top_k=moe_top_k,
845
+ device=device,
846
+ )
847
+ elif rnn_type.lower() == "mamba2":
848
+ controller = Mamba2ControllerWrapper(
849
+ in_dim=in_dim,
850
+ d_model=self.output_size,
851
+ num_blocks=self.num_hidden_layers,
852
+ d_state=mamba2_d_state,
853
+ d_conv=mamba2_d_conv,
854
+ expand=mamba2_expand,
855
+ headdim=mamba2_headdim,
856
+ ngroups=mamba2_ngroups,
857
+ moe_enabled=moe_enabled,
858
+ moe_num_experts=moe_num_experts,
859
+ moe_expert_dim=moe_expert_dim,
860
+ moe_capacity_factor=moe_capacity_factor,
861
+ moe_load_balance_alpha=moe_load_balance_alpha,
862
+ moe_top_k=moe_top_k,
863
+ device=device,
864
+ )
865
+ else:
866
+ controller = MambaControllerWrapper(
867
+ in_dim=in_dim,
868
+ d_model=self.output_size,
869
+ num_blocks=self.num_hidden_layers,
870
+ d_state=mamba_d_state,
871
+ d_conv=mamba_d_conv,
872
+ expand=mamba_expand,
873
+ moe_enabled=moe_enabled,
874
+ moe_num_experts=moe_num_experts,
875
+ moe_expert_dim=moe_expert_dim,
876
+ moe_capacity_factor=moe_capacity_factor,
877
+ moe_load_balance_alpha=moe_load_balance_alpha,
878
+ moe_top_k=moe_top_k,
879
+ device=device,
880
+ )
881
+ self.rnns.append(controller)
882
+ # setattr so this shows up as a proper submodule for autograd /
883
+ # optimizer.parameters(), same trick dnc.DNC itself uses for its
884
+ # own self.rnns entries.
885
+ setattr(self, "mamba_layer_" + str(layer), controller)
886
+
887
+ # memories for each layer -- copied verbatim from dnc.DNC.__init__
888
+ if not self.share_memory_between_layers:
889
+ self.memories.append(
890
+ Memory(
891
+ input_size=self.output_size,
892
+ nr_cells=self.nr_cells,
893
+ cell_size=self.w,
894
+ read_heads=self.r,
895
+ device=self.device,
896
+ independent_linears=self.independent_linears,
897
+ )
898
+ )
899
+ setattr(self, "rnn_layer_memory_" + str(layer), self.memories[layer])
900
+
901
+ # only one memory shared by all layers -- copied verbatim from
902
+ # dnc.DNC.__init__
903
+ if self.share_memory_between_layers:
904
+ self.memories.append(
905
+ Memory(
906
+ input_size=self.output_size,
907
+ nr_cells=self.nr_cells,
908
+ cell_size=self.w,
909
+ read_heads=self.r,
910
+ device=self.device,
911
+ independent_linears=self.independent_linears,
912
+ )
913
+ )
914
+ self.rnn_layer_memory_shared = self.memories[0]
915
+
916
+ # Option 4 (MoE): flat list of every installed MoEBlock across all
917
+ # layers, for the training script's pop_total_moe_aux_loss() call --
918
+ # same "give the caller one flat list" convention
919
+ # install_stochastic_write_heads() already uses for stochastic
920
+ # write heads in stochastic_write_head_v2.py.
921
+ self.moe_layers = []
922
+ if self.moe_enabled:
923
+ for layer_controller in self.rnns:
924
+ self.moe_layers.extend(cast(Any, layer_controller).moe_blocks)
925
+
926
+ # final output layer -- copied verbatim from dnc.DNC.__init__
927
+ self.output = nn.Linear(self.nn_output_size, self.input_size)
928
+ torch.nn.init.kaiming_uniform_(self.output.weight)
929
+
930
+ if self.device is not None and self.device.type == "cuda":
931
+ self.to(self.device)
932
+
933
+ def _init_hidden(self, hx, batch_size: int, reset_experience: bool):
934
+ if self.rnn_type.lower() not in (
935
+ "mamba",
936
+ "mamba2",
937
+ "mamba3",
938
+ "cfc",
939
+ ) and not is_hybrid_rnn_type(self.rnn_type):
940
+ return super()._init_hidden(hx, batch_size, reset_experience)
941
+
942
+ # ---- controller-state branch: Mamba (conv_state, ssm_state) ------
943
+ if hx is not None:
944
+ chx, mhx, last_read = hx
945
+ else:
946
+ chx, mhx, last_read = None, None, None
947
+
948
+ if chx is None:
949
+ chx = [
950
+ cast(Any, self.rnns[layer]).init_state(batch_size, device=self.device)
951
+ for layer in range(self.num_layers)
952
+ ]
953
+
954
+ if last_read is None:
955
+ last_read = cuda(torch.zeros(batch_size, self.w * self.r), device=self.device)
956
+
957
+ # ---- memory-state branch: byte-identical to dnc.DNC._init_hidden -
958
+ # (copied verbatim -- this logic is controller-agnostic and must not
959
+ # drift from upstream's own resume/reset semantics)
960
+ if mhx is None:
961
+ if self.share_memory_between_layers:
962
+ mhx = [self.memories[0].reset(batch_size, erase=reset_experience)]
963
+ else:
964
+ mhx = [m.reset(batch_size, erase=reset_experience) for m in self.memories]
965
+ else:
966
+ if self.share_memory_between_layers:
967
+ if len(mhx) == 0 or mhx[0] is None:
968
+ mhx = [self.memories[0].reset(batch_size, erase=reset_experience)]
969
+ else:
970
+ mhx = [self.memories[0].reset(batch_size, mhx[0], erase=reset_experience)]
971
+ else:
972
+ if len(mhx) == 0:
973
+ mhx = [m.reset(batch_size, erase=reset_experience) for m in self.memories]
974
+ else:
975
+ new_mhx = []
976
+ for i, m in enumerate(self.memories):
977
+ if i < len(mhx) and mhx[i] is not None:
978
+ new_mhx.append(m.reset(batch_size, mhx[i], erase=reset_experience))
979
+ else:
980
+ new_mhx.append(m.reset(batch_size, erase=reset_experience))
981
+ mhx = new_mhx
982
+
983
+ return chx, mhx, last_read