An independent replication of the continual-learning claims in
volotat/mini-AGI (commit 201852d), run on one RTX 4080 Super.
Written as a courtesy to the author. The project is careful and unusually honest about its own history,
and this report tries to return the favour.
AI assistance: the experiments, analysis and this write-up were produced with Claude Opus 5.5 (Anthropic), directed and reviewed by Jason Matthews.
Revision note. The first version of this report said the headline forgetting number did not replicate
(50–110× larger). That was our error. Our scaled-down setup took 4× more optimizer steps per character than
upstream, and forgetting turns out to be steeply sensitive to that. Once step density is matched, the headline
replicates. This version reruns the key results at matched density. The step-density effect turned out to be
the most instructive result, and it gets its own section below.
The short version
The README's headline replicates: reading one subject with a slowly-updated shared trunk leaves the
other subjects intact. What it demonstrates is a known principle, clearly: forgetting tracks how far the shared
weights move. Here that looks like roughly the square of the displacement, as in Biderman et al.'s
"learns less, forgets less". It also means the protection holds for a bounded amount of reading, not indefinitely.
+0.0067
README claim
trunk 0.1×, 524k chars of chess
+0.030 / +0.022
Ours, same probe
at upstream's step density; 99.3% / 99.5% retained
−0.051 / −0.017
Control
all subjects read; our noise floor
+1.49 / +1.09
Same probe, 4M chars
8× longer; 65% / 75% retained
Forgetting = mean change in held-out loss on the 7 subjects not being read, nats/char.
Pairs are seed 0 / seed 1 throughout.
The trunk's learning rate is the lever. A 1× trunk forgets ~30× more than a 0.1× trunk; the experts' rate barely matters.
The architecture's distinctive parts don't do the protecting. Running every rate at 0.1× forgets as little as the trunk/expert split, and freezing the expert working set changes nothing.
Forgetting grows roughly with the square of the trunk's displacement (learning rate × optimizer steps). That's why a 4× difference in step density produced a ~25× difference in forgetting.
The floor is horizon-bound. Forgetting stays at the control level for about 0.5M characters, then climbs steadily, while chess itself improves by only ~0.04 nats.
Recovery replicates: about 75% of heavy damage returns within 131k characters of mixed reading, then plateaus at 80–86%.
Claim by claim
#
README claim
Verdict
What we measured (at upstream's step density unless noted)
1
524k chars of chess at trunk 0.1× costs the unread subjects +0.0067 nats (99.84% retained)
At trunk LR = expert LR: +2.23 (swapping) / +2.59 (working set frozen)
Direction
+0.87 / +0.80 swapping: ~30× the 0.1× trunk, though smaller than the README's figure.
3
The expert pool is not the mechanism (freezing the working set explains 13.8%)
Replicates
Frozen vs swapping indistinguishable (sign flips across seeds; measured at 4× density). Every rate at 0.1× gives +0.026 / +0.016, the same as the split.
4
About ¾ of the damage comes back in 131k chars of mixed reading
Replicates
76% / 73% (measured at 4× density), then a plateau at 80–86%.
5
Only 54 of 136 experts receive gradient during the probe
Replicates
24 / 132 and 33 / 134, a similar minority of the pool.
Implied: continual reading without catastrophic forgetting
Horizon-bound
Same probe over 4M characters: +1.49 / +1.09 (65% / 75% retained), while chess gains +0.045 / +0.038.
How we tested it
Code & data
Upstream vendored unmodified. Bases trained with upstream's own train.py read: its LR controller, growth, pruning, paging and evaluator.
Only logging-side runtime changes (thinned text sampling; a guard on a crashing diagnostic meter).
Upstream's own 8-subject corpus from python -m corpora all, with the chat_hermes held-out set restored (see the notes for the author).
The probe
Upstream's probe tooling isn't published, so it was rebuilt from the README, reusing the same per-chunk training step as read.
Batch 1, 524,288 characters, each arm from the same base copy, at the learning rate the base's own LR controller had reached. The same held-out text (61k chars per subject) is scored every 65k characters.
Step density matched to upstream by accumulating gradients over 4 chunks per optimizer step (256 steps per 524k characters, as at upstream's chunk of 2,048).
Where the README is silent: growth and pruning off; LR frozen at the base controller's value; "frozen" = working set chosen once, never re-chosen.
Scale
Upstream
Here
d_model / heads
512 / 8
256 / 4
Loop depth (max)
24
8
Expert width / top-k / resident
2048 / 8 / 32
512 / 4 / 16
Experts at probe time
136
132 / 134
Trunk parameters
8.3M
2.1M
Chunk / context
2,048 / 4,096
512 / 1,024
Optimizer steps per 524k chars
256
256 (accumulate 4)
Chars read before probe
not stated
77M
Unread-subject loss at probe start
1.12
1.28 / 1.24
Ratios preserved: top-k/resident, mean/max halting depth, chunk/context, trunk_lr_mult 0.1. Two independent seeds (init + data order).
Metrics
Forgetting: mean Δ held-out loss over subjects not read.
Retained: 1 − forgetting / (ln 265 − start loss), which reproduces the README's figure.
Gain: improvement on the subject being read.
1 · The headline probe replicates
Forgetting on the unread subjects while reading 524k characters of chess, at upstream's step density. Solid = seed 0, dashed = seed 1.
Arm (524k chars of chess, matched density)
Upstream
Seed 0
Seed 1
Chess gain
Trunk 1×, swapping
+2.23
+0.87
+0.80
−0.011 / −0.006
Trunk 0.1×, swapping
+0.0067
+0.030
+0.022
+0.020 / +0.020
Every rate at 0.1× (no split)
n/a
+0.026
+0.016
+0.020 / +0.020
Control, all subjects read
−0.0077
−0.051
−0.017
n/a
The 0.1× trunk stays within our noise floor for the whole probe; the control's own spread (−0.05 to +0.02) is the measure of that floor. The contrast with a 1× trunk is ~30×. Removing the trunk/expert split, by slowing the experts too, changes nothing.
2 · Step density, and why it matters
Our first version ran every probe at 4× upstream's optimizer steps per character: we shrank the chunk from 2,048 to 512 characters to fit the scaled model, without matching the step count. Holding everything else fixed and varying only the steps:
Optimizer steps per 524k chars
Seed 0
Seed 1
Chess gain
1,024 (our first version)
+0.73
+0.32
+0.010 / +0.010
512
+0.25
+0.066
+0.017 / +0.018
256 (upstream's density)
+0.030
+0.022
+0.020 / +0.020
Every trunk-0.1× probe, plotted against probe learning rate × optimizer steps. Blue: learning rate varied at fixed steps. Green: steps varied by gradient accumulation at fixed learning rate. Log-log.
Steps and learning rate are one lever. Adam normalizes each update to roughly the learning rate in size, so how far the trunk moves per character is about learning rate × steps per character. Varying either lands on the same curve, within seed noise.
The curve is close to quadratic. At small displacement, the slope on a log-log plot is 2.1 on both seeds, flattening toward 1–1.5 as the damage saturates. That's why a 4× difference in steps produced a ~25× difference in forgetting.
More displacement bought no learning. Chess gains ~0.02 at the smallest displacement and ~0.01 at the largest. The extra movement is pure cost.
The practical consequence: tokens per optimizer step is a hidden hyperparameter in any streaming-learning claim. A forgetting number without the learning rate, the tokens per step and the horizon isn't comparable across setups. It nearly misled this replication.
3 · Read for longer
The headline probe run 8× longer (4M characters) at matched density. The shaded band is the README's probe length. Gray: control, all subjects read (from the first version, at 4× density; it is flat either way).
Forgetting holds at the control level through about 0.5M characters, then climbs steadily to +1.49 / +1.09 by 4M: 65% / 75% of the gain over chance retained. Chess improves by just +0.045 / +0.038 over the same stretch. The README's "never leaves the floor" is accurate for the window it measured; the square law explains why the window is finite.
4 · Recovery
After the trunk-1× probe (measured at 4× step density), share of the damage recovered while reading all subjects in rotation.
Reading all subjects recovers 79% / 77% within 65k characters. By then only 2 of the 8 subjects have been read, so recovery is general rather than per-subject relearning, which fits the README's "displacement, not destruction". Recovery then plateaus at 80–86%, with +0.64 / +0.69 nats still missing after 1M characters. (Subjects rotate every 32,768 characters, so the README's 131k window covers 4 of the 8.)
What this demonstrates
The result is a clean, cheap demonstration of a principle established in prior work, reproduced in a byte-level, sparse-expert model that learns from a stream.
Learns less, forgets less.Biderman et al. (2024) showed that constraining how much a model's weights can change during fine-tuning (there, with low-rank LoRA updates) preserves capabilities outside the target domain, at the cost of learning less of it. mini-AGI constrains the same thing a different way: a slow learning rate on the shared trunk. Our results show the "forgets less" half directly. Forgetting falls with the trunk's displacement, whether that displacement is reduced by the learning rate or by the number of steps. The "learns less" half is invisible here, because the probe subject was already learned (see below).
Why it's roughly quadratic. A base trained to a minimum on its subjects has near-zero gradient on them, so a small step costs nothing to first order, and the loss rises only through curvature: ΔL ≈ ½·δᵀHδ. Mirzadeh et al. (2020) analyse forgetting exactly this way, as a function of displacement and curvature, and show training-regime choices such as learning rate and batch size move it. Our slope of ~2 at small displacement, and the flat-then-rising curve over 4M characters, are what that view predicts.
Slowing the shared layers is established practice. Layer-wise learning rates, lower in the shared body than the head, are a standard fine-tuning tool (Howard & Ruder, 2018). The trunk multiplier is that idea applied to continual reading.
The curvature view also points past uniform slowing.EWC (Kirkpatrick et al., 2017) penalizes movement weighted by each parameter's importance to earlier tasks: the same quadratic, made selective.
What's specific to mini-AGI is where the effect lives. The expert pool confines each update to a minority of experts, yet that doesn't protect anything; the dense shared trunk carries the interference. That's a useful data point, given how often sparse or modular architectures are proposed as a continual-learning mechanism.
Paths that would qualify the claims further
These are the open questions this replication couldn't settle, offered as directions rather than gaps in the original work:
A subject the model hasn't learned. The chess probe improves chess by only ~0.02–0.04 nats, so it measures stability, not plasticity. Probing with a subject held out of base training would show the "learns less" side of the trade-off. The quadratic view predicts that learning grows linearly with displacement while forgetting grows quadratically, so slower reading should improve the gain-to-forgetting ratio, at the cost of time.
Step count vs learning rate at matched displacement. On one seed, accumulating gradients forgot about 3× less than lowering the learning rate to the same displacement; on the other it didn't. If averaging out batch-1 gradient noise is a real bonus, accumulation is the better knob for streaming learners. More seeds would settle it.
Selective vs uniform slowing. An EWC-style or L2-to-anchor penalty on the trunk, compared with the 0.1× trunk at equal learning, would test whether protection can be targeted rather than bought with a uniformly slow trunk.
Base maturity. The quadratic picture assumes the base sits at a minimum. On a less-trained base the first-order term returns; a short pilot on a 12-minute base showed large forgetting even at 0.1×.
Scale. Our trunk is a quarter of upstream's. A larger trunk may have a wider, flatter basin and hold the floor longer.
Caveats
Scale. 2.1M trunk parameters against 8.3M; the bases start the probe at a comparable loss on the unread subjects (1.24–1.28 vs 1.12).
Reconstructed probe. Rebuilt from the README's description; where it is silent we took the most literal reading.
Mixed densities in supporting results. The headline, trunk-1×, no-split, control and 4M results are at matched density. The frozen-working-set comparison and the recovery curve were measured at 4× density; their conclusions (no effect; ~75% then a plateau) don't depend on the absolute level.
Two seeds. They differ in magnitude by up to 2–3× but agree on every verdict.
Notes for the author
Tokens per optimizer step is worth reporting with any forgetting number. Forgetting here scales roughly with the square of learning rate × steps per character, so the same 524k-character probe gives +0.03 or +0.73 depending only on the chunk size. We learned this by getting it wrong first.
The corpus builder drops the chat_hermes held-out set. Hermes writes its held-out files to data/val/chat, then the synthetic-chat expand step wipes that folder, so a fresh python -m corpora all can't produce the README's eight held-out subjects. Noted on PR #11, which fixes the wipe as a side effect.
GradSNR.observe crashes when a parameter's grad is None on some steps: a depth-1 step leaves the halting head gradless. Reported in #19; fix proposed in #20.
The chess probe barely teaches the model anything (≤0.04 nats even over 4M characters). A probe on an unlearned subject would test the full stability-plasticity trade-off.
The probe tooling is referenced but not published (tools/, runs/cl/, runs/results/). Publishing it would make the headline directly checkable.
Reproduce
python -m corpora all # upstream corpus, then restore chat_hermes (see code/README.md)
python harness/run_upstream.py configs/rep_s.yaml read upstream/mini-AGI/data/train --save \
--weights-dir runs/rep_s_seed1/weights --held-out upstream/mini-AGI/data/val \
--sample-every 0.67 --minutes 240 --seed 1 --shuffle-seed 1
harness/run_accum4.sh # headline arms at upstream's step density (--accum 4)
harness/run_stepdensity.sh # the same probe at 1, 2 and 4 chunks per step
harness/run_matrix.sh runs/bases/s_seed1 runs/probes/s_seed1 0 # first-version arms (4x density)
python harness/summarize.py runs/probes/*/*/probe.json && python harness/figures.py v2
Code, configs, every probe's raw results (probe.json) and the full deviation and run log (NOTES.md) are published at huggingface.co/spaces/dreddnafious/mini-agi-replication. The two summary commands above regenerate every table and figure from them without a GPU.