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
initium/core_training.py
ADDED
|
@@ -0,0 +1,2182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: core_training.py
|
|
3
|
+
|
|
4
|
+
Dataset-agnostic DNC training core (stochastic write head + learned-prior KL,
|
|
5
|
+
selectable controller, AMP, LR schedule, checkpointing, periodic ID/OOD/
|
|
6
|
+
memory-dependency logging). All dataset specifics (data generation, encoding,
|
|
7
|
+
loss, curriculum, evaluation, OOD test) come from data/ via
|
|
8
|
+
dataset = get_dataset(--dataset-type, --dataset-link)
|
|
9
|
+
(default: graph-traversal). See data/base_dataset.py for the interface.
|
|
10
|
+
|
|
11
|
+
Version notes (core-level):
|
|
12
|
+
- v3: capacity scale-up (MODEL_* constants), linear LR warmup, AMP
|
|
13
|
+
init_scale=128 and GradScaler state saved/restored in checkpoints.
|
|
14
|
+
- v5 (Phase 2): learned, periodically-snapshotted prior N(mu_g, Sigma_g)
|
|
15
|
+
(math in stochastic_write_head_v2.py); prior_snapshots.csv log;
|
|
16
|
+
snapshot_step column; prior_state in checkpoints; run_id tag "_learnedprior".
|
|
17
|
+
- v6: snapshot_step in the console line; ood_rng state in checkpoints.
|
|
18
|
+
- v7+: pluggable controllers via MambaDNC (lstm/mamba/mamba2/mamba3/cfc/
|
|
19
|
+
hybrids), MoE (Option 4), split-graph (Option 5), link-matrix ablation
|
|
20
|
+
(Option 1), static/dynamic N (Option 2), dynamic beta. Each keeps its own
|
|
21
|
+
CLI flag and run_id tag so runs never collide in logs/.
|
|
22
|
+
- v17: dataset split out of this file into data/. Core only talk to the
|
|
23
|
+
dataset through the BaseDataset interface; curriculum's per-lesson memory
|
|
24
|
+
size lives on the curriculum instance (curriculum.lesson_nr_cells).
|
|
25
|
+
- v18: constants moved out: train_config.py (loop/model/KL/prior/checkpoint)
|
|
26
|
+
and controller_config.py (controllers, MoE, split-graph, link-matrix,
|
|
27
|
+
Dynamic-N). Pure move, no value changes.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import csv
|
|
31
|
+
import math
|
|
32
|
+
import os
|
|
33
|
+
import random
|
|
34
|
+
import shutil
|
|
35
|
+
import time
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
import torch
|
|
40
|
+
import torch.nn as nn
|
|
41
|
+
|
|
42
|
+
from initium.memory_manipulation.nvrtc_compat import patch_prod_jiterator
|
|
43
|
+
|
|
44
|
+
patch_prod_jiterator() # environment workaround -- see module docstring
|
|
45
|
+
|
|
46
|
+
from initium.config.controller_config import (
|
|
47
|
+
CFC_ACTIVATION,
|
|
48
|
+
CFC_BACKBONE_DROPOUT,
|
|
49
|
+
CFC_BACKBONE_LAYERS,
|
|
50
|
+
CFC_BACKBONE_UNITS,
|
|
51
|
+
CFC_MIXED_MEMORY,
|
|
52
|
+
CFC_MODE,
|
|
53
|
+
CFC_RESIDUAL,
|
|
54
|
+
CONTROLLER_TYPE,
|
|
55
|
+
DYNAMIC_N_CEILING,
|
|
56
|
+
DYNAMIC_N_COOLDOWN_STEPS,
|
|
57
|
+
DYNAMIC_N_EMA_DECAY,
|
|
58
|
+
DYNAMIC_N_FLOOR,
|
|
59
|
+
DYNAMIC_N_GROWTH_FACTOR,
|
|
60
|
+
DYNAMIC_N_MODE,
|
|
61
|
+
DYNAMIC_N_TRIGGER_FRAC,
|
|
62
|
+
DYNAMIC_N_USAGE_HIGH,
|
|
63
|
+
HYBRID_CFC_NUM_BLOCKS,
|
|
64
|
+
ISOLATE_LINK_ABLATION,
|
|
65
|
+
LINK_MATRIX_MODE,
|
|
66
|
+
LINK_MATRIX_TOPK,
|
|
67
|
+
MAMBA2_D_CONV,
|
|
68
|
+
MAMBA2_D_STATE,
|
|
69
|
+
MAMBA2_EXPAND,
|
|
70
|
+
MAMBA2_HEADDIM,
|
|
71
|
+
MAMBA2_NGROUPS,
|
|
72
|
+
MAMBA3_D_STATE,
|
|
73
|
+
MAMBA3_EXPAND,
|
|
74
|
+
MAMBA3_HEADDIM,
|
|
75
|
+
MAMBA3_ROPE_FRACTION,
|
|
76
|
+
MAMBA_D_CONV,
|
|
77
|
+
MAMBA_D_STATE,
|
|
78
|
+
MAMBA_EXPAND,
|
|
79
|
+
MOE_CAPACITY_FACTOR,
|
|
80
|
+
MOE_ENABLED,
|
|
81
|
+
MOE_EXPERT_DIM,
|
|
82
|
+
MOE_LOAD_BALANCE_ALPHA,
|
|
83
|
+
MOE_NUM_EXPERTS,
|
|
84
|
+
MOE_TOP_K,
|
|
85
|
+
SPLIT_GRAPH_COMBINE_READS,
|
|
86
|
+
SPLIT_GRAPH_COMBINER_CFC_MULTI_SOURCE_MOE,
|
|
87
|
+
SPLIT_GRAPH_COMBINER_MODE,
|
|
88
|
+
SPLIT_GRAPH_COMBINER_NUM_BLOCKS,
|
|
89
|
+
SPLIT_GRAPH_COMBINER_VARIANT,
|
|
90
|
+
SPLIT_GRAPH_ENABLED,
|
|
91
|
+
SPLIT_GRAPH_MAMBA_HEADDIM,
|
|
92
|
+
SPLIT_GRAPH_MAMBA_VARIANT,
|
|
93
|
+
SPLIT_GRAPH_NUM_BLOCKS,
|
|
94
|
+
)
|
|
95
|
+
from initium.config.train_config import (
|
|
96
|
+
AMP_INIT_SCALE,
|
|
97
|
+
BATCH_SIZE,
|
|
98
|
+
BETA_CTRL_ACC_TARGET,
|
|
99
|
+
BETA_CTRL_EMA_DECAY,
|
|
100
|
+
BETA_CTRL_LR,
|
|
101
|
+
BETA_CTRL_MAX,
|
|
102
|
+
BETA_CTRL_MIN,
|
|
103
|
+
BETA_MODE,
|
|
104
|
+
BETAS_TO_SWEEP,
|
|
105
|
+
CHECKPOINT_DIR,
|
|
106
|
+
CHECKPOINT_EVERY,
|
|
107
|
+
DATASET_LINK,
|
|
108
|
+
DATASET_TYPE,
|
|
109
|
+
EVAL_EVERY,
|
|
110
|
+
FREE_BITS,
|
|
111
|
+
KL_ANNEAL_STEPS,
|
|
112
|
+
LESSON_KL_DIP_STEPS,
|
|
113
|
+
LOG_DIR,
|
|
114
|
+
LOG_EVERY,
|
|
115
|
+
LR,
|
|
116
|
+
LR_DECAY_STEPS,
|
|
117
|
+
LR_MIN,
|
|
118
|
+
MODEL_CELL_SIZE,
|
|
119
|
+
MODEL_HIDDEN_SIZE,
|
|
120
|
+
MODEL_NR_CELLS,
|
|
121
|
+
MODEL_READ_HEADS,
|
|
122
|
+
OOD_EVAL_EPISODES,
|
|
123
|
+
OOD_EVAL_EPISODES_PERIODIC,
|
|
124
|
+
PRIOR_MAX_LOGVAR,
|
|
125
|
+
PRIOR_MIN_LOGVAR,
|
|
126
|
+
PRIOR_SNAPSHOT_EVERY,
|
|
127
|
+
SEED,
|
|
128
|
+
TOTAL_STEPS,
|
|
129
|
+
USE_AMP,
|
|
130
|
+
WARMUP_STEPS,
|
|
131
|
+
)
|
|
132
|
+
from initium.data.dataset_registry import get_dataset
|
|
133
|
+
from initium.mamba_controller.mamba_controller import MambaDNC
|
|
134
|
+
from initium.mamba_controller.split_graph_dnc import SplitGraphDNC
|
|
135
|
+
from initium.memory_manipulation.dynamic_memory_resize import resize_memory
|
|
136
|
+
from initium.memory_manipulation.dynamic_n_controller import DynamicNController
|
|
137
|
+
from initium.memory_manipulation.link_matrix_ablation import patch_link_matrix
|
|
138
|
+
from initium.memory_manipulation.stochastic_write_head_v2 import (
|
|
139
|
+
get_prior_state,
|
|
140
|
+
install_stochastic_write_heads,
|
|
141
|
+
load_prior_state,
|
|
142
|
+
pop_total_kl,
|
|
143
|
+
update_all_prior_snapshots,
|
|
144
|
+
)
|
|
145
|
+
from initium.MoE.moe_layer import pop_total_moe_aux_loss
|
|
146
|
+
|
|
147
|
+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
|
148
|
+
print(f"Using device: {device}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _arch_desc(
|
|
152
|
+
controller,
|
|
153
|
+
sg_enabled,
|
|
154
|
+
sg_variant,
|
|
155
|
+
sg_blocks,
|
|
156
|
+
sg_comb_mode,
|
|
157
|
+
sg_comb_variant,
|
|
158
|
+
sg_comb_blocks,
|
|
159
|
+
sg_combine_reads,
|
|
160
|
+
):
|
|
161
|
+
if not sg_enabled:
|
|
162
|
+
return f"controller={controller}"
|
|
163
|
+
if sg_comb_mode == "controller":
|
|
164
|
+
comb = f"controller:{sg_comb_variant}x{sg_comb_blocks}"
|
|
165
|
+
else:
|
|
166
|
+
comb = "linear" if sg_combine_reads else "none(no-combine)"
|
|
167
|
+
return f"split-graph[backbone={sg_variant}x{sg_blocks} | combiner={comb}]"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _first_nonfinite_report(tensor, name, step):
|
|
171
|
+
"""Silent when finite; prints once when not. Exists to bisect WHICH
|
|
172
|
+
pipeline stage a NaN/Inf first appears at, since the LOG_EVERY-averaged
|
|
173
|
+
training log only shows the aggregate, not the originating stage."""
|
|
174
|
+
ok = torch.isfinite(tensor).all()
|
|
175
|
+
if not ok:
|
|
176
|
+
bad_frac = (~torch.isfinite(tensor)).float().mean().item()
|
|
177
|
+
print(
|
|
178
|
+
f"[NaN-TRACE] step {step}: non-finite in '{name}' ({bad_frac * 100:.2f}% of elements)"
|
|
179
|
+
)
|
|
180
|
+
return ok
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def save_checkpoint(
|
|
184
|
+
path,
|
|
185
|
+
rnn,
|
|
186
|
+
output_proj,
|
|
187
|
+
stochastic_heads,
|
|
188
|
+
optimizer,
|
|
189
|
+
curriculum,
|
|
190
|
+
step,
|
|
191
|
+
beta_target,
|
|
192
|
+
run_id,
|
|
193
|
+
scaler,
|
|
194
|
+
ood_rng,
|
|
195
|
+
controller_type=CONTROLLER_TYPE,
|
|
196
|
+
link_matrix_mode=LINK_MATRIX_MODE,
|
|
197
|
+
link_matrix_topk=LINK_MATRIX_TOPK, # Static Option 1
|
|
198
|
+
dynamic_n_mode=DYNAMIC_N_MODE,
|
|
199
|
+
dynamic_n_state=None, # Dynamic-N (macro-scale)
|
|
200
|
+
moe_enabled=MOE_ENABLED,
|
|
201
|
+
moe_num_experts=MOE_NUM_EXPERTS,
|
|
202
|
+
moe_expert_dim=MOE_EXPERT_DIM,
|
|
203
|
+
moe_capacity_factor=MOE_CAPACITY_FACTOR,
|
|
204
|
+
moe_load_balance_alpha=MOE_LOAD_BALANCE_ALPHA,
|
|
205
|
+
moe_top_k=MOE_TOP_K,
|
|
206
|
+
split_graph_enabled=SPLIT_GRAPH_ENABLED,
|
|
207
|
+
split_graph_variant=SPLIT_GRAPH_MAMBA_VARIANT,
|
|
208
|
+
extra_model_config=None,
|
|
209
|
+
):
|
|
210
|
+
"""Save everything needed to resume training or re-run eval later:
|
|
211
|
+
- model + output-projection + optimizer state
|
|
212
|
+
- LR is NOT saved separately -- it's now a pure function of `step`
|
|
213
|
+
(see lr_at_step()/set_lr() in run()), clamped at LR_DECAY_STEPS, so
|
|
214
|
+
it's fully reconstructible from `step` alone on resume. This also
|
|
215
|
+
sidesteps the CosineAnnealingLR periodicity bug (see chat log /
|
|
216
|
+
Q21-Experiment-Log Section 5) that previously let LR drift back up
|
|
217
|
+
after LR_DECAY_STEPS via the old scheduler's carried internal state.
|
|
218
|
+
- the stochastic write head(s)' own params are already inside
|
|
219
|
+
rnn.state_dict() (they were installed as submodules via the
|
|
220
|
+
write_vector_transform swap), so no separate save needed for those
|
|
221
|
+
- RNG state (python/numpy/torch/cuda) for reproducibility
|
|
222
|
+
- curriculum lesson, so eval-time episode difficulty matches training
|
|
223
|
+
- v3 fix: GradScaler state (scale, growth tracker, etc). Previously
|
|
224
|
+
NOT saved -- `scaler = torch.amp.GradScaler(...)` was rebuilt fresh
|
|
225
|
+
on every resume, throwing away whatever scale the prior leg had
|
|
226
|
+
found and re-running a smaller version of the AMP overflow cascade
|
|
227
|
+
right after every resume (see header note). `scaler` is now a
|
|
228
|
+
required arg so this can't silently regress back to being dropped.
|
|
229
|
+
- v5 (Phase 2): `prior_state` -- mu_g, Sigma_g (as prior_logvar), and
|
|
230
|
+
last_snapshot_step for every installed stochastic write head (see
|
|
231
|
+
get_prior_state() in stochastic_write_head.py). NOT covered by
|
|
232
|
+
rnn.state_dict(): prior_mu/prior_logvar are registered buffers, so
|
|
233
|
+
they normally WOULD ride along inside rnn.state_dict() automatically
|
|
234
|
+
-- but they are captured here explicitly, as their own top-level
|
|
235
|
+
checkpoint key, so this checkpoint's prior state is self-describing
|
|
236
|
+
and independently loadable (e.g. by eval_from_checkpoint.py without
|
|
237
|
+
needing to reload the entire rnn state_dict), and so a Phase-1-era
|
|
238
|
+
reader that doesn't expect these buffers can still load
|
|
239
|
+
rnn_state_dict unmodified. Mirrors the reasoning already applied to
|
|
240
|
+
scaler_state_dict: model weights alone are not enough to reproduce
|
|
241
|
+
this run's behavior on resume/re-eval.
|
|
242
|
+
- v6: `ood_rng_state` -- the dedicated OOD-sampling random.Random
|
|
243
|
+
instance's own state (via .getstate()), saved separately from the
|
|
244
|
+
existing `rng_state` block (which only ever covered the global
|
|
245
|
+
python/numpy/torch/cuda streams). Without this, a resumed run would
|
|
246
|
+
silently re-seed ood_rng back to its start-of-process value
|
|
247
|
+
(deterministic from `seed` alone -- see run()) instead of
|
|
248
|
+
continuing the exact OOD walk sequence the pre-resume leg had
|
|
249
|
+
reached, which is a smaller version of the same "silently evaluate
|
|
250
|
+
against the wrong state" bug class prior_state (above) exists to
|
|
251
|
+
catch -- it doesn't change the metric's validity (ood_rng was
|
|
252
|
+
always decoupled from the training stream, which was the actual
|
|
253
|
+
fix), but it does mean a resume's OOD trajectory isn't bit-for-bit
|
|
254
|
+
continuous with the pre-resume leg unless this is restored. `ood_rng`
|
|
255
|
+
is now a required arg so this can't silently regress the way the
|
|
256
|
+
scaler-state omission originally did.
|
|
257
|
+
- v7 (Alternate Phase 3, Step 1): `model_config` gained `controller_type`
|
|
258
|
+
and (only when it's "mamba") `mamba_d_state`/`mamba_d_conv`/
|
|
259
|
+
`mamba_expand`. Same "self-describing checkpoint" rationale as
|
|
260
|
+
prior_state/scaler_state_dict above: an LSTM checkpoint's
|
|
261
|
+
rnn_state_dict has `lstm_layer_0...` keys, a Mamba checkpoint's has
|
|
262
|
+
`mamba_layer_0...` keys (see mamba_controller.py's MambaDNC), and
|
|
263
|
+
without this field a reader has no way to know which model class to
|
|
264
|
+
reconstruct before attempting `load_state_dict`.
|
|
265
|
+
- Dynamic-N (macro-scale): `model_config["dynamic_n_mode"]`, same
|
|
266
|
+
self-describing-checkpoint rationale as controller_type/
|
|
267
|
+
link_matrix_mode above -- records WHICH mechanism was responsible
|
|
268
|
+
for whatever nr_cells this checkpoint's rnn.memories[0].nr_cells
|
|
269
|
+
already self-describes (curriculum lookup vs. usage-triggered
|
|
270
|
+
growth), even though the resulting nr_cells value itself doesn't
|
|
271
|
+
care which mechanism produced it. Also `dynamic_n_state` (top-level
|
|
272
|
+
key, not inside model_config, same convention as prior_state /
|
|
273
|
+
scaler_state_dict): DynamicNController's usage-EMA, cooldown
|
|
274
|
+
counter, and full growth_history -- this is run *state*, not model
|
|
275
|
+
architecture, so it lives alongside prior_state/ood_rng_state
|
|
276
|
+
rather than inside model_config. None when this run isn't in
|
|
277
|
+
dynamic_n_mode.
|
|
278
|
+
"""
|
|
279
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
280
|
+
model_config = {
|
|
281
|
+
"input_size": rnn.input_size,
|
|
282
|
+
"hidden_size": MODEL_HIDDEN_SIZE,
|
|
283
|
+
"num_hidden_layers": getattr(rnn, "num_hidden_layers", None),
|
|
284
|
+
"nr_cells": rnn.memories[0].nr_cells,
|
|
285
|
+
"cell_size": MODEL_CELL_SIZE,
|
|
286
|
+
"read_heads": MODEL_READ_HEADS,
|
|
287
|
+
"controller_type": controller_type, # v7
|
|
288
|
+
"link_matrix_mode": link_matrix_mode,
|
|
289
|
+
"link_matrix_topk": link_matrix_topk, # Static Option 1
|
|
290
|
+
"dynamic_n_mode": dynamic_n_mode, # Dynamic-N (macro-scale)
|
|
291
|
+
"moe_enabled": moe_enabled,
|
|
292
|
+
"moe_num_experts": moe_num_experts, # v8 (Option 4)
|
|
293
|
+
"moe_expert_dim": moe_expert_dim,
|
|
294
|
+
"moe_capacity_factor": moe_capacity_factor,
|
|
295
|
+
"moe_load_balance_alpha": moe_load_balance_alpha,
|
|
296
|
+
"moe_top_k": moe_top_k,
|
|
297
|
+
"split_graph_enabled": split_graph_enabled, # v9 (Option 5)
|
|
298
|
+
"split_graph_variant": split_graph_variant,
|
|
299
|
+
}
|
|
300
|
+
if controller_type == "mamba": # v7
|
|
301
|
+
model_config.update(
|
|
302
|
+
{
|
|
303
|
+
"mamba_d_state": MAMBA_D_STATE,
|
|
304
|
+
"mamba_d_conv": MAMBA_D_CONV,
|
|
305
|
+
"mamba_expand": MAMBA_EXPAND,
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
elif controller_type == "mamba2":
|
|
309
|
+
model_config.update(
|
|
310
|
+
{
|
|
311
|
+
"mamba2_d_state": MAMBA2_D_STATE,
|
|
312
|
+
"mamba2_d_conv": MAMBA2_D_CONV,
|
|
313
|
+
"mamba2_expand": MAMBA2_EXPAND,
|
|
314
|
+
"mamba2_headdim": MAMBA2_HEADDIM,
|
|
315
|
+
"mamba2_ngroups": MAMBA2_NGROUPS,
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
elif controller_type == "mamba3":
|
|
319
|
+
model_config.update(
|
|
320
|
+
{
|
|
321
|
+
"mamba3_d_state": MAMBA3_D_STATE,
|
|
322
|
+
"mamba3_expand": MAMBA3_EXPAND,
|
|
323
|
+
"mamba3_headdim": MAMBA3_HEADDIM,
|
|
324
|
+
"mamba3_rope_fraction": MAMBA3_ROPE_FRACTION,
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
elif controller_type == "cfc":
|
|
328
|
+
model_config.update(
|
|
329
|
+
{
|
|
330
|
+
"cfc_mode": CFC_MODE,
|
|
331
|
+
"cfc_backbone_units": CFC_BACKBONE_UNITS,
|
|
332
|
+
"cfc_backbone_layers": CFC_BACKBONE_LAYERS,
|
|
333
|
+
"cfc_backbone_dropout": CFC_BACKBONE_DROPOUT,
|
|
334
|
+
"cfc_activation": CFC_ACTIVATION,
|
|
335
|
+
"cfc_mixed_memory": CFC_MIXED_MEMORY,
|
|
336
|
+
"cfc_residual": CFC_RESIDUAL,
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
elif "+" in controller_type: # v14: hybrid chains, e.g. "mamba+cfc"
|
|
340
|
+
model_config.update(
|
|
341
|
+
{
|
|
342
|
+
"mamba_d_state": MAMBA_D_STATE,
|
|
343
|
+
"mamba_d_conv": MAMBA_D_CONV,
|
|
344
|
+
"mamba_expand": MAMBA_EXPAND,
|
|
345
|
+
"mamba2_d_state": MAMBA2_D_STATE,
|
|
346
|
+
"mamba2_d_conv": MAMBA2_D_CONV,
|
|
347
|
+
"mamba2_expand": MAMBA2_EXPAND,
|
|
348
|
+
"mamba2_headdim": MAMBA2_HEADDIM,
|
|
349
|
+
"mamba2_ngroups": MAMBA2_NGROUPS,
|
|
350
|
+
"mamba3_d_state": MAMBA3_D_STATE,
|
|
351
|
+
"mamba3_expand": MAMBA3_EXPAND,
|
|
352
|
+
"mamba3_headdim": MAMBA3_HEADDIM,
|
|
353
|
+
"mamba3_rope_fraction": MAMBA3_ROPE_FRACTION,
|
|
354
|
+
"cfc_mode": CFC_MODE,
|
|
355
|
+
"cfc_backbone_units": CFC_BACKBONE_UNITS,
|
|
356
|
+
"cfc_backbone_layers": CFC_BACKBONE_LAYERS,
|
|
357
|
+
"cfc_backbone_dropout": CFC_BACKBONE_DROPOUT,
|
|
358
|
+
"cfc_activation": CFC_ACTIVATION,
|
|
359
|
+
"cfc_mixed_memory": CFC_MIXED_MEMORY,
|
|
360
|
+
"cfc_residual": CFC_RESIDUAL,
|
|
361
|
+
"hybrid_cfc_num_blocks": HYBRID_CFC_NUM_BLOCKS,
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if extra_model_config: # v19: dataset type / dims / split-graph details, read by inference/
|
|
366
|
+
model_config.update(extra_model_config)
|
|
367
|
+
|
|
368
|
+
torch.save(
|
|
369
|
+
{
|
|
370
|
+
"step": step,
|
|
371
|
+
"run_id": run_id,
|
|
372
|
+
"beta_target": beta_target,
|
|
373
|
+
"model_config": model_config,
|
|
374
|
+
"rnn_state_dict": rnn.state_dict(),
|
|
375
|
+
"output_proj_state_dict": output_proj.state_dict(),
|
|
376
|
+
"optimizer_state_dict": optimizer.state_dict(),
|
|
377
|
+
"scaler_state_dict": scaler.state_dict(), # v3 fix
|
|
378
|
+
"curriculum_lesson": curriculum.lesson,
|
|
379
|
+
"prior_state": get_prior_state(stochastic_heads), # v5 (Phase 2)
|
|
380
|
+
"ood_rng_state": ood_rng.getstate(), # v6
|
|
381
|
+
"dynamic_n_state": dynamic_n_state, # Dynamic-N (macro-scale); None if not dynamic_n_mode
|
|
382
|
+
"rng_state": {
|
|
383
|
+
"python": random.getstate(),
|
|
384
|
+
"numpy": np.random.get_state(),
|
|
385
|
+
"torch": torch.get_rng_state(),
|
|
386
|
+
"cuda": torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
path,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def load_checkpoint_for_resume(path, device):
|
|
394
|
+
"""Load a checkpoint saved by save_checkpoint(), for CONTINUING training
|
|
395
|
+
(as opposed to eval_from_checkpoint.py, which loads for eval only and
|
|
396
|
+
doesn't need optimizer/scheduler/RNG state).
|
|
397
|
+
|
|
398
|
+
map_location='cpu' (not `device`): torch.load's map_location moves
|
|
399
|
+
EVERY tensor in the checkpoint, including the RNG-state ByteTensors.
|
|
400
|
+
torch.cuda.set_rng_state_all() requires those to stay plain CPU
|
|
401
|
+
ByteTensors -- if map_location drags them onto CUDA they become
|
|
402
|
+
torch.cuda.ByteTensor and set_rng_state_all rejects them. Loading to
|
|
403
|
+
CPU and letting rnn.load_state_dict()/optimizer.load_state_dict() do
|
|
404
|
+
their own (automatic) device casting for the model/optimizer tensors
|
|
405
|
+
avoids that without needing two different map_locations for one file.
|
|
406
|
+
|
|
407
|
+
weights_only=False: this checkpoint stores non-tensor python/numpy RNG
|
|
408
|
+
state alongside the tensors, and PyTorch >=2.6 defaults torch.load to
|
|
409
|
+
weights_only=True. Safe here since it's a checkpoint we produced
|
|
410
|
+
ourselves, not a downloaded/untrusted file.
|
|
411
|
+
"""
|
|
412
|
+
return torch.load(path, map_location="cpu", weights_only=False)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def restore_rng_state(rng_state):
|
|
416
|
+
random.setstate(rng_state["python"])
|
|
417
|
+
np.random.set_state(rng_state["numpy"])
|
|
418
|
+
torch.set_rng_state(
|
|
419
|
+
rng_state["torch"].cpu() if torch.is_tensor(rng_state["torch"]) else rng_state["torch"]
|
|
420
|
+
)
|
|
421
|
+
if rng_state["cuda"] is not None and torch.cuda.is_available():
|
|
422
|
+
torch.cuda.set_rng_state_all(rng_state["cuda"])
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ==========================================
|
|
426
|
+
# TRAINING LOOP
|
|
427
|
+
# ==========================================
|
|
428
|
+
def run(
|
|
429
|
+
beta_target: float,
|
|
430
|
+
run_id: str,
|
|
431
|
+
seed: int = SEED,
|
|
432
|
+
resume_from: str | None = None,
|
|
433
|
+
controller: str = CONTROLLER_TYPE, # v7 (Alternate Phase 3, Step 1)
|
|
434
|
+
beta_mode: str = BETA_MODE, # dynamic-beta toggle: "static" or "dynamic"
|
|
435
|
+
total_steps: int = TOTAL_STEPS, # see --total-steps.
|
|
436
|
+
checkpoint_every: int = CHECKPOINT_EVERY, # see --checkpoint-every.
|
|
437
|
+
link_matrix_mode: str = LINK_MATRIX_MODE, # Static Option 1
|
|
438
|
+
link_matrix_topk: int | None = LINK_MATRIX_TOPK, # Static Option 1
|
|
439
|
+
isolate_link_ablation: bool = ISOLATE_LINK_ABLATION, # Static Option 1
|
|
440
|
+
dynamic_n_mode: bool = DYNAMIC_N_MODE, # Dynamic-N (macro-scale)
|
|
441
|
+
dynamic_n_floor: int = DYNAMIC_N_FLOOR,
|
|
442
|
+
dynamic_n_ceiling: int = DYNAMIC_N_CEILING,
|
|
443
|
+
dynamic_n_trigger_frac: float = DYNAMIC_N_TRIGGER_FRAC,
|
|
444
|
+
dynamic_n_cooldown_steps: int = DYNAMIC_N_COOLDOWN_STEPS,
|
|
445
|
+
moe_enabled: bool = MOE_ENABLED,
|
|
446
|
+
moe_num_experts: int = MOE_NUM_EXPERTS,
|
|
447
|
+
moe_expert_dim: int | None = MOE_EXPERT_DIM,
|
|
448
|
+
moe_capacity_factor: float = MOE_CAPACITY_FACTOR,
|
|
449
|
+
moe_load_balance_alpha: float = MOE_LOAD_BALANCE_ALPHA,
|
|
450
|
+
moe_top_k: int = MOE_TOP_K,
|
|
451
|
+
split_graph_combiner_cfc_multi_source_moe: bool = SPLIT_GRAPH_COMBINER_CFC_MULTI_SOURCE_MOE,
|
|
452
|
+
split_graph_enabled: bool = SPLIT_GRAPH_ENABLED,
|
|
453
|
+
split_graph_variant: str = SPLIT_GRAPH_MAMBA_VARIANT,
|
|
454
|
+
split_graph_num_blocks: int = SPLIT_GRAPH_NUM_BLOCKS,
|
|
455
|
+
split_graph_headdim: int = SPLIT_GRAPH_MAMBA_HEADDIM,
|
|
456
|
+
split_graph_combine_reads: bool = SPLIT_GRAPH_COMBINE_READS,
|
|
457
|
+
split_graph_combiner_mode: str = SPLIT_GRAPH_COMBINER_MODE,
|
|
458
|
+
split_graph_combiner_variant: str = SPLIT_GRAPH_COMBINER_VARIANT,
|
|
459
|
+
split_graph_combiner_num_blocks: int = SPLIT_GRAPH_COMBINER_NUM_BLOCKS,
|
|
460
|
+
dataset_type: str = DATASET_TYPE,
|
|
461
|
+
dataset_link: str | None = DATASET_LINK,
|
|
462
|
+
test_dataset_link: str | None = None,
|
|
463
|
+
):
|
|
464
|
+
# Deliberately does NOT touch LR_DECAY_STEPS - that's a separate
|
|
465
|
+
# module-level constant, fixed at import time from the *original*
|
|
466
|
+
# TOTAL_STEPS, and lr_at_step()/set_lr() below read it directly by
|
|
467
|
+
# name, not through this parameter. A pilot run still anneals LR on
|
|
468
|
+
# the full 120000-step schedule and simply stops early partway
|
|
469
|
+
# through it, exactly as the --total-steps help text promises.
|
|
470
|
+
dataset = get_dataset(dataset_type, dataset_link, test_dataset_link=test_dataset_link)
|
|
471
|
+
INPUT_DIM, TRIPLE_DIM = dataset.input_dim, dataset.output_dim
|
|
472
|
+
beta_ctrl_acc_target = (
|
|
473
|
+
BETA_CTRL_ACC_TARGET if BETA_CTRL_ACC_TARGET is not None else dataset.advance_threshold
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
torch.manual_seed(seed)
|
|
477
|
+
random.seed(seed)
|
|
478
|
+
np.random.seed(seed)
|
|
479
|
+
torch.backends.cudnn.benchmark = True
|
|
480
|
+
|
|
481
|
+
os.makedirs(LOG_DIR, exist_ok=True)
|
|
482
|
+
log_path = os.path.join(LOG_DIR, f"run_{run_id}.csv")
|
|
483
|
+
resuming = resume_from is not None
|
|
484
|
+
log_file = open(log_path, "a" if resuming else "w", newline="")
|
|
485
|
+
log_writer = csv.writer(log_file)
|
|
486
|
+
if not resuming:
|
|
487
|
+
log_writer.writerow(
|
|
488
|
+
[
|
|
489
|
+
"step",
|
|
490
|
+
"lesson",
|
|
491
|
+
"beta_effective",
|
|
492
|
+
"task_loss",
|
|
493
|
+
"kl_loss",
|
|
494
|
+
"total_loss",
|
|
495
|
+
"digit_diversity",
|
|
496
|
+
"kl_mean",
|
|
497
|
+
"kl_max",
|
|
498
|
+
"kl_min",
|
|
499
|
+
"kl_std",
|
|
500
|
+
"lr",
|
|
501
|
+
"grad_norm",
|
|
502
|
+
"amp_scale",
|
|
503
|
+
"elapsed_sec",
|
|
504
|
+
"snapshot_step",
|
|
505
|
+
"gpu_mem_peak_mb",
|
|
506
|
+
"param_count",
|
|
507
|
+
"moe_aux_loss",
|
|
508
|
+
"moe_cv_importance",
|
|
509
|
+
"moe_cv_load",
|
|
510
|
+
"moe_max_load_frac",
|
|
511
|
+
]
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
ood_log_path = os.path.join(LOG_DIR, f"run_{run_id}_ood.csv")
|
|
515
|
+
ood_log_file = open(ood_log_path, "a" if resuming else "w", newline="")
|
|
516
|
+
ood_log_writer = csv.writer(ood_log_file)
|
|
517
|
+
if not resuming:
|
|
518
|
+
ood_log_writer.writerow(
|
|
519
|
+
[
|
|
520
|
+
"step",
|
|
521
|
+
"lesson",
|
|
522
|
+
"id_triple_acc",
|
|
523
|
+
"id_perfect_frac",
|
|
524
|
+
"ood_triple_acc",
|
|
525
|
+
"ood_perfect_frac",
|
|
526
|
+
"ood_offset_triple",
|
|
527
|
+
"ood_offset_perfect",
|
|
528
|
+
]
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Functional-usage check (LB-9/Concept 6): same-cadence, same-distribution
|
|
532
|
+
# companion to the ID/OOD log above, but with the model's own memory
|
|
533
|
+
# path switched off (see evaluate_traversal's ablate_memory kwarg). A
|
|
534
|
+
# standing per-run check, not a one-off sanity pass, per the roadmap's
|
|
535
|
+
# explicit requirement -- catches rsDNC's silent bypass-path collapse
|
|
536
|
+
# (still trains, still runs, loss curves look fine) that a KL- or
|
|
537
|
+
# loss-only view can't see.
|
|
538
|
+
mem_check_path = os.path.join(LOG_DIR, f"run_{run_id}_memory_dependency.csv")
|
|
539
|
+
mem_check_file = open(mem_check_path, "a" if resuming else "w", newline="")
|
|
540
|
+
mem_check_writer = csv.writer(mem_check_file)
|
|
541
|
+
if not resuming:
|
|
542
|
+
mem_check_writer.writerow(
|
|
543
|
+
[
|
|
544
|
+
"step",
|
|
545
|
+
"lesson",
|
|
546
|
+
"id_triple_acc",
|
|
547
|
+
"id_perfect_frac",
|
|
548
|
+
"ablated_triple_acc",
|
|
549
|
+
"ablated_perfect_frac",
|
|
550
|
+
"memory_dependency_triple",
|
|
551
|
+
"memory_dependency_perfect",
|
|
552
|
+
]
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
combiner_stage_log_path = os.path.join(LOG_DIR, f"run_{run_id}_combiner_stage_dependency.csv")
|
|
556
|
+
combiner_stage_log_file = open(combiner_stage_log_path, "a" if resuming else "w", newline="")
|
|
557
|
+
combiner_stage_log_writer = csv.writer(combiner_stage_log_file)
|
|
558
|
+
if not resuming:
|
|
559
|
+
combiner_stage_log_writer.writerow(
|
|
560
|
+
[
|
|
561
|
+
"step",
|
|
562
|
+
"lesson",
|
|
563
|
+
"stage_index",
|
|
564
|
+
"stage_kind",
|
|
565
|
+
"id_triple_acc",
|
|
566
|
+
"id_perfect_frac",
|
|
567
|
+
"ablated_triple_acc",
|
|
568
|
+
"ablated_perfect_frac",
|
|
569
|
+
"stage_dependency_triple",
|
|
570
|
+
"stage_dependency_perfect",
|
|
571
|
+
]
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
lesson_log_path = os.path.join(LOG_DIR, f"run_{run_id}_lesson_advances.csv")
|
|
575
|
+
modality_log_path = os.path.join(LOG_DIR, f"run_{run_id}_modality_dependency.csv")
|
|
576
|
+
modality_log_file = open(modality_log_path, "a" if resuming else "w", newline="")
|
|
577
|
+
modality_log_writer = csv.writer(modality_log_file)
|
|
578
|
+
if not resuming:
|
|
579
|
+
modality_log_writer.writerow(
|
|
580
|
+
[
|
|
581
|
+
"step",
|
|
582
|
+
"lesson",
|
|
583
|
+
"modality",
|
|
584
|
+
"id_acc",
|
|
585
|
+
"id_perfect_frac",
|
|
586
|
+
"ablated_acc",
|
|
587
|
+
"ablated_perfect_frac",
|
|
588
|
+
"modality_dependency_acc",
|
|
589
|
+
"modality_dependency_perfect",
|
|
590
|
+
]
|
|
591
|
+
)
|
|
592
|
+
lesson_log_file = open(lesson_log_path, "a" if resuming else "w", newline="")
|
|
593
|
+
lesson_log_writer = csv.writer(lesson_log_file)
|
|
594
|
+
if not resuming:
|
|
595
|
+
lesson_log_writer.writerow(["step", "new_lesson", "of_total_lessons"])
|
|
596
|
+
|
|
597
|
+
# Static Option 1 watch-item: per-hop-count accuracy, logged as its own
|
|
598
|
+
# trajectory instead of console-only. The link-matrix ablation/sparsify
|
|
599
|
+
# finding this option validates (Session 009 synthesis) was established
|
|
600
|
+
# on QA-style tasks, where each answer is effectively a 1-hop lookup and
|
|
601
|
+
# content-based addressing dominates. Graph traversal chains multiple
|
|
602
|
+
# hops per episode -- exactly where temporal/sequential-link addressing
|
|
603
|
+
# could matter more than it does for QA. So the thing to watch isn't
|
|
604
|
+
# whether aggregate triple_acc holds after ablating/sparsifying the link
|
|
605
|
+
# matrix, it's whether accuracy holds *evenly across hop counts* or
|
|
606
|
+
# collapses specifically at higher hops. hop_breakdown already computes
|
|
607
|
+
# this at every advance-check; this file just persists it.
|
|
608
|
+
hop_log_path = os.path.join(LOG_DIR, f"run_{run_id}_hop_breakdown.csv")
|
|
609
|
+
hop_log_file = open(hop_log_path, "a" if resuming else "w", newline="")
|
|
610
|
+
hop_log_writer = csv.writer(hop_log_file)
|
|
611
|
+
if not resuming:
|
|
612
|
+
hop_log_writer.writerow(
|
|
613
|
+
[
|
|
614
|
+
"step",
|
|
615
|
+
"lesson",
|
|
616
|
+
"eval_type",
|
|
617
|
+
"hop_count",
|
|
618
|
+
"triple_acc",
|
|
619
|
+
"perfect_frac",
|
|
620
|
+
"n_episodes",
|
|
621
|
+
]
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# v5 (Phase 2): periodic "prior snapshot updated" log -- net-new file,
|
|
625
|
+
# written every PRIOR_SNAPSHOT_EVERY steps by update_all_prior_snapshots()
|
|
626
|
+
# below. This is the direct evidence for the Q48 periodic-snapshot
|
|
627
|
+
# classification and for a collapse/drift check on the prior itself
|
|
628
|
+
# (Sigma_g -> 0 would not show up in the existing kl_mean diagnostic,
|
|
629
|
+
# which only ever describes q(v_t), never the prior it's compared
|
|
630
|
+
# against). `snapshot_step` here is always == `step` (the update just
|
|
631
|
+
# happened at this step); it's included anyway so this file's schema is
|
|
632
|
+
# self-describing without cross-referencing the main log.
|
|
633
|
+
prior_log_path = os.path.join(LOG_DIR, f"run_{run_id}_prior_snapshots.csv")
|
|
634
|
+
prior_log_file = open(prior_log_path, "a" if resuming else "w", newline="")
|
|
635
|
+
prior_log_writer = csv.writer(prior_log_file)
|
|
636
|
+
if not resuming:
|
|
637
|
+
prior_log_writer.writerow(
|
|
638
|
+
[
|
|
639
|
+
"step",
|
|
640
|
+
"snapshot_step",
|
|
641
|
+
"mu_g_norm",
|
|
642
|
+
"sigma_g_mean",
|
|
643
|
+
"sigma_g_min",
|
|
644
|
+
"sigma_g_max",
|
|
645
|
+
"trace_sigma_g",
|
|
646
|
+
"n_samples",
|
|
647
|
+
"raw_var_mean",
|
|
648
|
+
"raw_var_max",
|
|
649
|
+
"raw_hi_frac",
|
|
650
|
+
]
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
curriculum = dataset.make_curriculum()
|
|
654
|
+
curriculum.advance_log_writer = lesson_log_writer # log addition #4, read by maybe_advance
|
|
655
|
+
curriculum.advance_log_file = lesson_log_file # flushed after each write
|
|
656
|
+
curriculum.hop_log_writer = hop_log_writer # Static Option 1 watch-item
|
|
657
|
+
curriculum.hop_log_file = hop_log_file
|
|
658
|
+
|
|
659
|
+
field_log_path = os.path.join(LOG_DIR, f"run_{run_id}_field_breakdown.csv")
|
|
660
|
+
field_log_file = open(field_log_path, "a" if resuming else "w", newline="")
|
|
661
|
+
field_log_writer = csv.writer(field_log_file)
|
|
662
|
+
if os.path.getsize(field_log_path) == 0:
|
|
663
|
+
# v20: header is dataset-owned (see BaseDataset.field_log_header) instead of
|
|
664
|
+
# hardcoded to graph's src/edge/dst schema, since text/audio/video/multimodal
|
|
665
|
+
# now log their own value/cumsum breakdown into this same file.
|
|
666
|
+
field_log_writer.writerow(["step", "lesson", "eval_type"] + dataset.field_log_header())
|
|
667
|
+
curriculum.field_log_writer = field_log_writer
|
|
668
|
+
curriculum.field_log_file = field_log_file
|
|
669
|
+
|
|
670
|
+
# v5 (Phase 2): dedicated RNG stream for OOD (London Underground) walk
|
|
671
|
+
# sampling, decoupled from the global `random` stream that curriculum ID
|
|
672
|
+
# sampling uses. Constructed once per run (not re-seeded per call --
|
|
673
|
+
# that was exactly the bug already fixed for
|
|
674
|
+
# build_london_underground_eval()'s station/label mapping, see the v2
|
|
675
|
+
# header note) and passed as `rng=` to every OOD evaluate_traversal()
|
|
676
|
+
# call below. Seed is derived from this run's own seed so different
|
|
677
|
+
# seeds in a panel still get different (but each internally
|
|
678
|
+
# reproducible) OOD walk sequences, rather than all sharing one fixed
|
|
679
|
+
# stream. Purely a cleanliness fix for the cross-seed comparisons Phase
|
|
680
|
+
# 2 relies on -- see Section 2g in
|
|
681
|
+
# Q21-Phase_1_Multi-seed_verification.md for the confound this removes.
|
|
682
|
+
ood_rng = random.Random(seed * 1_000_003 + 17)
|
|
683
|
+
|
|
684
|
+
starting_nr_cells = dynamic_n_floor if dynamic_n_mode else MODEL_NR_CELLS
|
|
685
|
+
hidden_size, nr_cells, cell_size, read_heads = (
|
|
686
|
+
MODEL_HIDDEN_SIZE,
|
|
687
|
+
starting_nr_cells,
|
|
688
|
+
MODEL_CELL_SIZE,
|
|
689
|
+
MODEL_READ_HEADS,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
print(
|
|
693
|
+
f"[{run_id}] Model config: hidden={hidden_size} nr_cells={nr_cells} "
|
|
694
|
+
f"cell_size={cell_size} read_heads={read_heads} | beta={beta_target} "
|
|
695
|
+
f"| "
|
|
696
|
+
+ _arch_desc(
|
|
697
|
+
controller,
|
|
698
|
+
split_graph_enabled,
|
|
699
|
+
split_graph_variant,
|
|
700
|
+
split_graph_num_blocks,
|
|
701
|
+
split_graph_combiner_mode,
|
|
702
|
+
split_graph_combiner_variant,
|
|
703
|
+
split_graph_combiner_num_blocks,
|
|
704
|
+
split_graph_combine_reads,
|
|
705
|
+
)
|
|
706
|
+
+ (f" | moe={moe_num_experts}e" if moe_enabled else "")
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
if isolate_link_ablation:
|
|
710
|
+
curriculum.lesson_nr_cells = [MODEL_NR_CELLS] * len(curriculum.table)
|
|
711
|
+
print(
|
|
712
|
+
f"[{run_id}] isolate_link_ablation=True - nr_cells held fixed "
|
|
713
|
+
f"at {MODEL_NR_CELLS} for the whole run (Option 2's resize "
|
|
714
|
+
f"mechanism will never fire this run)."
|
|
715
|
+
)
|
|
716
|
+
if dynamic_n_mode:
|
|
717
|
+
# Same isolation mechanism as isolate_link_ablation above (Concept
|
|
718
|
+
# 16/SP-10): hold the curriculum-indexed lookup constant so
|
|
719
|
+
# TraversalCurriculum.maybe_advance()'s own resize_memory() call
|
|
720
|
+
# never fires -- DynamicNController is the only thing allowed to
|
|
721
|
+
# resize nr_cells this run.
|
|
722
|
+
curriculum.lesson_nr_cells = [dynamic_n_floor] * len(curriculum.table)
|
|
723
|
+
print(
|
|
724
|
+
f"[{run_id}] dynamic_n_mode=True - nr_cells starts at "
|
|
725
|
+
f"{dynamic_n_floor} and grows via usage-triggered "
|
|
726
|
+
f"DynamicNController (ceiling={dynamic_n_ceiling}, "
|
|
727
|
+
f"trigger_frac={dynamic_n_trigger_frac}, "
|
|
728
|
+
f"cooldown_steps={dynamic_n_cooldown_steps}); Option 2's "
|
|
729
|
+
f"curriculum-indexed resize mechanism will never fire this run."
|
|
730
|
+
)
|
|
731
|
+
if isolate_link_ablation:
|
|
732
|
+
print(
|
|
733
|
+
f"[{run_id}] NOTE: isolate_link_ablation AND dynamic_n_mode "
|
|
734
|
+
f"are both set -- these are two independent overrides of "
|
|
735
|
+
f"LESSON_NR_CELLS (dynamic_n_mode's wins, since it's "
|
|
736
|
+
f"checked second). Combining an isolated-Option-1 run "
|
|
737
|
+
f"with Dynamic-N growth is unusual; make sure that's "
|
|
738
|
+
f"actually what you meant to measure per Concept 16/SP-10."
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
mamba_kwargs: dict[str, Any] = {}
|
|
742
|
+
if controller == "mamba":
|
|
743
|
+
mamba_kwargs = dict(
|
|
744
|
+
mamba_d_state=MAMBA_D_STATE,
|
|
745
|
+
mamba_d_conv=MAMBA_D_CONV,
|
|
746
|
+
mamba_expand=MAMBA_EXPAND,
|
|
747
|
+
moe_enabled=moe_enabled,
|
|
748
|
+
moe_num_experts=moe_num_experts,
|
|
749
|
+
moe_expert_dim=moe_expert_dim,
|
|
750
|
+
moe_capacity_factor=moe_capacity_factor,
|
|
751
|
+
moe_load_balance_alpha=moe_load_balance_alpha,
|
|
752
|
+
moe_top_k=moe_top_k,
|
|
753
|
+
)
|
|
754
|
+
elif controller == "mamba2":
|
|
755
|
+
mamba_kwargs = dict(
|
|
756
|
+
mamba2_d_state=MAMBA2_D_STATE,
|
|
757
|
+
mamba2_d_conv=MAMBA2_D_CONV,
|
|
758
|
+
mamba2_expand=MAMBA2_EXPAND,
|
|
759
|
+
mamba2_headdim=MAMBA2_HEADDIM,
|
|
760
|
+
mamba2_ngroups=MAMBA2_NGROUPS,
|
|
761
|
+
moe_enabled=moe_enabled,
|
|
762
|
+
moe_num_experts=moe_num_experts,
|
|
763
|
+
moe_expert_dim=moe_expert_dim,
|
|
764
|
+
moe_capacity_factor=moe_capacity_factor,
|
|
765
|
+
moe_load_balance_alpha=moe_load_balance_alpha,
|
|
766
|
+
moe_top_k=moe_top_k,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
if controller == "mamba3":
|
|
770
|
+
mamba_kwargs = dict(
|
|
771
|
+
mamba3_d_state=MAMBA3_D_STATE,
|
|
772
|
+
mamba3_expand=MAMBA3_EXPAND,
|
|
773
|
+
mamba3_headdim=MAMBA3_HEADDIM,
|
|
774
|
+
mamba3_rope_fraction=MAMBA3_ROPE_FRACTION,
|
|
775
|
+
moe_enabled=moe_enabled,
|
|
776
|
+
moe_num_experts=moe_num_experts,
|
|
777
|
+
moe_expert_dim=moe_expert_dim,
|
|
778
|
+
moe_capacity_factor=moe_capacity_factor,
|
|
779
|
+
moe_load_balance_alpha=moe_load_balance_alpha,
|
|
780
|
+
moe_top_k=moe_top_k,
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
if controller == "cfc":
|
|
784
|
+
mamba_kwargs = dict(
|
|
785
|
+
cfc_mode=CFC_MODE,
|
|
786
|
+
cfc_backbone_units=CFC_BACKBONE_UNITS,
|
|
787
|
+
cfc_backbone_layers=CFC_BACKBONE_LAYERS,
|
|
788
|
+
cfc_backbone_dropout=CFC_BACKBONE_DROPOUT,
|
|
789
|
+
cfc_activation=CFC_ACTIVATION,
|
|
790
|
+
cfc_mixed_memory=CFC_MIXED_MEMORY,
|
|
791
|
+
cfc_residual=CFC_RESIDUAL,
|
|
792
|
+
moe_enabled=moe_enabled,
|
|
793
|
+
moe_top_k=moe_top_k,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
if "+" in controller: # v14: hybrid chain, e.g. "mamba+cfc"
|
|
797
|
+
mamba_kwargs = dict(
|
|
798
|
+
mamba_d_state=MAMBA_D_STATE,
|
|
799
|
+
mamba_d_conv=MAMBA_D_CONV,
|
|
800
|
+
mamba_expand=MAMBA_EXPAND,
|
|
801
|
+
mamba2_d_state=MAMBA2_D_STATE,
|
|
802
|
+
mamba2_d_conv=MAMBA2_D_CONV,
|
|
803
|
+
mamba2_expand=MAMBA2_EXPAND,
|
|
804
|
+
mamba2_headdim=MAMBA2_HEADDIM,
|
|
805
|
+
mamba2_ngroups=MAMBA2_NGROUPS,
|
|
806
|
+
mamba3_d_state=MAMBA3_D_STATE,
|
|
807
|
+
mamba3_expand=MAMBA3_EXPAND,
|
|
808
|
+
mamba3_headdim=MAMBA3_HEADDIM,
|
|
809
|
+
mamba3_rope_fraction=MAMBA3_ROPE_FRACTION,
|
|
810
|
+
cfc_mode=CFC_MODE,
|
|
811
|
+
cfc_backbone_units=CFC_BACKBONE_UNITS,
|
|
812
|
+
cfc_backbone_layers=CFC_BACKBONE_LAYERS,
|
|
813
|
+
cfc_backbone_dropout=CFC_BACKBONE_DROPOUT,
|
|
814
|
+
cfc_activation=CFC_ACTIVATION,
|
|
815
|
+
cfc_mixed_memory=CFC_MIXED_MEMORY,
|
|
816
|
+
cfc_residual=CFC_RESIDUAL,
|
|
817
|
+
hybrid_cfc_num_blocks=HYBRID_CFC_NUM_BLOCKS,
|
|
818
|
+
moe_enabled=moe_enabled,
|
|
819
|
+
moe_top_k=moe_top_k,
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
if split_graph_enabled:
|
|
823
|
+
if moe_enabled:
|
|
824
|
+
print(
|
|
825
|
+
f"[{run_id}] moe_enabled=True with split_graph_enabled=True: MoE is now "
|
|
826
|
+
f"wired into both the parallel backbone and the sequential combiner (see "
|
|
827
|
+
f"split_graph_dnc.py). A combiner_variant='cfc' additionally routes its two "
|
|
828
|
+
f"natural input sources (backbone output, previous read vector) through "
|
|
829
|
+
f"per-source specialized experts when "
|
|
830
|
+
f"split_graph_combiner_cfc_multi_source_moe={split_graph_combiner_cfc_multi_source_moe} "
|
|
831
|
+
f"(see moe_layer.MultiSourceMoEBlock)."
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# v10: variant-matched hyperparameter defaults instead of always
|
|
835
|
+
# reusing the Mamba-1 constants here -- harmless previously (any
|
|
836
|
+
# d_state/headdim is accepted), but not what the SSD paper's own
|
|
837
|
+
# Mamba-2 defaults are.
|
|
838
|
+
_sg_d_state, _sg_d_conv, _sg_expand = (
|
|
839
|
+
(MAMBA3_D_STATE, MAMBA_D_CONV, MAMBA3_EXPAND)
|
|
840
|
+
if split_graph_variant.startswith("mamba3")
|
|
841
|
+
else (MAMBA2_D_STATE, MAMBA2_D_CONV, MAMBA2_EXPAND)
|
|
842
|
+
if split_graph_variant.startswith("mamba2")
|
|
843
|
+
else (MAMBA_D_STATE, MAMBA_D_CONV, MAMBA_EXPAND)
|
|
844
|
+
)
|
|
845
|
+
rnn = SplitGraphDNC(
|
|
846
|
+
input_size=INPUT_DIM,
|
|
847
|
+
hidden_size=hidden_size,
|
|
848
|
+
nr_cells=nr_cells,
|
|
849
|
+
cell_size=cell_size,
|
|
850
|
+
read_heads=read_heads,
|
|
851
|
+
num_backbone_blocks=split_graph_num_blocks,
|
|
852
|
+
mamba_variant=split_graph_variant,
|
|
853
|
+
mamba_d_state=_sg_d_state,
|
|
854
|
+
mamba_d_conv=_sg_d_conv,
|
|
855
|
+
mamba_expand=_sg_expand,
|
|
856
|
+
mamba_headdim=split_graph_headdim,
|
|
857
|
+
cfc_kwargs=dict(
|
|
858
|
+
mode=CFC_MODE,
|
|
859
|
+
backbone_units=CFC_BACKBONE_UNITS,
|
|
860
|
+
backbone_layers=CFC_BACKBONE_LAYERS,
|
|
861
|
+
backbone_dropout=CFC_BACKBONE_DROPOUT,
|
|
862
|
+
activation=CFC_ACTIVATION,
|
|
863
|
+
mixed_memory=CFC_MIXED_MEMORY,
|
|
864
|
+
residual=CFC_RESIDUAL,
|
|
865
|
+
),
|
|
866
|
+
combine_reads=split_graph_combine_reads,
|
|
867
|
+
combiner_mode=split_graph_combiner_mode,
|
|
868
|
+
combiner_variant=split_graph_combiner_variant,
|
|
869
|
+
combiner_num_blocks=split_graph_combiner_num_blocks,
|
|
870
|
+
independent_linears=True,
|
|
871
|
+
moe_enabled=moe_enabled,
|
|
872
|
+
moe_num_experts=moe_num_experts,
|
|
873
|
+
moe_expert_dim=moe_expert_dim,
|
|
874
|
+
moe_top_k=moe_top_k,
|
|
875
|
+
moe_capacity_factor=moe_capacity_factor,
|
|
876
|
+
moe_load_balance_alpha=moe_load_balance_alpha,
|
|
877
|
+
moe_cfc_multi_source=split_graph_combiner_cfc_multi_source_moe,
|
|
878
|
+
device=device,
|
|
879
|
+
).to(device)
|
|
880
|
+
else:
|
|
881
|
+
rnn = MambaDNC(
|
|
882
|
+
input_size=INPUT_DIM,
|
|
883
|
+
hidden_size=hidden_size,
|
|
884
|
+
rnn_type=controller, # v7: 'lstm' (default) or 'mamba'
|
|
885
|
+
num_layers=1,
|
|
886
|
+
nr_cells=nr_cells,
|
|
887
|
+
cell_size=cell_size,
|
|
888
|
+
read_heads=read_heads,
|
|
889
|
+
batch_first=True,
|
|
890
|
+
device=device,
|
|
891
|
+
independent_linears=True, # Phase 1: required so Memory exposes a
|
|
892
|
+
# standalone write_vector_transform Linear for
|
|
893
|
+
# install_stochastic_write_heads to swap out. Doesn't change the
|
|
894
|
+
# addressing math - just which code path builds the (functionally
|
|
895
|
+
# equivalent) per-head transforms. See header note + Section 5 of
|
|
896
|
+
# Q21-Experiment-Log.md for why this makes Run 0 an anchor-by-
|
|
897
|
+
# equivalence rather than a bit-identical beta=0 substitute.
|
|
898
|
+
# v7: unaffected by the controller swap -- MambaDNC builds
|
|
899
|
+
# dnc.memory.Memory identically regardless of rnn_type.
|
|
900
|
+
**mamba_kwargs,
|
|
901
|
+
).to(device)
|
|
902
|
+
|
|
903
|
+
combiner_stage_kinds = getattr(
|
|
904
|
+
getattr(rnn, "combiner_wrapper", None), "stage_kinds", None
|
|
905
|
+
) # set only for a hybrid ("<kind>+<kind>") split-graph combiner; None otherwise
|
|
906
|
+
# v19: self-describing metadata for inference/ (capabilities + exact rebuild)
|
|
907
|
+
extra_model_config = {
|
|
908
|
+
"dataset_type": dataset_type,
|
|
909
|
+
"dataset_name": dataset.name,
|
|
910
|
+
"supported_dataset_types": [dataset_type], # use ["*"] for a future multi-modal model
|
|
911
|
+
"input_dim": INPUT_DIM,
|
|
912
|
+
"output_dim": TRIPLE_DIM,
|
|
913
|
+
"num_hidden_layers": getattr(rnn, "num_hidden_layers", None),
|
|
914
|
+
"split_graph_num_blocks": split_graph_num_blocks,
|
|
915
|
+
"split_graph_headdim": split_graph_headdim,
|
|
916
|
+
"split_graph_combine_reads": split_graph_combine_reads,
|
|
917
|
+
"split_graph_combiner_mode": split_graph_combiner_mode,
|
|
918
|
+
"split_graph_combiner_variant": split_graph_combiner_variant,
|
|
919
|
+
"split_graph_combiner_num_blocks": split_graph_combiner_num_blocks,
|
|
920
|
+
"split_graph_mamba_hparams": (
|
|
921
|
+
dict(d_state=_sg_d_state, d_conv=_sg_d_conv, expand=_sg_expand)
|
|
922
|
+
if split_graph_enabled
|
|
923
|
+
else None
|
|
924
|
+
),
|
|
925
|
+
"split_graph_cfc_kwargs": (
|
|
926
|
+
dict(
|
|
927
|
+
mode=CFC_MODE,
|
|
928
|
+
backbone_units=CFC_BACKBONE_UNITS,
|
|
929
|
+
backbone_layers=CFC_BACKBONE_LAYERS,
|
|
930
|
+
backbone_dropout=CFC_BACKBONE_DROPOUT,
|
|
931
|
+
activation=CFC_ACTIVATION,
|
|
932
|
+
mixed_memory=CFC_MIXED_MEMORY,
|
|
933
|
+
residual=CFC_RESIDUAL,
|
|
934
|
+
)
|
|
935
|
+
if split_graph_enabled
|
|
936
|
+
else None
|
|
937
|
+
),
|
|
938
|
+
"split_graph_combiner_cfc_multi_source_moe": split_graph_combiner_cfc_multi_source_moe,
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if link_matrix_mode != "dense":
|
|
942
|
+
patch_link_matrix(rnn, mode=link_matrix_mode, topk=link_matrix_topk)
|
|
943
|
+
topk_note = f" topk={link_matrix_topk}" if link_matrix_mode == "sparse_topk" else ""
|
|
944
|
+
print(
|
|
945
|
+
f"[{run_id}] link_matrix_mode={link_matrix_mode}{topk_note} -- "
|
|
946
|
+
f"Static Option 1 applied to rnn.memories."
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
output_proj = nn.Linear(INPUT_DIM, TRIPLE_DIM).to(device)
|
|
950
|
+
dataset.set_output_proj(output_proj)
|
|
951
|
+
|
|
952
|
+
# Install the stochastic write head(s) BEFORE building the
|
|
953
|
+
# optimizer, so their parameters (mu_transform, logvar_transform) are
|
|
954
|
+
# included in optimizer.parameters(). mu_transform is initialized from
|
|
955
|
+
# the original deterministic write_vector_transform's weights, so at
|
|
956
|
+
# step 0 the sampled mean exactly matches Phase 0's write vector.
|
|
957
|
+
# sample=True for all beta in BETAS_TO_SWEEP now that beta=0 is dropped
|
|
958
|
+
# from the sweep (see header note) -- beta_target is always > 0.0 here.
|
|
959
|
+
stochastic_heads = install_stochastic_write_heads(
|
|
960
|
+
rnn, device=device, sample=(beta_target > 0.0)
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
optimizer = torch.optim.Adam(list(rnn.parameters()) + list(output_proj.parameters()), lr=LR)
|
|
964
|
+
# Option 3 (Concept 24/LB-17): multi-axis efficiency accounting.
|
|
965
|
+
# param_count is fixed for the life of this run -- static Option 2's
|
|
966
|
+
# nr_cells resize never changes it (every Memory sublayer is sized by
|
|
967
|
+
# cell_size/read_heads/input_size, never nr_cells, per the Option-2
|
|
968
|
+
# analysis) -- so this is a one-time computation, not per-step.
|
|
969
|
+
param_count = sum(p.numel() for p in rnn.parameters()) + sum(
|
|
970
|
+
p.numel() for p in output_proj.parameters()
|
|
971
|
+
)
|
|
972
|
+
amp_enabled = USE_AMP and device.type == "cuda"
|
|
973
|
+
kl_on = stochastic_heads[0].sample # False when beta==0 -> no KL/prior terms in console
|
|
974
|
+
print(
|
|
975
|
+
f"[{run_id}] params {param_count} | kl_terms={'on' if kl_on else 'off'} | amp={'on' if amp_enabled else 'off'}"
|
|
976
|
+
)
|
|
977
|
+
scaler = torch.amp.GradScaler("cuda", enabled=amp_enabled, init_scale=AMP_INIT_SCALE)
|
|
978
|
+
|
|
979
|
+
# Dynamic-N (macro-scale): None unless dynamic_n_mode -- every call site
|
|
980
|
+
# below guards on `dynamic_n_ctrl is not None` so this is a true no-op
|
|
981
|
+
# (not just a disabled-but-present object) when the flag is off.
|
|
982
|
+
dynamic_n_ctrl = (
|
|
983
|
+
DynamicNController(
|
|
984
|
+
floor=dynamic_n_floor,
|
|
985
|
+
ceiling=dynamic_n_ceiling,
|
|
986
|
+
growth_factor=DYNAMIC_N_GROWTH_FACTOR,
|
|
987
|
+
trigger_frac=dynamic_n_trigger_frac,
|
|
988
|
+
ema_decay=DYNAMIC_N_EMA_DECAY,
|
|
989
|
+
cooldown_steps=dynamic_n_cooldown_steps,
|
|
990
|
+
)
|
|
991
|
+
if dynamic_n_mode
|
|
992
|
+
else None
|
|
993
|
+
)
|
|
994
|
+
dynamic_n_log_writer = None
|
|
995
|
+
if dynamic_n_ctrl is not None:
|
|
996
|
+
# Own CSV, same one-file-per-mechanism convention as
|
|
997
|
+
# run_{run_id}_lesson_advances.csv / _prior_snapshots.csv above.
|
|
998
|
+
dynamic_n_log_path = os.path.join(LOG_DIR, f"run_{run_id}_dynamic_n_growth.csv")
|
|
999
|
+
dynamic_n_log_file = open(dynamic_n_log_path, "a" if resuming else "w", newline="")
|
|
1000
|
+
dynamic_n_log_writer = csv.writer(dynamic_n_log_file)
|
|
1001
|
+
if not resuming:
|
|
1002
|
+
dynamic_n_log_writer.writerow(
|
|
1003
|
+
["step", "lesson", "old_nr_cells", "new_nr_cells", "usage_ema"]
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
# --- LR schedule -----------------------------------------------------
|
|
1007
|
+
# FIX (see chat log / Q21-Experiment-Log Section 5): CosineAnnealingLR's
|
|
1008
|
+
# formula -- eta_min + 0.5*(base_lr-eta_min)*(1+cos(pi*last_epoch/T_max))
|
|
1009
|
+
# -- is PERIODIC in last_epoch with period 2*T_max, not a one-way ramp
|
|
1010
|
+
# that flatlines at eta_min once last_epoch exceeds T_max. Every run
|
|
1011
|
+
# that trains past LR_DECAY_STEPS=20000 (i.e. every extended/switch-late
|
|
1012
|
+
# run in this project, since TOTAL_STEPS=80000) was therefore climbing
|
|
1013
|
+
# back toward LR=3e-4 after each trough at step 20000, 60000, ... and
|
|
1014
|
+
# peaking again at step 40000, 80000, ... -- confirmed empirically via
|
|
1015
|
+
# the added LR log column (observed ~2.94e-4 at step 78000, matching
|
|
1016
|
+
# the closed-form prediction almost exactly). The original 20,000-step
|
|
1017
|
+
# beta-sweep (Run 1 beta in {0.01,0.1,1.0}) is NOT affected -- it ends
|
|
1018
|
+
# exactly at the first trough -- but the extended beta=0 run and both
|
|
1019
|
+
# switch-late runs (beta=0.001, beta=0.01) trained/evaluated well past
|
|
1020
|
+
# that point under a partially-cycling, not monotonically-decaying, LR.
|
|
1021
|
+
#
|
|
1022
|
+
# Fix: don't use torch's stateful scheduler at all (its internal
|
|
1023
|
+
# last_epoch counter is also what silently carried the drift across
|
|
1024
|
+
# checkpoint resumes). Instead recompute LR directly from the absolute
|
|
1025
|
+
# step every iteration, with the step clamped at LR_DECAY_STEPS so the
|
|
1026
|
+
# schedule flatlines at LR_MIN once reached, exactly matching the
|
|
1027
|
+
# "cosine anneal from LR to LR_MIN over this many steps" comment this
|
|
1028
|
+
# constant always had -- it just was never enforced past that point.
|
|
1029
|
+
#
|
|
1030
|
+
# v3 addition: linear warmup 0 -> LR over the first WARMUP_STEPS,
|
|
1031
|
+
# before the cosine anneal (which now spans WARMUP_STEPS..LR_DECAY_STEPS
|
|
1032
|
+
# instead of 0..LR_DECAY_STEPS, so it still reaches LR_MIN exactly at
|
|
1033
|
+
# LR_DECAY_STEPS as before). Since LR is still a pure function of the
|
|
1034
|
+
# absolute step, this stays resume-safe for the same reason the original
|
|
1035
|
+
# fix was -- a resume past WARMUP_STEPS just lands in the cosine part,
|
|
1036
|
+
# no separate warmup state to save/restore.
|
|
1037
|
+
def lr_at_step(s):
|
|
1038
|
+
if s < WARMUP_STEPS:
|
|
1039
|
+
return LR * (s + 1) / WARMUP_STEPS
|
|
1040
|
+
s_clamped = min(s, LR_DECAY_STEPS)
|
|
1041
|
+
decay_span = max(1, LR_DECAY_STEPS - WARMUP_STEPS)
|
|
1042
|
+
progress = (s_clamped - WARMUP_STEPS) / decay_span
|
|
1043
|
+
return LR_MIN + 0.5 * (LR - LR_MIN) * (1 + math.cos(math.pi * progress))
|
|
1044
|
+
|
|
1045
|
+
def set_lr(step_now):
|
|
1046
|
+
lr_now = lr_at_step(step_now)
|
|
1047
|
+
for g in optimizer.param_groups:
|
|
1048
|
+
g["lr"] = lr_now
|
|
1049
|
+
return lr_now
|
|
1050
|
+
|
|
1051
|
+
step = 0
|
|
1052
|
+
anneal_start_step = 0 # Phase 1: absolute step the KL anneal ramp is measured from.
|
|
1053
|
+
# Stays 0 for a normal fresh run or a same-beta resume (anneal counted
|
|
1054
|
+
# from the true start, as before). Reset to the resume step below when
|
|
1055
|
+
# the checkpoint's beta_target differs from this run's -- i.e. a
|
|
1056
|
+
# deliberate deterministic(or other-beta)->this-beta switch -- so the
|
|
1057
|
+
# ramp restarts at 0 from the switch point instead of reading the
|
|
1058
|
+
# already-large absolute step and jumping straight to full beta_target
|
|
1059
|
+
# on the first post-switch update.
|
|
1060
|
+
lesson_dip_start_step = -(
|
|
1061
|
+
10**9
|
|
1062
|
+
) # idle at run start (so the dip ramp is already at 1.0 immediately);
|
|
1063
|
+
# reset to `step` on every lesson advance instead
|
|
1064
|
+
if resuming:
|
|
1065
|
+
ckpt = load_checkpoint_for_resume(resume_from, device)
|
|
1066
|
+
# Static Option 2 resume fix: rnn was just constructed above at
|
|
1067
|
+
# LESSON_NR_CELLS[0] (curriculum.lesson doesn't exist yet at that
|
|
1068
|
+
# point in run()). If this checkpoint is mid-schedule, the live
|
|
1069
|
+
# model's Memory is still the wrong size -- maybe_advance() only
|
|
1070
|
+
# resizes on a LIVE lesson transition, which never replays for
|
|
1071
|
+
# lessons already passed before this resume. Must happen BEFORE
|
|
1072
|
+
# rnn.load_state_dict() below, so load_state_dict fills the
|
|
1073
|
+
# resized module's parameters with the checkpoint's trained values
|
|
1074
|
+
# directly, instead of transplanting fresh-init weights that then
|
|
1075
|
+
# get immediately overwritten anyway.
|
|
1076
|
+
# Prefer the checkpoint's own recorded nr_cells (self-describing,
|
|
1077
|
+
# Edit A above) over recomputing from the current LESSON_NR_CELLS
|
|
1078
|
+
# list, in case the schedule was edited since this checkpoint was
|
|
1079
|
+
# produced.
|
|
1080
|
+
ckpt_nr_cells = ckpt.get("model_config", {}).get("nr_cells")
|
|
1081
|
+
if ckpt_nr_cells is None:
|
|
1082
|
+
ckpt_nr_cells = curriculum.lesson_nr_cells[ckpt["curriculum_lesson"]]
|
|
1083
|
+
print(
|
|
1084
|
+
f"[{run_id}] WARNING: checkpoint predates per-lesson "
|
|
1085
|
+
f"nr_cells recording (static Option 2) -- inferring "
|
|
1086
|
+
f"nr_cells={ckpt_nr_cells} from LESSON_NR_CELLS"
|
|
1087
|
+
f"[{ckpt['curriculum_lesson']}] instead of a recorded value."
|
|
1088
|
+
)
|
|
1089
|
+
resize_memory(rnn, ckpt_nr_cells, device=device, optimizer=optimizer)
|
|
1090
|
+
|
|
1091
|
+
rnn.load_state_dict(ckpt["rnn_state_dict"])
|
|
1092
|
+
output_proj.load_state_dict(ckpt["output_proj_state_dict"])
|
|
1093
|
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
|
1094
|
+
# v3 fix: restore GradScaler state (scale, growth tracker) instead
|
|
1095
|
+
# of leaving `scaler` at its freshly-constructed init_scale -- see
|
|
1096
|
+
# header note. `.get(...)` instead of ["..."] so a pre-v3 checkpoint
|
|
1097
|
+
# (saved before this key existed) still loads, just without this fix.
|
|
1098
|
+
if "scaler_state_dict" in ckpt:
|
|
1099
|
+
scaler.load_state_dict(ckpt["scaler_state_dict"])
|
|
1100
|
+
else:
|
|
1101
|
+
print(
|
|
1102
|
+
f"[{run_id}] WARNING: checkpoint predates GradScaler-state "
|
|
1103
|
+
f"saving (v3 fix) -- scaler starting fresh at "
|
|
1104
|
+
f"init_scale={scaler.get_scale()} instead of resuming the "
|
|
1105
|
+
# (continued below, unchanged)
|
|
1106
|
+
f"prior leg's scale."
|
|
1107
|
+
)
|
|
1108
|
+
# NOT loading scheduler_state_dict: LR is now recomputed from the
|
|
1109
|
+
# absolute step every iteration via set_lr() (see above), clamped
|
|
1110
|
+
# at LR_DECAY_STEPS -- this sidesteps the periodicity bug entirely,
|
|
1111
|
+
# and also means old checkpoints' scheduler_state_dict (which may
|
|
1112
|
+
# already reflect the buggy drifted state) is simply ignored rather
|
|
1113
|
+
# than needing migration.
|
|
1114
|
+
curriculum.lesson = ckpt["curriculum_lesson"]
|
|
1115
|
+
# Dynamic-N (macro-scale): restore the usage-EMA/cooldown/growth
|
|
1116
|
+
# history, so a resumed run doesn't silently reset the trigger's
|
|
1117
|
+
# sustained-saturation window back to 0 (which would just delay the
|
|
1118
|
+
# next growth event by however long the EMA takes to re-climb --
|
|
1119
|
+
# not incorrect, but not reproducible either) and so growth_history
|
|
1120
|
+
# stays complete across resumed legs. `.get(...)` so a checkpoint
|
|
1121
|
+
# saved before dynamic_n_mode existed, or a checkpoint from a
|
|
1122
|
+
# non-dynamic-N run, still loads -- the controller (if this run
|
|
1123
|
+
# even has one) just starts fresh, same as a brand-new run would.
|
|
1124
|
+
if dynamic_n_ctrl is not None:
|
|
1125
|
+
if ckpt.get("dynamic_n_state") is not None:
|
|
1126
|
+
dynamic_n_ctrl.load_state_dict(ckpt["dynamic_n_state"])
|
|
1127
|
+
else:
|
|
1128
|
+
print(
|
|
1129
|
+
f"[{run_id}] WARNING: checkpoint has no dynamic_n_state "
|
|
1130
|
+
f"(pre-dates Dynamic-N, or was saved by a non-dynamic-N "
|
|
1131
|
+
f"run) -- usage EMA/cooldown/growth_history starting "
|
|
1132
|
+
f"fresh instead of resuming the prior leg's."
|
|
1133
|
+
)
|
|
1134
|
+
# v5 (Phase 2): restore (mu_g, Sigma_g, last_snapshot_step) for every
|
|
1135
|
+
# installed stochastic write head. Without this, resuming would
|
|
1136
|
+
# silently continue training/evaluating against whatever prior
|
|
1137
|
+
# snapshot happens to sit in each freshly-constructed head's
|
|
1138
|
+
# zero-init buffers (i.e. silently reset to N(0,I)) instead of the
|
|
1139
|
+
# snapshot this checkpoint was actually trained against -- the same
|
|
1140
|
+
# class of bug the write_mode deterministic/sampled mismatch check
|
|
1141
|
+
# already exists to catch in eval_from_checkpoint.py. `.get(...)`
|
|
1142
|
+
# instead of ["..."] so a pre-v5 (Phase 1) checkpoint, which has no
|
|
1143
|
+
# prior state to restore at all, still loads -- just starting the
|
|
1144
|
+
# learned prior fresh at N(0,I), same as a brand-new run would.
|
|
1145
|
+
if "prior_state" in ckpt:
|
|
1146
|
+
load_prior_state(stochastic_heads, ckpt["prior_state"])
|
|
1147
|
+
else:
|
|
1148
|
+
print(
|
|
1149
|
+
f"[{run_id}] WARNING: checkpoint predates the learned-prior "
|
|
1150
|
+
f"snapshot (v5/Phase 2) - prior starting fresh at N(0,I) "
|
|
1151
|
+
f"instead of resuming a prior snapshot."
|
|
1152
|
+
)
|
|
1153
|
+
# v6: restore the dedicated OOD-sampling RNG's state, so a resumed
|
|
1154
|
+
# run's OOD walk sequence continues from exactly where the
|
|
1155
|
+
# pre-resume leg left off, rather than silently restarting from
|
|
1156
|
+
# ood_rng's fresh, seed-derived starting state (see save_checkpoint()
|
|
1157
|
+
# docstring). `.get(...)` so a pre-v6 checkpoint still loads --
|
|
1158
|
+
# ood_rng just starts fresh in that case, identical to today's
|
|
1159
|
+
# behavior before this fix.
|
|
1160
|
+
if "ood_rng_state" in ckpt:
|
|
1161
|
+
ood_rng.setstate(ckpt["ood_rng_state"])
|
|
1162
|
+
else:
|
|
1163
|
+
print(
|
|
1164
|
+
f"[{run_id}] WARNING: checkpoint predates ood_rng-state "
|
|
1165
|
+
f"saving (v6 fix) -- OOD sampling restarting from its "
|
|
1166
|
+
f"fresh, seed-derived state instead of continuing the "
|
|
1167
|
+
f"pre-resume leg's exact walk sequence."
|
|
1168
|
+
)
|
|
1169
|
+
restore_rng_state(ckpt["rng_state"])
|
|
1170
|
+
step = ckpt["step"]
|
|
1171
|
+
if ckpt["beta_target"] != beta_target:
|
|
1172
|
+
anneal_start_step = step # Phase 1: restart the ramp here
|
|
1173
|
+
print(
|
|
1174
|
+
f"[{run_id}] beta_target changed on resume "
|
|
1175
|
+
f"({ckpt['beta_target']} -> {beta_target}); KL anneal "
|
|
1176
|
+
f"restarted from step {anneal_start_step}, ramping over "
|
|
1177
|
+
f"the next {KL_ANNEAL_STEPS} steps."
|
|
1178
|
+
)
|
|
1179
|
+
print(
|
|
1180
|
+
f"[{run_id}] Resumed from {resume_from} at step {step} "
|
|
1181
|
+
f"(lesson {curriculum.lesson + 1}/{len(curriculum.table)}); "
|
|
1182
|
+
f"continuing to total_steps={total_steps}"
|
|
1183
|
+
)
|
|
1184
|
+
if step >= total_steps:
|
|
1185
|
+
print(
|
|
1186
|
+
f"[{run_id}] Checkpoint step {step} already >= total_steps "
|
|
1187
|
+
f"{total_steps} -- nothing to do. Raise --total-steps if you "
|
|
1188
|
+
f"want to extend further."
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
print(
|
|
1192
|
+
f"\n=== [{run_id}] Training (beta_target={beta_target}, beta_mode={beta_mode}) "
|
|
1193
|
+
f"{'[resumed]' if resuming else ''} ==="
|
|
1194
|
+
)
|
|
1195
|
+
rnn.train()
|
|
1196
|
+
set_lr(step) # Phase 1 FIX: ensure correct clamped LR from the very first
|
|
1197
|
+
# iteration -- otherwise a resumed run's first step would briefly use
|
|
1198
|
+
# whatever LR was saved inside optimizer_state_dict at checkpoint time
|
|
1199
|
+
# (potentially still reflecting the old buggy drifted value).
|
|
1200
|
+
running_task_loss, running_kl_loss, running_div, running_grad_norm = 0.0, 0.0, 0, 0.0
|
|
1201
|
+
|
|
1202
|
+
# Dynamic-beta controller state (no-op / unused when beta_mode == "static").
|
|
1203
|
+
# Initialized post-resume so a resumed dynamic run starts its health check
|
|
1204
|
+
# from the just-restored scaler scale rather than a fresh-process default.
|
|
1205
|
+
beta_constraint_ema = 0.0
|
|
1206
|
+
scale_at_last_eval = scaler.get_scale()
|
|
1207
|
+
grad_was_finite_since_eval = True
|
|
1208
|
+
|
|
1209
|
+
t0 = time.time()
|
|
1210
|
+
|
|
1211
|
+
if torch.cuda.is_available():
|
|
1212
|
+
torch.cuda.reset_peak_memory_stats(device)
|
|
1213
|
+
|
|
1214
|
+
# Log addition #5: cumulative wall-clock for this run(). Starts fresh on
|
|
1215
|
+
# a resumed leg too (i.e. measures THIS process's elapsed time, not
|
|
1216
|
+
# elapsed time since the original run began across all resumed legs --
|
|
1217
|
+
# legs already have their own timestamped log rows if that's needed).
|
|
1218
|
+
t_run_start = time.time()
|
|
1219
|
+
|
|
1220
|
+
while step < total_steps:
|
|
1221
|
+
input_seq, target_digits, answer_mask = dataset.sample_batch(curriculum, BATCH_SIZE)
|
|
1222
|
+
|
|
1223
|
+
input_seq = input_seq.to(device, non_blocking=True)
|
|
1224
|
+
target_digits = target_digits.to(device, non_blocking=True)
|
|
1225
|
+
answer_mask = answer_mask.to(device, non_blocking=True)
|
|
1226
|
+
_first_nonfinite_report(input_seq, "input_seq", step)
|
|
1227
|
+
hidden = (None, None, None)
|
|
1228
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1229
|
+
|
|
1230
|
+
with torch.amp.autocast("cuda", enabled=amp_enabled):
|
|
1231
|
+
output, hidden = rnn(input_seq, hidden, reset_experience=True)
|
|
1232
|
+
_first_nonfinite_report(output, "rnn_output", step)
|
|
1233
|
+
output = output.transpose(0, 1).contiguous() # (B, T, 92)
|
|
1234
|
+
output = output_proj(output) # (B, T, 90)
|
|
1235
|
+
task_loss = dataset.loss(output, target_digits, answer_mask)
|
|
1236
|
+
|
|
1237
|
+
_first_nonfinite_report(task_loss, "task_loss", step)
|
|
1238
|
+
kl_loss, kl_diag = pop_total_kl(stochastic_heads, free_bits=FREE_BITS) # Phase 1
|
|
1239
|
+
_first_nonfinite_report(kl_loss, "kl_loss", step)
|
|
1240
|
+
moe_aux_loss, moe_diag = (
|
|
1241
|
+
pop_total_moe_aux_loss(rnn.moe_layers)
|
|
1242
|
+
if moe_enabled
|
|
1243
|
+
else (torch.zeros((), device=device), {})
|
|
1244
|
+
) # v8 (Option 4): Switch-style load-balancing loss, already scaled
|
|
1245
|
+
# by moe_load_balance_alpha inside SwitchMoE.pop_aux_loss() -- no
|
|
1246
|
+
# extra weighting applied here, unlike beta_eff*kl_loss below.
|
|
1247
|
+
global_ramp = min(1.0, (step - anneal_start_step) / max(1, KL_ANNEAL_STEPS))
|
|
1248
|
+
lesson_dip_ramp = min(1.0, (step - lesson_dip_start_step) / max(1, LESSON_KL_DIP_STEPS))
|
|
1249
|
+
beta_eff = beta_target * global_ramp * lesson_dip_ramp
|
|
1250
|
+
loss = (
|
|
1251
|
+
task_loss + beta_eff * kl_loss + moe_aux_loss
|
|
1252
|
+
) # Phase 1 + Option 4: L = L_task + beta*L_KL + L_moe_aux
|
|
1253
|
+
scaler.scale(loss).backward()
|
|
1254
|
+
scaler.unscale_(optimizer)
|
|
1255
|
+
# Log addition #2: capture the pre-clip gradient norm instead of
|
|
1256
|
+
# discarding clip_grad_norm_'s return value. Gives a β=0 baseline
|
|
1257
|
+
# gradient-scale trajectory to compare a future β>0 switch-in
|
|
1258
|
+
# against, useful for telling a genuine instability apart from the
|
|
1259
|
+
# expected one-off "shock" when sampling noise is switched on.
|
|
1260
|
+
grad_norm = torch.nn.utils.clip_grad_norm_(rnn.parameters(), max_norm=10.0)
|
|
1261
|
+
if not math.isfinite(float(grad_norm)):
|
|
1262
|
+
grad_was_finite_since_eval = False
|
|
1263
|
+
# clip_grad_norm_ computes clip_coef = max_norm/(total_norm+eps)
|
|
1264
|
+
# and multiplies EVERY gradient by it. A NaN total_norm therefore
|
|
1265
|
+
# NaNs out every gradient in the model, including ones that were
|
|
1266
|
+
# finite. GradScaler's found_inf was recorded during unscale_,
|
|
1267
|
+
# BEFORE this clip, so it can be clean while the post-clip grads
|
|
1268
|
+
# are all NaN -- scaler.step() then applies them and the weights
|
|
1269
|
+
# are permanently poisoned, which is exactly the one-way collapse
|
|
1270
|
+
# at step 23700. Zero the grads and skip the step instead: this
|
|
1271
|
+
# costs one wasted batch and is fully recoverable.
|
|
1272
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1273
|
+
scaler.update()
|
|
1274
|
+
else:
|
|
1275
|
+
scaler.step(optimizer)
|
|
1276
|
+
scaler.update()
|
|
1277
|
+
# Log addition #3: AMP loss-scale value. A collapsing/repeatedly
|
|
1278
|
+
# halved scale is the standard AMP symptom of inf/NaN gradients --
|
|
1279
|
+
# the exp()/logvar path in the stochastic write head is the most
|
|
1280
|
+
# numerically fragile part of this pipeline, so a clean β=0 baseline
|
|
1281
|
+
# of scaler behavior is useful context once β>0 introduces that path.
|
|
1282
|
+
amp_scale = scaler.get_scale()
|
|
1283
|
+
running_task_loss += task_loss.item()
|
|
1284
|
+
running_kl_loss += kl_loss.item()
|
|
1285
|
+
running_div += dataset.diversity(output.detach(), target_digits, answer_mask)
|
|
1286
|
+
running_grad_norm += float(grad_norm)
|
|
1287
|
+
step += 1
|
|
1288
|
+
current_lr = set_lr(step) # Phase 1 FIX: clamped manual LR, replaces scheduler.step()
|
|
1289
|
+
|
|
1290
|
+
# Dynamic-N (usage-triggered), macro-scale: record this just-finished
|
|
1291
|
+
# episode's memory-usage saturation and, if the trigger has been
|
|
1292
|
+
# sustained past cooldown, resize nr_cells before the NEXT episode's
|
|
1293
|
+
# forward pass. Placed here -- after scaler.step()/scaler.update()
|
|
1294
|
+
# above, using `hidden` from the forward pass that just completed --
|
|
1295
|
+
# so a resize never touches a live autograd graph: this episode's
|
|
1296
|
+
# backward pass and optimizer step are both already done, and the
|
|
1297
|
+
# very next `hidden = (None, None, None)` at the top of the loop
|
|
1298
|
+
# means nothing carries the old size forward anyway (same "no
|
|
1299
|
+
# cross-episode memory content to preserve" fact static Option 2
|
|
1300
|
+
# relies on -- see dynamic_memory_resize.py's module docstring).
|
|
1301
|
+
# This record()/should_grow() split (see dynamic_n_controller.py) is
|
|
1302
|
+
# deliberately timing-agnostic -- it's what lets a future mid-episode
|
|
1303
|
+
# (micro-scale) variant reuse the same decision core fed from a
|
|
1304
|
+
# per-timestep usage reading instead, without redesigning the
|
|
1305
|
+
# EMA/cooldown logic.
|
|
1306
|
+
if dynamic_n_ctrl is not None:
|
|
1307
|
+
with torch.no_grad():
|
|
1308
|
+
# mem_hidden's shape differs by controller: MambaDNC's
|
|
1309
|
+
# _init_hidden returns mhx as a bare dict for rnn_type!=
|
|
1310
|
+
# 'mamba' (deferring to stock dnc.DNC._init_hidden, share_
|
|
1311
|
+
# memory_between_layers=True case), but wraps it in a
|
|
1312
|
+
# single-entry list for rnn_type=='mamba' (see
|
|
1313
|
+
# mamba_controller.py's _init_hidden memory-state branch,
|
|
1314
|
+
# which is otherwise byte-identical to upstream). Both cases
|
|
1315
|
+
# are layer 0 of a single shared Memory in this project, so
|
|
1316
|
+
# this just picks the right unwrap for whichever controller
|
|
1317
|
+
# is active rather than hardcoding one shape.
|
|
1318
|
+
mem_hidden = hidden[1]
|
|
1319
|
+
mhx = mem_hidden[0] if isinstance(mem_hidden, list) else mem_hidden
|
|
1320
|
+
usage = mhx["usage_vector"].float()
|
|
1321
|
+
frac_saturated = (usage > DYNAMIC_N_USAGE_HIGH).float().mean().item()
|
|
1322
|
+
dynamic_n_ctrl.record(frac_saturated)
|
|
1323
|
+
last_frac_saturated = frac_saturated
|
|
1324
|
+
current_nr_cells = rnn.memories[0].nr_cells
|
|
1325
|
+
new_n = dynamic_n_ctrl.should_grow(current_nr_cells)
|
|
1326
|
+
if new_n is not None:
|
|
1327
|
+
ema_at_trigger = dynamic_n_ctrl.ema # captured before mark_grown() resets it to 0.0
|
|
1328
|
+
resize_memory(rnn, new_n, device=device, optimizer=optimizer)
|
|
1329
|
+
dynamic_n_ctrl.mark_grown(step, current_nr_cells, new_n)
|
|
1330
|
+
print(
|
|
1331
|
+
f"[{run_id}] Step {step} Dynamic-N growth: nr_cells "
|
|
1332
|
+
f"{current_nr_cells} -> {new_n} (usage-saturation EMA "
|
|
1333
|
+
f"{ema_at_trigger:.3f} had reached the "
|
|
1334
|
+
f"{dynamic_n_ctrl.trigger_frac:.2f} trigger, sustained "
|
|
1335
|
+
f"past cooldown)"
|
|
1336
|
+
)
|
|
1337
|
+
assert dynamic_n_log_writer is not None and dynamic_n_log_file is not None
|
|
1338
|
+
dynamic_n_log_writer.writerow(
|
|
1339
|
+
[
|
|
1340
|
+
step,
|
|
1341
|
+
curriculum.lesson + 1,
|
|
1342
|
+
current_nr_cells,
|
|
1343
|
+
new_n,
|
|
1344
|
+
ema_at_trigger,
|
|
1345
|
+
]
|
|
1346
|
+
)
|
|
1347
|
+
dynamic_n_log_file.flush()
|
|
1348
|
+
|
|
1349
|
+
if step % LOG_EVERY == 0:
|
|
1350
|
+
elapsed = time.time() - t0
|
|
1351
|
+
t0 = time.time()
|
|
1352
|
+
total_elapsed = time.time() - t_run_start # log addition #5
|
|
1353
|
+
avg_task = running_task_loss / LOG_EVERY
|
|
1354
|
+
avg_kl = running_kl_loss / LOG_EVERY
|
|
1355
|
+
avg_div = running_div / LOG_EVERY
|
|
1356
|
+
avg_grad_norm = running_grad_norm / LOG_EVERY
|
|
1357
|
+
kl_contrib = (
|
|
1358
|
+
beta_eff * avg_kl
|
|
1359
|
+
) # actual beta*L_KL added to total loss, vs. avg_kl (pre-beta, raw clamped sum)
|
|
1360
|
+
gpu_mem_peak_mb = (
|
|
1361
|
+
torch.cuda.max_memory_allocated(device) / 1e6 if torch.cuda.is_available() else 0.0
|
|
1362
|
+
)
|
|
1363
|
+
dyn_n_str = ""
|
|
1364
|
+
if dynamic_n_ctrl is not None:
|
|
1365
|
+
dyn_n_str = (
|
|
1366
|
+
f"| dynN[nr_cells {rnn.memories[0].nr_cells} frac_sat {last_frac_saturated:.3f} "
|
|
1367
|
+
f"ema {dynamic_n_ctrl.ema:.3f} cooldown {dynamic_n_ctrl.cooldown_remaining}] "
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
parts = [
|
|
1371
|
+
f"[{run_id}] Step {step}/{total_steps} | Lesson {curriculum.lesson + 1}/{len(curriculum.table)}",
|
|
1372
|
+
f"L_task {avg_task:.4f}",
|
|
1373
|
+
]
|
|
1374
|
+
if kl_on:
|
|
1375
|
+
parts += [
|
|
1376
|
+
f"L_KL {avg_kl:.4f}",
|
|
1377
|
+
f"beta {beta_eff:.4f}",
|
|
1378
|
+
f"KL_contrib {kl_contrib:.4f}",
|
|
1379
|
+
f"KL[mean {kl_diag['kl_mean']:.4f} max {kl_diag['kl_max']:.4f}]",
|
|
1380
|
+
f"clamp_frac {kl_diag['clamp_frac']:.4f}",
|
|
1381
|
+
f"floor_frac {kl_diag['floor_frac']:.4f}",
|
|
1382
|
+
f"snapshot_step {kl_diag['snapshot_step']}",
|
|
1383
|
+
]
|
|
1384
|
+
parts += [
|
|
1385
|
+
f"diversity {avg_div:.2f}",
|
|
1386
|
+
f"{LOG_EVERY / elapsed:.2f} steps/s",
|
|
1387
|
+
f"LR {current_lr:.6f}",
|
|
1388
|
+
f"grad_norm {avg_grad_norm:.4f}",
|
|
1389
|
+
]
|
|
1390
|
+
if amp_enabled:
|
|
1391
|
+
parts.append(f"amp_scale {amp_scale:.1f}")
|
|
1392
|
+
parts.append(f"gpu_mem_peak_mb {gpu_mem_peak_mb:.1f}")
|
|
1393
|
+
if moe_enabled:
|
|
1394
|
+
parts += [
|
|
1395
|
+
f"moe_aux {float(moe_aux_loss.detach()):.4f}",
|
|
1396
|
+
f"moe_cv_load {moe_diag.get('moe_cv_load', 0.0):.4f}",
|
|
1397
|
+
f"moe_max_load_frac {moe_diag.get('moe_max_load_frac', 0.0):.4f}",
|
|
1398
|
+
]
|
|
1399
|
+
if dyn_n_str:
|
|
1400
|
+
parts.append(dyn_n_str.strip(" |"))
|
|
1401
|
+
print(" | ".join(parts))
|
|
1402
|
+
|
|
1403
|
+
log_writer.writerow(
|
|
1404
|
+
[
|
|
1405
|
+
step,
|
|
1406
|
+
curriculum.lesson + 1,
|
|
1407
|
+
beta_eff,
|
|
1408
|
+
avg_task,
|
|
1409
|
+
avg_kl,
|
|
1410
|
+
kl_contrib,
|
|
1411
|
+
avg_task + kl_contrib,
|
|
1412
|
+
avg_div,
|
|
1413
|
+
kl_diag["kl_mean"],
|
|
1414
|
+
kl_diag["kl_max"],
|
|
1415
|
+
kl_diag["kl_min"],
|
|
1416
|
+
kl_diag["kl_std"],
|
|
1417
|
+
kl_diag["clamp_frac"],
|
|
1418
|
+
current_lr,
|
|
1419
|
+
avg_grad_norm,
|
|
1420
|
+
amp_scale,
|
|
1421
|
+
total_elapsed,
|
|
1422
|
+
kl_diag["snapshot_step"],
|
|
1423
|
+
gpu_mem_peak_mb,
|
|
1424
|
+
param_count,
|
|
1425
|
+
float(moe_aux_loss.detach()),
|
|
1426
|
+
moe_diag.get("moe_cv_importance", 0.0),
|
|
1427
|
+
moe_diag.get("moe_cv_load", 0.0),
|
|
1428
|
+
moe_diag.get("moe_max_load_frac", 0.0),
|
|
1429
|
+
]
|
|
1430
|
+
)
|
|
1431
|
+
if torch.cuda.is_available():
|
|
1432
|
+
torch.cuda.reset_peak_memory_stats(device) # so next window's peak isn't cumulative
|
|
1433
|
+
log_file.flush()
|
|
1434
|
+
running_task_loss, running_kl_loss, running_div, running_grad_norm = 0.0, 0.0, 0, 0.0
|
|
1435
|
+
|
|
1436
|
+
if kl_on and step % PRIOR_SNAPSHOT_EVERY == 0:
|
|
1437
|
+
# v5 (Phase 2): refit (mu_g, Sigma_g) from the writes
|
|
1438
|
+
# accumulated since the last snapshot, freeze the result into
|
|
1439
|
+
# every stochastic write head's buffers, and log a summary.
|
|
1440
|
+
# This call -- and only this call -- is where the prior's
|
|
1441
|
+
# non-locality lives (see stochastic_write_head.py v2,
|
|
1442
|
+
# update_prior_snapshot); it happens here, outside the
|
|
1443
|
+
# per-step backward pass above, never inside it. This is a
|
|
1444
|
+
# brand-new console line and a brand-new log file -- it does
|
|
1445
|
+
# not touch the per-step console line above, or any other
|
|
1446
|
+
# existing print/log call in this loop.
|
|
1447
|
+
snap_diag = update_all_prior_snapshots(
|
|
1448
|
+
stochastic_heads,
|
|
1449
|
+
step,
|
|
1450
|
+
min_logvar=PRIOR_MIN_LOGVAR,
|
|
1451
|
+
max_logvar=PRIOR_MAX_LOGVAR,
|
|
1452
|
+
)
|
|
1453
|
+
print(
|
|
1454
|
+
f"[{run_id}] Step {step} prior snapshot updated | "
|
|
1455
|
+
f"||mu_g|| {snap_diag['mu_g_norm']:.4f} | "
|
|
1456
|
+
f"diag(Sigma_g) mean/min/max {snap_diag['sigma_g_mean']:.4f}/"
|
|
1457
|
+
f"{snap_diag['sigma_g_min']:.4f}/{snap_diag['sigma_g_max']:.4f} | "
|
|
1458
|
+
f"trace(Sigma_g) {snap_diag['trace_sigma_g']:.4f} | "
|
|
1459
|
+
f"n_samples {snap_diag['n_samples']} "
|
|
1460
|
+
f" | raw_var mean/max {snap_diag['raw_var_mean']:.3f}/{snap_diag['raw_var_max']:.3f} hi_frac {snap_diag['raw_hi_frac']:.2f}"
|
|
1461
|
+
)
|
|
1462
|
+
prior_log_writer.writerow(
|
|
1463
|
+
[
|
|
1464
|
+
step,
|
|
1465
|
+
snap_diag["snapshot_step"],
|
|
1466
|
+
snap_diag["mu_g_norm"],
|
|
1467
|
+
snap_diag["sigma_g_mean"],
|
|
1468
|
+
snap_diag["sigma_g_min"],
|
|
1469
|
+
snap_diag["sigma_g_max"],
|
|
1470
|
+
snap_diag["trace_sigma_g"],
|
|
1471
|
+
snap_diag["n_samples"],
|
|
1472
|
+
snap_diag["raw_var_mean"],
|
|
1473
|
+
snap_diag["raw_var_max"],
|
|
1474
|
+
snap_diag["raw_hi_frac"],
|
|
1475
|
+
]
|
|
1476
|
+
)
|
|
1477
|
+
prior_log_file.flush()
|
|
1478
|
+
|
|
1479
|
+
if step % EVAL_EVERY == 0:
|
|
1480
|
+
pre_advance_lesson = curriculum.lesson # capture before maybe_advance can bump it
|
|
1481
|
+
_, id_triple_acc, id_perfect_frac = curriculum.maybe_advance(
|
|
1482
|
+
rnn, device, step=step, optimizer=optimizer
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
if curriculum.lesson != pre_advance_lesson:
|
|
1486
|
+
lesson_dip_start_step = step
|
|
1487
|
+
if kl_on:
|
|
1488
|
+
print(
|
|
1489
|
+
f"[{run_id}] Step {step} lesson advance {pre_advance_lesson + 1}->"
|
|
1490
|
+
f"{curriculum.lesson + 1}: KL briefly dipping and ramping back up "
|
|
1491
|
+
f"over the next {LESSON_KL_DIP_STEPS} steps (global anneal unaffected)."
|
|
1492
|
+
)
|
|
1493
|
+
if beta_mode == "dynamic":
|
|
1494
|
+
# Hard safety ceiling -- enforced every eval cycle unconditionally,
|
|
1495
|
+
# independent of the health gate below. Without this, starting
|
|
1496
|
+
# beta_target above [BETA_CTRL_MIN, BETA_CTRL_MAX] (e.g. testing
|
|
1497
|
+
# 0.001) could ride out the whole KL_ANNEAL_STEPS window unclamped
|
|
1498
|
+
# if early-training grad instability keeps failing the health
|
|
1499
|
+
# check on every eval cycle -- i.e. behave identically to static
|
|
1500
|
+
# mode at the known-collapse value, silently. This is a clamp,
|
|
1501
|
+
# not a rate-limited step: it can move beta_target more than
|
|
1502
|
+
# BETA_CTRL_LR in one cycle, on purpose.
|
|
1503
|
+
if not (BETA_CTRL_MIN <= beta_target <= BETA_CTRL_MAX):
|
|
1504
|
+
clamped = float(np.clip(beta_target, BETA_CTRL_MIN, BETA_CTRL_MAX))
|
|
1505
|
+
print(
|
|
1506
|
+
f"[{run_id}] Step {step} beta_target {beta_target:.6f} outside "
|
|
1507
|
+
f"[{BETA_CTRL_MIN}, {BETA_CTRL_MAX}] -- hard-clamping to {clamped:.6f} "
|
|
1508
|
+
f"(unconditional, ignores health gate below)"
|
|
1509
|
+
)
|
|
1510
|
+
beta_target = clamped
|
|
1511
|
+
|
|
1512
|
+
scale_backed_off = scaler.get_scale() < scale_at_last_eval
|
|
1513
|
+
healthy = grad_was_finite_since_eval and not scale_backed_off
|
|
1514
|
+
if healthy:
|
|
1515
|
+
acc_frac = id_triple_acc / 100.0
|
|
1516
|
+
raw_constraint = acc_frac - beta_ctrl_acc_target
|
|
1517
|
+
beta_constraint_ema = (
|
|
1518
|
+
BETA_CTRL_EMA_DECAY * beta_constraint_ema
|
|
1519
|
+
+ (1 - BETA_CTRL_EMA_DECAY) * raw_constraint
|
|
1520
|
+
)
|
|
1521
|
+
beta_target = float(
|
|
1522
|
+
np.clip(
|
|
1523
|
+
beta_target + BETA_CTRL_LR * beta_constraint_ema,
|
|
1524
|
+
BETA_CTRL_MIN,
|
|
1525
|
+
BETA_CTRL_MAX,
|
|
1526
|
+
)
|
|
1527
|
+
)
|
|
1528
|
+
print(
|
|
1529
|
+
f"[{run_id}] Step {step} dynamic-beta update | "
|
|
1530
|
+
f"acc {acc_frac:.4f} target {beta_ctrl_acc_target:.4f} | "
|
|
1531
|
+
f"constraint_ema {beta_constraint_ema:.4f} | beta -> {beta_target:.6f}"
|
|
1532
|
+
)
|
|
1533
|
+
else:
|
|
1534
|
+
print(
|
|
1535
|
+
f"[{run_id}] Step {step} dynamic-beta update SKIPPED "
|
|
1536
|
+
f"(grad_finite={grad_was_finite_since_eval}, "
|
|
1537
|
+
f"scale_backoff={scale_backed_off}) | beta stays {beta_target:.6f}"
|
|
1538
|
+
)
|
|
1539
|
+
scale_at_last_eval = scaler.get_scale()
|
|
1540
|
+
grad_was_finite_since_eval = True
|
|
1541
|
+
|
|
1542
|
+
ood_field_log: dict[tuple[int, int], list[int]] = {}
|
|
1543
|
+
ood_triple_acc, ood_perfect_frac, ood_hop_breakdown = dataset.evaluate_ood(
|
|
1544
|
+
rnn,
|
|
1545
|
+
device,
|
|
1546
|
+
num_episodes=OOD_EVAL_EPISODES_PERIODIC,
|
|
1547
|
+
rng=ood_rng,
|
|
1548
|
+
field_log=ood_field_log,
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
ood_log_writer.writerow(
|
|
1552
|
+
[
|
|
1553
|
+
step,
|
|
1554
|
+
curriculum.lesson + 1,
|
|
1555
|
+
id_triple_acc,
|
|
1556
|
+
id_perfect_frac,
|
|
1557
|
+
ood_triple_acc,
|
|
1558
|
+
ood_perfect_frac,
|
|
1559
|
+
id_triple_acc - ood_triple_acc,
|
|
1560
|
+
id_perfect_frac - ood_perfect_frac,
|
|
1561
|
+
]
|
|
1562
|
+
)
|
|
1563
|
+
ood_log_file.flush()
|
|
1564
|
+
dataset.write_field_log(
|
|
1565
|
+
field_log_writer, field_log_file, step, curriculum.lesson + 1, "ood", ood_field_log
|
|
1566
|
+
)
|
|
1567
|
+
print(
|
|
1568
|
+
f"[{run_id}] Step {step} periodic OOD check: "
|
|
1569
|
+
f"ID {id_triple_acc:.2f}% | OOD {ood_triple_acc:.2f}% | "
|
|
1570
|
+
f"offset {id_triple_acc - ood_triple_acc:.2f}"
|
|
1571
|
+
)
|
|
1572
|
+
for hops, (acc, pf, n) in sorted(ood_hop_breakdown.items()):
|
|
1573
|
+
hop_log_writer.writerow([step, curriculum.lesson + 1, "ood", hops, acc, pf, n])
|
|
1574
|
+
hop_log_file.flush()
|
|
1575
|
+
|
|
1576
|
+
# Functional-usage check: same lesson distribution the ID eval
|
|
1577
|
+
# above just used (pre_advance_lesson, not curriculum.lesson --
|
|
1578
|
+
# this call may have just advanced it).
|
|
1579
|
+
ablated_triple_acc, ablated_perfect_frac = dataset.evaluate_id_ablated(
|
|
1580
|
+
rnn, device, curriculum, pre_advance_lesson
|
|
1581
|
+
)
|
|
1582
|
+
|
|
1583
|
+
mem_check_writer.writerow(
|
|
1584
|
+
[
|
|
1585
|
+
step,
|
|
1586
|
+
pre_advance_lesson + 1,
|
|
1587
|
+
id_triple_acc,
|
|
1588
|
+
id_perfect_frac,
|
|
1589
|
+
ablated_triple_acc,
|
|
1590
|
+
ablated_perfect_frac,
|
|
1591
|
+
id_triple_acc - ablated_triple_acc,
|
|
1592
|
+
id_perfect_frac - ablated_perfect_frac,
|
|
1593
|
+
]
|
|
1594
|
+
)
|
|
1595
|
+
mem_check_file.flush()
|
|
1596
|
+
print(
|
|
1597
|
+
f"[{run_id}] Step {step} memory-dependency check: "
|
|
1598
|
+
f"ID (memory on) {id_triple_acc:.2f}% | ablated (memory off) "
|
|
1599
|
+
f"{ablated_triple_acc:.2f}% | dependency {id_triple_acc - ablated_triple_acc:.2f}"
|
|
1600
|
+
)
|
|
1601
|
+
|
|
1602
|
+
if combiner_stage_kinds is not None:
|
|
1603
|
+
# Same pattern as the memory-dependency check above, but
|
|
1604
|
+
# ablating one combiner stage (e.g. the CfC block in a
|
|
1605
|
+
# "mamba+cfc" combiner) at a time, so each stage's actual
|
|
1606
|
+
# contribution to accuracy is measured directly instead of
|
|
1607
|
+
# inferred from loss curves / plateau shape.
|
|
1608
|
+
for stage_idx, stage_kind in enumerate(combiner_stage_kinds):
|
|
1609
|
+
stage_triple_acc, stage_perfect_frac = (
|
|
1610
|
+
dataset.evaluate_id_combiner_stage_ablated(
|
|
1611
|
+
rnn, device, curriculum, pre_advance_lesson, skip_stages={stage_idx}
|
|
1612
|
+
)
|
|
1613
|
+
)
|
|
1614
|
+
combiner_stage_log_writer.writerow(
|
|
1615
|
+
[
|
|
1616
|
+
step,
|
|
1617
|
+
pre_advance_lesson + 1,
|
|
1618
|
+
stage_idx,
|
|
1619
|
+
stage_kind,
|
|
1620
|
+
id_triple_acc,
|
|
1621
|
+
id_perfect_frac,
|
|
1622
|
+
stage_triple_acc,
|
|
1623
|
+
stage_perfect_frac,
|
|
1624
|
+
id_triple_acc - stage_triple_acc,
|
|
1625
|
+
id_perfect_frac - stage_perfect_frac,
|
|
1626
|
+
]
|
|
1627
|
+
)
|
|
1628
|
+
combiner_stage_log_file.flush()
|
|
1629
|
+
print(
|
|
1630
|
+
f"[{run_id}] Step {step} combiner-stage-dependency check "
|
|
1631
|
+
f"[{stage_idx}:{stage_kind}]: ID (full) {id_triple_acc:.2f}% | "
|
|
1632
|
+
f"ablated ({stage_kind} off) {stage_triple_acc:.2f}% | "
|
|
1633
|
+
f"dependency {id_triple_acc - stage_triple_acc:.2f}"
|
|
1634
|
+
)
|
|
1635
|
+
|
|
1636
|
+
if getattr(dataset, "modalities", None):
|
|
1637
|
+
for modality in dataset.modalities:
|
|
1638
|
+
m_acc, m_pf = dataset.evaluate_modality_ablated(
|
|
1639
|
+
rnn, device, curriculum, pre_advance_lesson, modality
|
|
1640
|
+
)
|
|
1641
|
+
modality_log_writer.writerow(
|
|
1642
|
+
[
|
|
1643
|
+
step,
|
|
1644
|
+
pre_advance_lesson + 1,
|
|
1645
|
+
modality,
|
|
1646
|
+
id_triple_acc,
|
|
1647
|
+
id_perfect_frac,
|
|
1648
|
+
m_acc,
|
|
1649
|
+
m_pf,
|
|
1650
|
+
id_triple_acc - m_acc,
|
|
1651
|
+
id_perfect_frac - m_pf,
|
|
1652
|
+
]
|
|
1653
|
+
)
|
|
1654
|
+
modality_log_file.flush()
|
|
1655
|
+
print(
|
|
1656
|
+
f"[{run_id}] Step {step} modality-dependency check "
|
|
1657
|
+
f"[{modality}]: full {id_triple_acc:.2f}% | ablated ({modality} off) "
|
|
1658
|
+
f"{m_acc:.2f}% | dependency {id_triple_acc - m_acc:.2f}"
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
if step % checkpoint_every == 0 or step == total_steps:
|
|
1662
|
+
ckpt_path = os.path.join(CHECKPOINT_DIR, f"{run_id}_step{step}.pt")
|
|
1663
|
+
save_checkpoint(
|
|
1664
|
+
ckpt_path,
|
|
1665
|
+
rnn,
|
|
1666
|
+
output_proj,
|
|
1667
|
+
stochastic_heads,
|
|
1668
|
+
optimizer,
|
|
1669
|
+
curriculum,
|
|
1670
|
+
step,
|
|
1671
|
+
beta_target,
|
|
1672
|
+
run_id,
|
|
1673
|
+
scaler,
|
|
1674
|
+
ood_rng,
|
|
1675
|
+
controller_type=controller,
|
|
1676
|
+
link_matrix_mode=link_matrix_mode,
|
|
1677
|
+
link_matrix_topk=link_matrix_topk,
|
|
1678
|
+
dynamic_n_mode=dynamic_n_mode,
|
|
1679
|
+
dynamic_n_state=(
|
|
1680
|
+
dynamic_n_ctrl.state_dict() if dynamic_n_ctrl is not None else None
|
|
1681
|
+
),
|
|
1682
|
+
moe_enabled=moe_enabled,
|
|
1683
|
+
moe_num_experts=moe_num_experts,
|
|
1684
|
+
moe_expert_dim=moe_expert_dim,
|
|
1685
|
+
moe_capacity_factor=moe_capacity_factor,
|
|
1686
|
+
moe_load_balance_alpha=moe_load_balance_alpha,
|
|
1687
|
+
moe_top_k=moe_top_k,
|
|
1688
|
+
split_graph_enabled=split_graph_enabled,
|
|
1689
|
+
split_graph_variant=split_graph_variant,
|
|
1690
|
+
extra_model_config=extra_model_config,
|
|
1691
|
+
)
|
|
1692
|
+
latest_path = os.path.join(CHECKPOINT_DIR, f"{run_id}_latest.pt")
|
|
1693
|
+
shutil.copyfile(ckpt_path, latest_path)
|
|
1694
|
+
|
|
1695
|
+
print(f"\n[{run_id}] Training complete. Final evaluation on training-distribution lesson:")
|
|
1696
|
+
_, id_triple_acc, id_perfect_frac = curriculum.maybe_advance(
|
|
1697
|
+
rnn, device, step=step, optimizer=optimizer
|
|
1698
|
+
)
|
|
1699
|
+
|
|
1700
|
+
print(f"\n[{run_id}] Generalization test:")
|
|
1701
|
+
|
|
1702
|
+
ood_triple_acc, ood_perfect_frac, ood_hop_breakdown = dataset.evaluate_ood(
|
|
1703
|
+
rnn,
|
|
1704
|
+
device,
|
|
1705
|
+
num_episodes=OOD_EVAL_EPISODES,
|
|
1706
|
+
rng=ood_rng,
|
|
1707
|
+
verbose_n=10,
|
|
1708
|
+
)
|
|
1709
|
+
|
|
1710
|
+
ood_offset_triple = id_triple_acc - ood_triple_acc
|
|
1711
|
+
ood_offset_perfect = id_perfect_frac - ood_perfect_frac
|
|
1712
|
+
|
|
1713
|
+
ood_log_writer.writerow(
|
|
1714
|
+
[
|
|
1715
|
+
step,
|
|
1716
|
+
curriculum.lesson + 1,
|
|
1717
|
+
id_triple_acc,
|
|
1718
|
+
id_perfect_frac,
|
|
1719
|
+
ood_triple_acc,
|
|
1720
|
+
ood_perfect_frac,
|
|
1721
|
+
ood_offset_triple,
|
|
1722
|
+
ood_offset_perfect,
|
|
1723
|
+
]
|
|
1724
|
+
)
|
|
1725
|
+
ood_log_file.flush()
|
|
1726
|
+
|
|
1727
|
+
for hops, (acc, pf, n) in sorted(ood_hop_breakdown.items()):
|
|
1728
|
+
hop_log_writer.writerow([step, curriculum.lesson + 1, "ood", hops, acc, pf, n])
|
|
1729
|
+
hop_log_file.flush()
|
|
1730
|
+
|
|
1731
|
+
summary = {
|
|
1732
|
+
"run_id": run_id,
|
|
1733
|
+
"beta_target": beta_target,
|
|
1734
|
+
"id_triple_acc": id_triple_acc,
|
|
1735
|
+
"id_perfect_frac": id_perfect_frac,
|
|
1736
|
+
"ood_triple_acc": ood_triple_acc,
|
|
1737
|
+
"ood_perfect_frac": ood_perfect_frac,
|
|
1738
|
+
"ood_offset_triple": ood_offset_triple,
|
|
1739
|
+
"ood_offset_perfect": ood_offset_perfect,
|
|
1740
|
+
"total_elapsed_sec": time.time() - t_run_start,
|
|
1741
|
+
}
|
|
1742
|
+
print(f"\n[{run_id}] SUMMARY: {summary}")
|
|
1743
|
+
|
|
1744
|
+
log_writer.writerow([])
|
|
1745
|
+
log_writer.writerow(["SUMMARY"] + list(summary.keys()))
|
|
1746
|
+
log_writer.writerow([""] + list(summary.values()))
|
|
1747
|
+
log_file.close()
|
|
1748
|
+
ood_log_file.close()
|
|
1749
|
+
lesson_log_file.close()
|
|
1750
|
+
prior_log_file.close()
|
|
1751
|
+
field_log_file.close() # v16
|
|
1752
|
+
combiner_stage_log_file.close()
|
|
1753
|
+
modality_log_file.close()
|
|
1754
|
+
if dynamic_n_ctrl is not None:
|
|
1755
|
+
dynamic_n_log_file.close()
|
|
1756
|
+
|
|
1757
|
+
return summary
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
# ==========================================
|
|
1761
|
+
# 11. GATE
|
|
1762
|
+
# ==========================================
|
|
1763
|
+
if __name__ == "__main__":
|
|
1764
|
+
import argparse
|
|
1765
|
+
import sys
|
|
1766
|
+
|
|
1767
|
+
parser = argparse.ArgumentParser(
|
|
1768
|
+
description="Phase 2 KL sweep (learned, periodically-snapshotted "
|
|
1769
|
+
"prior). Positional beta for single-beta mode; "
|
|
1770
|
+
"no beta runs the full BETAS_TO_SWEEP list sequentially."
|
|
1771
|
+
)
|
|
1772
|
+
parser.add_argument(
|
|
1773
|
+
"beta",
|
|
1774
|
+
nargs="?",
|
|
1775
|
+
type=float,
|
|
1776
|
+
default=None,
|
|
1777
|
+
help=f"beta to run, one of {BETAS_TO_SWEEP}. Omit to run the full sweep sequentially.",
|
|
1778
|
+
)
|
|
1779
|
+
parser.add_argument(
|
|
1780
|
+
"--resume",
|
|
1781
|
+
type=str,
|
|
1782
|
+
default=None,
|
|
1783
|
+
help="path to a checkpoint .pt to resume from (requires --beta / positional beta "
|
|
1784
|
+
"to also be given, so run_id/beta_target can be matched against the checkpoint). "
|
|
1785
|
+
"Training continues from the checkpoint's step up to TOTAL_STEPS.",
|
|
1786
|
+
)
|
|
1787
|
+
parser.add_argument(
|
|
1788
|
+
"--run-id-suffix",
|
|
1789
|
+
type=str,
|
|
1790
|
+
default=None,
|
|
1791
|
+
help="appended to the derived run_id (e.g. 'switch') so this run's checkpoints "
|
|
1792
|
+
"(checkpoints/beta_XXX<suffix>_stepN.pt) and log "
|
|
1793
|
+
"(logs/run_beta_XXX<suffix>.csv) don't collide with an existing run "
|
|
1794
|
+
"that used the same beta value -- e.g. resuming a beta=0 checkpoint into a "
|
|
1795
|
+
"beta=0.01 run would otherwise reuse the same run_id/files as a prior plain "
|
|
1796
|
+
"beta=0.01 sweep run.",
|
|
1797
|
+
)
|
|
1798
|
+
parser.add_argument(
|
|
1799
|
+
"--seed",
|
|
1800
|
+
type=int,
|
|
1801
|
+
default=SEED,
|
|
1802
|
+
help="random seed for torch/random/numpy (default: SEED module constant)",
|
|
1803
|
+
)
|
|
1804
|
+
parser.add_argument(
|
|
1805
|
+
"--controller",
|
|
1806
|
+
type=str,
|
|
1807
|
+
default=CONTROLLER_TYPE,
|
|
1808
|
+
choices=[
|
|
1809
|
+
"lstm",
|
|
1810
|
+
"mamba",
|
|
1811
|
+
"mamba2",
|
|
1812
|
+
"mamba3",
|
|
1813
|
+
"cfc",
|
|
1814
|
+
"mamba+cfc",
|
|
1815
|
+
"mamba2+cfc",
|
|
1816
|
+
"mamba3+cfc",
|
|
1817
|
+
],
|
|
1818
|
+
help="DNC controller type. 'lstm' (default) "
|
|
1819
|
+
"reproduces Phase 2 exactly. 'mamba' swaps in the "
|
|
1820
|
+
"Mamba-1 controller from mamba_controller.py. "
|
|
1821
|
+
"'mamba2' swaps in the standalone, "
|
|
1822
|
+
"per-timestep Mamba-2 controller from "
|
|
1823
|
+
"mamba_controller/mamba2_controller.py - distinct "
|
|
1824
|
+
"from --split-graph-variant=mamba2, which is a "
|
|
1825
|
+
"parallel whole-sequence backbone, not an "
|
|
1826
|
+
"interleaved controller. Both require mamba-ssm.",
|
|
1827
|
+
)
|
|
1828
|
+
parser.add_argument(
|
|
1829
|
+
"--beta-mode",
|
|
1830
|
+
type=str,
|
|
1831
|
+
default=BETA_MODE,
|
|
1832
|
+
choices=["static", "dynamic"],
|
|
1833
|
+
help="'static' (default): beta_target is fixed for the run, "
|
|
1834
|
+
"current sweep behavior, unchanged. 'dynamic': beta_target "
|
|
1835
|
+
"starts at the given positional beta and is then adjusted "
|
|
1836
|
+
"every EVAL_EVERY steps by a task-accuracy-constrained "
|
|
1837
|
+
"controller, bounded to [BETA_CTRL_MIN, BETA_CTRL_MAX].",
|
|
1838
|
+
)
|
|
1839
|
+
parser.add_argument(
|
|
1840
|
+
"--total-steps",
|
|
1841
|
+
type=int,
|
|
1842
|
+
default=None,
|
|
1843
|
+
help="Override the training loop's stopping point for a "
|
|
1844
|
+
"cheap pilot run (e.g. the seed-0/seed-2, short-budget staging pass "
|
|
1845
|
+
"suggested before committing to the full 4-seed x 3-config x "
|
|
1846
|
+
f"{TOTAL_STEPS}-step sweep). Defaults to the module constant "
|
|
1847
|
+
f"TOTAL_STEPS ({TOTAL_STEPS}) if omitted. Does NOT change "
|
|
1848
|
+
"LR_DECAY_STEPS -- a pilot run still anneals on the full schedule.",
|
|
1849
|
+
)
|
|
1850
|
+
parser.add_argument(
|
|
1851
|
+
"--checkpoint-every",
|
|
1852
|
+
type=int,
|
|
1853
|
+
default=None,
|
|
1854
|
+
help="Override how often (in steps) a periodic safety checkpoint "
|
|
1855
|
+
"is saved, in addition to the end-of-run checkpoint. Defaults "
|
|
1856
|
+
f"to the module constant CHECKPOINT_EVERY ({CHECKPOINT_EVERY}) "
|
|
1857
|
+
"if omitted.",
|
|
1858
|
+
)
|
|
1859
|
+
parser.add_argument(
|
|
1860
|
+
"--link-matrix-mode",
|
|
1861
|
+
type=str,
|
|
1862
|
+
default=LINK_MATRIX_MODE,
|
|
1863
|
+
choices=["dense", "ablated", "sparse_topk"],
|
|
1864
|
+
help="Static Option 1: link-matrix ablation/sparsification at "
|
|
1865
|
+
"fixed N. 'dense' (default): unchanged Memory behavior. "
|
|
1866
|
+
"'ablated': link matrix never updated (temporal addressing "
|
|
1867
|
+
"contributes nothing). 'sparse_topk': link matrix updated "
|
|
1868
|
+
"as normal, then each row keeps only its --link-matrix-topk "
|
|
1869
|
+
"largest-magnitude entries.",
|
|
1870
|
+
)
|
|
1871
|
+
parser.add_argument(
|
|
1872
|
+
"--link-matrix-topk",
|
|
1873
|
+
type=int,
|
|
1874
|
+
default=LINK_MATRIX_TOPK,
|
|
1875
|
+
help="Required when --link-matrix-mode=sparse_topk: how many "
|
|
1876
|
+
"entries each link-matrix row keeps per step.",
|
|
1877
|
+
)
|
|
1878
|
+
parser.add_argument(
|
|
1879
|
+
"--isolate-link-ablation",
|
|
1880
|
+
action="store_true",
|
|
1881
|
+
help="Hold nr_cells fixed at MODEL_NR_CELLS for the whole run "
|
|
1882
|
+
"(overrides LESSON_NR_CELLS to a constant list), so Option "
|
|
1883
|
+
"2's curriculum-indexed resize never fires. Use this for "
|
|
1884
|
+
"the isolated Option-1 run the roadmap's ordering requires "
|
|
1885
|
+
"-- do not combine with Option 2 in the same run.",
|
|
1886
|
+
)
|
|
1887
|
+
parser.add_argument(
|
|
1888
|
+
"--dynamic-n-mode",
|
|
1889
|
+
action="store_true",
|
|
1890
|
+
help="Dynamic-N, macro-scale: usage-triggered nr_cells growth "
|
|
1891
|
+
"between episodes, instead of LESSON_NR_CELLS's "
|
|
1892
|
+
"curriculum-indexed lookup (which this disables, same "
|
|
1893
|
+
"isolation as --isolate-link-ablation). Model starts at "
|
|
1894
|
+
"--dynamic-n-floor and grows toward --dynamic-n-ceiling "
|
|
1895
|
+
"whenever memory-usage saturation stays above "
|
|
1896
|
+
"--dynamic-n-trigger-frac past the post-growth cooldown.",
|
|
1897
|
+
)
|
|
1898
|
+
parser.add_argument(
|
|
1899
|
+
"--dynamic-n-floor",
|
|
1900
|
+
type=int,
|
|
1901
|
+
default=DYNAMIC_N_FLOOR,
|
|
1902
|
+
help="Starting (and minimum) nr_cells under --dynamic-n-mode.",
|
|
1903
|
+
)
|
|
1904
|
+
parser.add_argument(
|
|
1905
|
+
"--dynamic-n-ceiling",
|
|
1906
|
+
type=int,
|
|
1907
|
+
default=DYNAMIC_N_CEILING,
|
|
1908
|
+
help="Hard ceiling nr_cells will never grow past under "
|
|
1909
|
+
"--dynamic-n-mode (Strategy doc: sparse-link-matrix "
|
|
1910
|
+
"approximation only validated to N=512).",
|
|
1911
|
+
)
|
|
1912
|
+
parser.add_argument(
|
|
1913
|
+
"--dynamic-n-trigger-frac",
|
|
1914
|
+
type=float,
|
|
1915
|
+
default=DYNAMIC_N_TRIGGER_FRAC,
|
|
1916
|
+
help="Growth fires when the EMA fraction of memory cells with "
|
|
1917
|
+
"usage above DYNAMIC_N_USAGE_HIGH exceeds this, past cooldown.",
|
|
1918
|
+
)
|
|
1919
|
+
parser.add_argument(
|
|
1920
|
+
"--dynamic-n-cooldown-steps",
|
|
1921
|
+
type=int,
|
|
1922
|
+
default=DYNAMIC_N_COOLDOWN_STEPS,
|
|
1923
|
+
help="Steps to wait after a growth event before another can fire, "
|
|
1924
|
+
"so the model's addressing policy gets time to re-settle.",
|
|
1925
|
+
)
|
|
1926
|
+
parser.add_argument(
|
|
1927
|
+
"--moe",
|
|
1928
|
+
action="store_true",
|
|
1929
|
+
help="Alternative Phase 3, Step 2, Option 4: interleave external "
|
|
1930
|
+
"SwitchMoE blocks between Mamba controller blocks (mamba_controller.py "
|
|
1931
|
+
"/ moe_layer.py).",
|
|
1932
|
+
)
|
|
1933
|
+
parser.add_argument(
|
|
1934
|
+
"--moe-num-experts",
|
|
1935
|
+
type=int,
|
|
1936
|
+
default=MOE_NUM_EXPERTS,
|
|
1937
|
+
help="Number of experts per MoE block (>=4 enforced by SwitchMoE; "
|
|
1938
|
+
"roadmap recommends 8+, per Dead-End #45).",
|
|
1939
|
+
)
|
|
1940
|
+
parser.add_argument(
|
|
1941
|
+
"--moe-expert-dim",
|
|
1942
|
+
type=int,
|
|
1943
|
+
default=MOE_EXPERT_DIM,
|
|
1944
|
+
help="Hidden dim of each expert FFN. Defaults to 3x the controller's "
|
|
1945
|
+
"hidden_size (MoE-Mamba's own '3:3' active-parameter ratio) if omitted.",
|
|
1946
|
+
)
|
|
1947
|
+
parser.add_argument(
|
|
1948
|
+
"--moe-capacity-factor",
|
|
1949
|
+
type=float,
|
|
1950
|
+
default=MOE_CAPACITY_FACTOR,
|
|
1951
|
+
help="Switch-style expert capacity buffer above an even token split.",
|
|
1952
|
+
)
|
|
1953
|
+
parser.add_argument(
|
|
1954
|
+
"--moe-load-balance-alpha",
|
|
1955
|
+
type=float,
|
|
1956
|
+
default=MOE_LOAD_BALANCE_ALPHA,
|
|
1957
|
+
help="Weight on the Switch-style auxiliary load-balancing loss.",
|
|
1958
|
+
)
|
|
1959
|
+
parser.add_argument("--moe-top-k", type=int, default=MOE_TOP_K, help="Number of top-k")
|
|
1960
|
+
parser.add_argument(
|
|
1961
|
+
"--split-graph",
|
|
1962
|
+
action="store_true",
|
|
1963
|
+
help="Alternate Phase 3, Step 2, Option 5: enable the "
|
|
1964
|
+
"split-compute-graph controller (parallel Mamba backbone + "
|
|
1965
|
+
"sequential memory addressing). DISABLED unless this flag is "
|
|
1966
|
+
"passed -- roadmap flags this as untested/isolated-ablation-"
|
|
1967
|
+
"required. Overrides --controller entirely when set.",
|
|
1968
|
+
)
|
|
1969
|
+
parser.add_argument(
|
|
1970
|
+
"--split-graph-variant",
|
|
1971
|
+
type=str,
|
|
1972
|
+
default=SPLIT_GRAPH_MAMBA_VARIANT,
|
|
1973
|
+
choices=["mamba1", "mamba2", "mamba3", "cfc", "mamba1+cfc", "mamba2+cfc", "mamba3+cfc"],
|
|
1974
|
+
help="Backbone variant for --split-graph. 'mamba2' requires "
|
|
1975
|
+
"mamba_ssm.modules.mamba2.Mamba2 to be importable. - same with 'mamba3'",
|
|
1976
|
+
)
|
|
1977
|
+
parser.add_argument(
|
|
1978
|
+
"--split-graph-num-blocks",
|
|
1979
|
+
type=int,
|
|
1980
|
+
default=SPLIT_GRAPH_NUM_BLOCKS,
|
|
1981
|
+
help="Number of stacked backbone blocks for --split-graph.",
|
|
1982
|
+
)
|
|
1983
|
+
parser.add_argument(
|
|
1984
|
+
"--split-graph-headdim",
|
|
1985
|
+
type=int,
|
|
1986
|
+
default=SPLIT_GRAPH_MAMBA_HEADDIM,
|
|
1987
|
+
help="Mamba2 headdim (mamba2 variant only).",
|
|
1988
|
+
)
|
|
1989
|
+
parser.add_argument(
|
|
1990
|
+
"--split-graph-no-combine",
|
|
1991
|
+
action="store_true",
|
|
1992
|
+
help="Built-in ablation for the roadmap's required 'read now, "
|
|
1993
|
+
"decide next hop' verification: disables the read-vector "
|
|
1994
|
+
"combiner entirely, so addressing never sees any read vector. "
|
|
1995
|
+
"Only meaningful with --split-graph-combiner-mode=linear.",
|
|
1996
|
+
)
|
|
1997
|
+
parser.add_argument(
|
|
1998
|
+
"--split-graph-combiner-mode",
|
|
1999
|
+
type=str,
|
|
2000
|
+
default=SPLIT_GRAPH_COMBINER_MODE,
|
|
2001
|
+
choices=["linear", "controller"],
|
|
2002
|
+
help="v11: 'linear' (default) is the original, already-efficient "
|
|
2003
|
+
"stateless combiner. 'controller' drives the sequential "
|
|
2004
|
+
"addressing step with a real interleaved Mamba controller "
|
|
2005
|
+
"cell instead (see --split-graph-combiner-variant).",
|
|
2006
|
+
)
|
|
2007
|
+
parser.add_argument(
|
|
2008
|
+
"--split-graph-combiner-variant",
|
|
2009
|
+
type=str,
|
|
2010
|
+
default=SPLIT_GRAPH_COMBINER_VARIANT,
|
|
2011
|
+
choices=[
|
|
2012
|
+
"mamba1",
|
|
2013
|
+
"mamba2",
|
|
2014
|
+
"mamba3",
|
|
2015
|
+
"cfc",
|
|
2016
|
+
"mamba+cfc",
|
|
2017
|
+
"mamba2+cfc",
|
|
2018
|
+
"mamba3+cfc",
|
|
2019
|
+
"cfc+mamba",
|
|
2020
|
+
"cfc+cfc",
|
|
2021
|
+
"mamba+mamba",
|
|
2022
|
+
"mamba2+mamba2",
|
|
2023
|
+
"mamba3+mamba3",
|
|
2024
|
+
],
|
|
2025
|
+
help="v11: which controller drives the sequential combiner when "
|
|
2026
|
+
"--split-graph-combiner-mode=controller. 'mamba1' is the "
|
|
2027
|
+
"recommended/efficient choice; 'mamba2' is wired but not "
|
|
2028
|
+
"expected to be used (known inefficient, same reason plain "
|
|
2029
|
+
"--controller mamba2 runs aren't pursued). Same-kind stacks "
|
|
2030
|
+
"(e.g. 'cfc+cfc') isolate depth from cross-kind mixing -- "
|
|
2031
|
+
"hybrid_controller.py's STAGE_KINDS already supports "
|
|
2032
|
+
"duplicated kinds, this just widens the CLI's accepted list.",
|
|
2033
|
+
)
|
|
2034
|
+
parser.add_argument(
|
|
2035
|
+
"--split-graph-combiner-num-blocks",
|
|
2036
|
+
type=int,
|
|
2037
|
+
default=SPLIT_GRAPH_COMBINER_NUM_BLOCKS,
|
|
2038
|
+
help="Number of stacked blocks in the controller combiner "
|
|
2039
|
+
"(--split-graph-combiner-mode=controller only). Kept small "
|
|
2040
|
+
"by default -- this step is meant to stay cheap.",
|
|
2041
|
+
)
|
|
2042
|
+
parser.add_argument(
|
|
2043
|
+
"--dataset-type",
|
|
2044
|
+
type=str,
|
|
2045
|
+
default=DATASET_TYPE,
|
|
2046
|
+
choices=["graph", "text", "audio", "video", "multimodal"],
|
|
2047
|
+
help="Dataset plugged into the core loop (data/dataset_registry.py). "
|
|
2048
|
+
"Only 'graph' (graph-traversal) is implemented so far.",
|
|
2049
|
+
)
|
|
2050
|
+
parser.add_argument(
|
|
2051
|
+
"--dataset-link",
|
|
2052
|
+
type=str,
|
|
2053
|
+
default=DATASET_LINK,
|
|
2054
|
+
help="Real training-data source for --dataset-type: a graph edge file "
|
|
2055
|
+
"(graph), a text file (text), an audio file (audio, needs torchaudio), "
|
|
2056
|
+
"or a video file (video, needs opencv-python). Omit (or 'graph-traversal' "
|
|
2057
|
+
"for graph) to keep the built-in synthetic data for that type.",
|
|
2058
|
+
)
|
|
2059
|
+
parser.add_argument(
|
|
2060
|
+
"--test-dataset-link",
|
|
2061
|
+
type=str,
|
|
2062
|
+
default=None,
|
|
2063
|
+
help="Path to a real held-out TEST source (a second text/audio/video file, "
|
|
2064
|
+
"or a graph edge file), used instead of the default synthetic/seeded "
|
|
2065
|
+
"held-out split -- the modality's analogue of the graph dataset's "
|
|
2066
|
+
"London Underground test graph. Omit to keep default behavior: for "
|
|
2067
|
+
"graph, the built-in London Underground; for text/audio/video, a "
|
|
2068
|
+
"disjoint-key held-out slice of --dataset-link (or the fully synthetic "
|
|
2069
|
+
"seeded table if --dataset-link is also omitted).",
|
|
2070
|
+
)
|
|
2071
|
+
args = parser.parse_args()
|
|
2072
|
+
|
|
2073
|
+
if args.resume is not None and args.beta is None:
|
|
2074
|
+
raise SystemExit(
|
|
2075
|
+
"--resume requires the beta positional arg too, e.g.:\n"
|
|
2076
|
+
f" python3 {sys.argv[0]} 0.02 --resume checkpoints/beta_0p02_latest.pt"
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
if args.link_matrix_mode == "sparse_topk" and args.link_matrix_topk is None:
|
|
2080
|
+
raise SystemExit("--link-matrix-mode=sparse_topk requires --link-matrix-topk")
|
|
2081
|
+
|
|
2082
|
+
if args.beta is not None:
|
|
2083
|
+
betas_to_run = [args.beta]
|
|
2084
|
+
if args.beta not in BETAS_TO_SWEEP:
|
|
2085
|
+
print(
|
|
2086
|
+
f"WARNING: {args.beta} is not in the planned sweep "
|
|
2087
|
+
f"{BETAS_TO_SWEEP} -- running it anyway, but it won't be "
|
|
2088
|
+
f"picked up by the combined summary step below."
|
|
2089
|
+
)
|
|
2090
|
+
else:
|
|
2091
|
+
betas_to_run = BETAS_TO_SWEEP
|
|
2092
|
+
|
|
2093
|
+
all_summaries = []
|
|
2094
|
+
for beta in betas_to_run:
|
|
2095
|
+
# These are made to avoid checkpoint collisions between different types of runs.
|
|
2096
|
+
run_id = f"beta_{beta}_seed{args.seed}_learnedprior".replace(".", "p")
|
|
2097
|
+
if args.controller == "mamba":
|
|
2098
|
+
run_id = f"{run_id}_mambactrl"
|
|
2099
|
+
elif args.controller == "mamba2":
|
|
2100
|
+
run_id = f"{run_id}_mamba2ctrl"
|
|
2101
|
+
elif args.controller == "mamba3":
|
|
2102
|
+
run_id = f"{run_id}_mamba3ctrl"
|
|
2103
|
+
elif args.controller == "cfc":
|
|
2104
|
+
run_id = f"{run_id}_cfcctrl"
|
|
2105
|
+
elif "+" in args.controller:
|
|
2106
|
+
run_id = f"{run_id}_{args.controller.replace('+', '')}ctrl"
|
|
2107
|
+
if args.beta_mode == "dynamic":
|
|
2108
|
+
run_id = f"{run_id}_dynbeta"
|
|
2109
|
+
if args.link_matrix_mode != "dense":
|
|
2110
|
+
tag = (
|
|
2111
|
+
args.link_matrix_mode
|
|
2112
|
+
if args.link_matrix_mode != "sparse_topk"
|
|
2113
|
+
else f"sparsetopk{args.link_matrix_topk}"
|
|
2114
|
+
)
|
|
2115
|
+
run_id = f"{run_id}_link{tag}"
|
|
2116
|
+
if args.dynamic_n_mode:
|
|
2117
|
+
run_id = f"{run_id}_dynN"
|
|
2118
|
+
if args.moe:
|
|
2119
|
+
run_id = f"{run_id}_moe{args.moe_num_experts}e"
|
|
2120
|
+
if args.split_graph:
|
|
2121
|
+
run_id = f"{run_id}_splitgraph_{args.split_graph_variant.replace('+', '')}"
|
|
2122
|
+
if args.split_graph_combiner_mode == "controller":
|
|
2123
|
+
run_id = f"{run_id}_combctrl{args.split_graph_combiner_variant.replace('+', '')}"
|
|
2124
|
+
if args.dataset_type != "graph":
|
|
2125
|
+
run_id = f"{run_id}_ds{args.dataset_type}"
|
|
2126
|
+
if args.run_id_suffix:
|
|
2127
|
+
run_id = f"{run_id}_{args.run_id_suffix}"
|
|
2128
|
+
summary = run(
|
|
2129
|
+
beta_target=beta,
|
|
2130
|
+
run_id=run_id,
|
|
2131
|
+
seed=args.seed,
|
|
2132
|
+
resume_from=args.resume,
|
|
2133
|
+
controller=args.controller, # v7
|
|
2134
|
+
beta_mode=args.beta_mode,
|
|
2135
|
+
total_steps=(args.total_steps if args.total_steps is not None else TOTAL_STEPS),
|
|
2136
|
+
checkpoint_every=(
|
|
2137
|
+
args.checkpoint_every if args.checkpoint_every is not None else CHECKPOINT_EVERY
|
|
2138
|
+
),
|
|
2139
|
+
link_matrix_mode=args.link_matrix_mode,
|
|
2140
|
+
link_matrix_topk=args.link_matrix_topk,
|
|
2141
|
+
isolate_link_ablation=args.isolate_link_ablation,
|
|
2142
|
+
dynamic_n_mode=args.dynamic_n_mode,
|
|
2143
|
+
dynamic_n_floor=args.dynamic_n_floor,
|
|
2144
|
+
dynamic_n_ceiling=args.dynamic_n_ceiling,
|
|
2145
|
+
dynamic_n_trigger_frac=args.dynamic_n_trigger_frac,
|
|
2146
|
+
dynamic_n_cooldown_steps=args.dynamic_n_cooldown_steps,
|
|
2147
|
+
moe_enabled=args.moe,
|
|
2148
|
+
moe_num_experts=args.moe_num_experts,
|
|
2149
|
+
moe_expert_dim=args.moe_expert_dim,
|
|
2150
|
+
moe_capacity_factor=args.moe_capacity_factor,
|
|
2151
|
+
moe_load_balance_alpha=args.moe_load_balance_alpha,
|
|
2152
|
+
moe_top_k=args.moe_top_k,
|
|
2153
|
+
split_graph_enabled=args.split_graph,
|
|
2154
|
+
split_graph_variant=args.split_graph_variant,
|
|
2155
|
+
split_graph_num_blocks=args.split_graph_num_blocks,
|
|
2156
|
+
split_graph_headdim=args.split_graph_headdim,
|
|
2157
|
+
split_graph_combine_reads=not args.split_graph_no_combine,
|
|
2158
|
+
split_graph_combiner_mode=args.split_graph_combiner_mode,
|
|
2159
|
+
split_graph_combiner_variant=args.split_graph_combiner_variant,
|
|
2160
|
+
split_graph_combiner_num_blocks=args.split_graph_combiner_num_blocks,
|
|
2161
|
+
dataset_type=args.dataset_type,
|
|
2162
|
+
dataset_link=args.dataset_link,
|
|
2163
|
+
test_dataset_link=args.test_dataset_link,
|
|
2164
|
+
)
|
|
2165
|
+
all_summaries.append(summary)
|
|
2166
|
+
|
|
2167
|
+
print("\n===== Sweep summary (this process) =====")
|
|
2168
|
+
for s in all_summaries:
|
|
2169
|
+
print(s)
|
|
2170
|
+
|
|
2171
|
+
# Per-process partial summary - when running one beta per process, each
|
|
2172
|
+
# writes its own file; merge them by hand (or a short follow-up script)
|
|
2173
|
+
# once all processes finish. Only the no-arg (full sequential) path
|
|
2174
|
+
# produces a complete combined summary in one file.
|
|
2175
|
+
summary_suffix = "_".join(f"b{b}".replace(".", "p") for b in betas_to_run)
|
|
2176
|
+
summary_path = os.path.join(LOG_DIR, f"gate1_sweep_summary_{summary_suffix}.csv")
|
|
2177
|
+
with open(summary_path, "w", newline="") as f:
|
|
2178
|
+
w = csv.DictWriter(f, fieldnames=list(all_summaries[0].keys()))
|
|
2179
|
+
w.writeheader()
|
|
2180
|
+
for s in all_summaries:
|
|
2181
|
+
w.writerow(s)
|
|
2182
|
+
print(f"\nWrote summary to {summary_path}")
|