Replication report · continual learning

Does mini-AGI forget? A small-scale replication

Jason Matthews · github.com/dreddnafious · September 2026 · revision 2

Download PDF · Code, configs & raw results · Upstream project

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.

Claim by claim

#README claimVerdictWhat we measured (at upstream's step density unless noted)
1524k chars of chess at trunk 0.1× costs the unread subjects +0.0067 nats (99.84% retained) Replicates +0.030 / +0.022 (99.3% / 99.5% retained); control −0.051 / −0.017.
2At 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.
3The 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.
4About ¾ of the damage comes back in 131k chars of mixed reading Replicates 76% / 73% (measured at 4× density), then a plateau at 80–86%.
5Only 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

UpstreamHere
d_model / heads512 / 8256 / 4
Loop depth (max)248
Expert width / top-k / resident2048 / 8 / 32512 / 4 / 16
Experts at probe time136132 / 134
Trunk parameters8.3M2.1M
Chunk / context2,048 / 4,096512 / 1,024
Optimizer steps per 524k chars256256 (accumulate 4)
Chars read before probenot stated77M
Unread-subject loss at probe start1.121.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

headline probe at matched step density
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)UpstreamSeed 0Seed 1Chess 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.017n/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 charsSeed 0Seed 1Chess 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
forgetting vs displacement
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.

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

4M characters at matched density
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

recovery after damage
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.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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×.
  5. Scale. Our trunk is a quarter of upstream's. A larger trunk may have a wider, flatter basin and hold the floor longer.

Caveats

Notes for the author

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.