The matching game at scale: a recipe that ports

The matching-game setup is a graph-theoretic extension of the Berglund setup: 6 categories, an arbitrary bijection across them, a random directed-edge topology where each of the 30 possible edges is held in for training with probability ptrain = 0.4, and 20 % of the remaining edges held out for evaluation. Train sentences are templated (“In Bostock's matching game, the animal associated with the colour red is…”), the eval set uses unseen templates, and the metric throughout this page is accuracy — defined as “log-prob(correct completion) is strictly greater than log-prob of each of three in-category random negatives”. Chance is therefore 0.25 (a 4-way ranking task per example).

At Pythia-70M with LoRA, the model learns its 12 training edges and generalizes to held-out edges whose source and target are reachable through a trained intermediate node. Test edges that aren't 2-hop composable stay at chance. So far so neat. The motivating question was: does the same picture survive when we scale up by 20× and then by 500×?

The headline this page started with — “concept crystallization is a small-model phenomenon” — turned out to be wrong, twice. V4 showed it ports to Pythia-1.4B with full-rank + L2-SP. V5 shows it ports to Qwen-14B once the L2-SP penalty and learning rate are scaled to the new param count. The recipe works; what you need to do is stop holding the wrong things constant when you change scale.

§0The recipe (V5)

Working backwards from the result: three scales of base model, the same matching-game task at ptrain = 0.6, the same evaluation. The third column is the full-parameter fine-tune recipe that gets the model from chance to crystallization. Train accuracy ≈ 1 means the model memorised its training edges; test accuracy is forward-transitive composition on held-out edges.

model recipe L2-SP λ LR TRAIN TEST
Pythia-70M (1× A100) LoRA r=16 dp=0.3, eff. batch 32, ~20k steps 4e-4 1.00 1.00
Pythia-1.4B (1× A100) full-rank, eff. batch 32, 10k steps 1e-32e-4 1.00 1.00
Qwen2.5-14B (1× B200) full-rank, embed + lm_head frozen, eff. batch 32, 4k steps 1e-45e-4 0.94 0.79

Two scaling rules emerge from this table that aren't visible at any single scale:

The other change is mechanical, not statistical: freeze the input embeddings and lm_head for full-parameter FT on a large-vocab model. On Qwen-14B that drops 1.56B of the 14.8B params from the optimizer state without measurable cost on test (the Pythia-1.4B + frozen-embed control still hits 0.995 test). It also makes the run fit on one 180 GB GPU instead of needing two.

Train and test accuracy across Pythia-70M, Pythia-1.4B, Qwen-14B with the right recipe, plus a Qwen-14B ablation that reuses the 1.4B λ and LR.
Fig. R · The recipe at three scales, plus a Qwen-14B ablation where I held the 1.4B hyperparameters (λ=1e-3, lr=2e-4) constant instead of scaling them. The ablation barely learns even the training edges (train 0.45). Once the L2-SP λ is scaled to 1e-4 and the LR bumped to 5e-4, train climbs to 0.94 and test to 0.79.

The rest of this page is the historical record of how we got here — the V1 wrong headline, the V4 1.4B overturn, and the V5 Qwen-14B port — with the failing-and-fixing ablations spelled out at the end (§13).

§1Original setup (V1)

§1Setup

task
random_6_0.4_0.2 — 6 categories, 12 train edges, 6 held-out test edges, bijection k=8
templates
8 train templates · 4 held-out eval templates
models
EleutherAI/pythia-70m · pythia-1.4b · Qwen/Qwen2.5-32B (4-bit + LoRA)
fine-tuning
LoRA on attention + MLP projections, bf16, SDPA, completion-only loss
metric
mean log-odds gap on held-out templates · 3 random negatives per example
repeats
1 seed (=42). Multiple seeds out of session scope — the topology effect is much larger than the per-seed variance reported in the original Pythia-70M runs.
code
github.com/jonathanbostock/a-is-b-is-c · branch pythia-repro-and-scale

§2The 70M picture: forward-transitive composition

The specific sampled topology for this run is on the left below. The right panel shows each held-out test edge labelled with Pythia-70M's final log-odds gap. Edges drawn in green are 2-hop composable from training edges (there exists a node m such that sm and mt are both trained); orange edges are not.

Training graph and per-test-edge final log-odds gap.
Fig. 1 · Left: the 12 training edges sampled at ptrain=0.4. Right: the 6 held-out test edges, coloured by whether a 2-hop composition exists in the training set. Labels are the final-step Pythia-70M log-odds gap.

The split is clean: every composable test edge ends with accuracy above 0.80; every non-composable edge stays at chance (0.20–0.25). The model isn't learning a generic “Bostock matching” lookup table; it has built a structure where the bijection acts as composable maps, and it can chain them when training has fixed both legs.

§3Then we tried to scale up

Same topology, same templates, same eval. Only the base model changes (with LoRA target modules adjusted to each architecture's projection names). For each scale I ran a low-LR pass (similar to what the published configs use) and a higher-LR retry tuned to match 70M's learning dynamics, because under-training was the natural first hypothesis.

Train and test accuracy trajectories for all six runs (chance line at 0.25).
Fig. 2 · Mean accuracy on training (left) and held-out (right) edges across all six runs. Dashed line = chance (0.25). Dotted lines on the right show max-over-test-edges accuracy. The Pythia-70M templated run climbs to test acc ≈ 0.6 (because half its test edges are composable and the model gets those right); 1.4B and 32B sit at ∼0.3 and ∼0.2 respectively. The synthetic-document Pythia-70M run (orange) doesn't even cross chance on training edges. Note the X axis is symlog.

On the training side, every scale eventually memorises — in fact Qwen-32B at hi-LR memorises fastest, reaching training accuracy 1.0 by step 100. On the test side, only Pythia-70M templated clearly lifts off chance: its mean test accuracy plateaus around 0.55–0.60. The 1.4B hi-LR retry reaches ~0.40, the Qwen-32B hi-LR retry stays at chance, and synth-doc never crosses chance even on training edges.

Final test-edge accuracy by edge and run.
Fig. 3 · Final accuracy on each of the six held-out edges (x axis) across all six runs (bar colours). Green edge labels = composable from training edges. Pythia-70M templated takes the green edges to near-1.0 and the orange ones stay at chance. Every other model stays close to chance everywhere.

§4How the runs compare

modelLRLoRA rsteps TRAIN acc TEST acc crystallizes?
Pythia-70M (templated)4e-41620 000 1.00 0.56 yes
Pythia-70M (synth-doc)4e-4168 000 0.29 0.19 didn't memorise (see §5)
Pythia-1.4B (lo-LR)1e-4324 000 1.00 0.31 no (marginal)
Pythia-1.4B (hi-LR)4e-4328 000 1.00 0.37 no (marginal)
Qwen2.5-32B (lo-LR, 4-bit)5e-532400 0.99 0.20 no
Qwen2.5-32B (hi-LR, 4-bit)2e-4642 000 0.99 0.20 no

If concept crystallization at scale were just a matter of finishing training, you'd expect raising the LR by 4× and the step budget by 2× (the 1.4B hi-LR retry) to push test accuracy toward the 70M one. It barely moves — test accuracy goes from 0.31 to 0.37 (cf. 70M's 0.56). The Qwen-32B retry doesn't move at all: training accuracy hits 1.0, test accuracy stays at chance (0.20).

What the larger models look like they're doing is the simpler thing: learning each training edge as an isolated key-value lookup that the LoRA adapter can install. The 70M model can't afford that — rank-16 LoRA over 70M parameters has nowhere near 12 independent slots for arbitrary template-to-token mappings — so it ends up sharing parameters across edges, and the only way to share is to discover that the bijection is the actual structure. That's the crystallization step.

Caveats: (i) single seed, single topology — for the 70M case the original paper saw the composition pattern across seeds, but I haven't confirmed that the non-crystallization at 1.4B and 32B is robust across seeds. (ii) All scaled-up runs are LoRA, not full fine-tunes — the rank cap could itself be the constraint that prevents the larger models from copying the small-model strategy. A full-rank 1.4B retry is the obvious next experiment.

§5What knobs actually drive crystallization on 70M? — rank, regularization, full-rank

Sticking with Pythia-70M and the same random_6_0.4_0.2 topology, 22 runs sweeping LoRA rank, the LoRA α/r ratio, weight-decay × lora-dropout, and a full-rank baseline. Below we report mean accuracy on the 2-hop-composable subset of test edges and on the non-composable subset, both with the same 0.25 chance line. Training-edge accuracy is ≈1.0 in every run except synth-doc, so we omit it.

First read: concept crystallization is robust to extreme rank reduction. Even at r=1 (the LoRA delta is a rank-1 update on each weight matrix) Pythia-70M reaches 0.51 accuracy on composable test edges — well above chance, though only half-solving. Accuracy climbs with rank to a plateau of 0.85–0.97 from r=16 upward, peaking at r=128 (0.97). The full-rank baseline at lr=2e-4 sits at 0.77 — worse than every LoRA rank from 16 to 256. Non-composable edges stay at chance (0.18–0.23) under every LoRA setting and under full-rank.

Composable + non-composable test-edge accuracy vs LoRA rank, with chance line.
Fig. 4 · Final accuracy vs LoRA rank (with α = 2r so the implicit-LR-via-α/r stays constant). Composable-edge accuracy climbs from 0.51 (r=1) to ~0.97 (r=128); non-composable accuracy stays at chance (dashed line at 0.25) everywhere. The horizontal full-rank lines show it underperforms high-rank LoRA on composable edges (0.77) and is also at chance on non-composable (0.24).

The reg sweep moves the composable-edge needle only mildly in accuracy units — the strongest dropout config (wd=0, dp=0.3) hits 0.98, vs the no-dropout baseline at 0.94. wd=0.1, dp=0.1 actually scores 1.00. The differences are small enough that the “big regularization win” you'd read off the log-odds version is mostly a function of the gap inflating once accuracy is already near ceiling. Non-composable edges stay at chance across the entire 3×3 grid.

Composable and non-composable test-edge accuracy as a heatmap over weight_decay and lora_dropout.
Fig. 5 · Composable-edge accuracy (left) and non-composable accuracy (right) over weight_decay (rows) and lora_dropout (cols), all at r=16, α=32. Left panel saturates near 1.0; right panel hovers around chance (0.25, marked in title). Two takeaways: rank=16 LoRA is already solving the composable edges well enough that regularization has limited headroom; no regularization at this rank gets above chance on the non-composable ones.

§6Non-composable test edges stay at chance under every setting

This is the negative result the accuracy view makes visible. In log-odds units, full-rank looked like it had a tiny edge on the “partially-anchored” edge 1→4. In accuracy units that disappears — full-rank's 1→4 accuracy is 0.25 (= chance), while the lift on 1→4 actually goes to LoRA r=256 at 0.41 (16/24 examples correct). None of the configurations we ran genuinely cracks the source-without-outgoing-edge case.

Per-edge accuracy on the three non-composable test edges for full-rank vs four LoRA configs, all near chance.
Fig. 6 · Accuracy on the three non-composable test edges across the top p=0.4 configs. Source-0 edges (0→1 and 0→4) sit at chance for everyone, because node 0 was sampled as a pure sink — it never appears as a source in training. The 1→4 edge has a small lift for LoRA r=256 only (0.41). Full-rank is not the winner here in accuracy terms.

So the cleaner framing is: concept crystallization perfectly explains generalization to 2-hop composable test edges, and fails to explain anything beyond that. To get any signal on the source-was-never-a-source edges, you'd need a denser training topology (so the model actually sees node 0 as a source somewhere), more data per training edge, or some inductive bias we haven't tried yet. That last bucket is what the in-flight p=0.6 sweep with L2-to-init and on-policy mixin is testing.

Per-edge accuracy on all six test edges across the best p=0.4 configs.
Fig. 7 · All six test edges. Green-labelled (composable) edges are near-1.0 for every high-rank LoRA config; orange-labelled (non-composable) edges hug the 0.25 chance line, except a small LoRA-r=256 lift on 1→4.

§7Sweep summary

configrαdpwd TRAIN acc TEST composable TEST non-composable
rank=11200 0.980.510.18
rank=44800 1.000.580.18
rank=881600 1.000.810.19
rank=16 (baseline)163200 0.990.940.21
rank=12812825600 0.990.970.22
rank=25625651200 0.980.840.23
dp=0.3 (best dropout)16320.30 1.000.980.23
wd=0.1, dp=0.116320.10.1 1.001.000.21
α=64166400 0.990.940.20
full-rank, lr=2e-40 0.990.770.24

Best composable-edge accuracy is wd=0.1, dp=0.1 at 1.00 (with rank-128 and dropout-0.3 tied at 0.97–0.98). Best non-composable accuracy is 0.24 (full-rank) — statistically indistinguishable from the 0.25 chance line. The headline that survives the accuracy translation is: LoRA rank above ~16 is sufficient to fully solve the composable edges; nothing here moves the non-composable edges off chance. The next session takes that to p=0.6 where a denser graph means more edges have at least one outgoing training source, and tries L2-to-init and on-policy data-mixin in case some of those still need additional inductive bias.

§8Synthetic-document fine-tuning: infrastructure works, learning doesn't

The other ask was to try synthetic-document fine-tuning (Allen-Zhu / Berglund-2-style: generate diverse natural-prose documents that embed each fact in passing, then train with LM loss on the whole document). Pipeline runs end-to-end — 768 documents per repeat at 12 genres (story, encyclopedia entry, diary, puzzle solution, tweet thread, …) generated through gpt-4.1-mini with on-disk caching, fed straight into the existing loss path. Pythia-70M trained on those documents reaches a training-edge log-odds gap of only +2 (vs +33 for templates) and a test-edge gap near zero (Fig. 2, orange).

The probable cause is signal dilution. Each document is ~150 tokens but only 1–2 tokens are the association being trained. Averaged LM loss puts ~1 % of the gradient on the bit we actually care about, so the effective “steps per association” in this run is ∼150× lower than in the templated run. Two clean fixes for V2:

I'm bullish on the focused-loss variant working — for the templated case, the loss is already focused on completion tokens (this is how the original setup avoids the prompt-token-loss trap of the Berglund paper), so “synth doc + focused loss” is the natural minimal modification.

§9p = 0.6: dense topology + full-rank + L2-SP + on-policy mixin

The natural follow-up to §5–7 is what happens at a denser training graph. At ptrain = 0.6, 18 of 30 directed edges are in training and the 6 held-out test edges all turn out to be 2-hop composable from the training set — the “source has no outgoing edge” pathology from p = 0.4 disappears. Eleven runs comparing the two best p=0.4 LoRA configs, a full-rank LR sweep, full-rank with L2-to-init at three penalty strengths, and full-rank with an on-policy data mixin at two ratios.

The on-policy mixin uses 1024 documents I sampled from base Pythia-70M itself: temp-0.9 / top-p=0.95 completions of 32 diverse generic prompt prefixes (recipes, history, code, philosophy, …) at 96 tokens each. The Trainer draws each batch element from the matching-game training set with probability 1−rmix, else from this corpus. The L2-SP penalty is the standard λ·∑pppinit2, snapshotted at run start.

Bar chart of test-edge accuracy across 11 configs at p=0.6: LoRA, full-rank LR sweep, full-rank + L2-SP, full-rank + on-policy mixin.
Fig. 8 · Test-edge accuracy at ptrain = 0.6 across 11 Pythia-70M configs. All 6 test edges are 2-hop composable from training at this density. LoRA r=16 dp=0.3 reaches 1.00; full-rank lr=2e-4 ties at 0.99. L2-SP gives a small lift over baseline (λ=1e-3 best at 0.97). The two on-policy-mixin configs (bottom, magenta) actively hurt: dropping to 0.54 and 0.47, well below the bottom of the LR sweep.

On-policy data-mixin at this scale is a net negative regulariser. The base model's own samples don't carry matching-game structure, so they're a noisy distractor whose only effect is to dilute the training signal — and at full-rank, that's enough to drop test accuracy by half.

The flip side is positive: concept crystallization isn't a LoRA-specific phenomenon. Full-rank fine-tuning at lr=2e-4 reaches the same test accuracy (0.99) as the best LoRA setting. The undertraining narrative from §3 — that 1.4B/32B fail because LoRA caps the optimisation — is consistent with this: the 70M result is robust to swapping LoRA for full-rank as long as the LR is in the right band. So the next thing to try at 1.4B and 32B is full-rank with the matched LR.

The L2-SP results are mildly positive but not dramatic. The biggest λ tested (1e-3) helped most (0.97 vs. baseline 0.99 at the matched LR), but didn't move the needle further. This is consistent with the L2 penalty being small relative to the task loss at these settings — a larger λ sweep could see whether there's a regime where it actually buys something.

configtype TRAIN accTEST acc
rank16_dp0p3LoRA r=16 α=32 dp=0.3 1.001.00
fullrank_lr2e-4full-rank 0.990.99
fullrank_lr4e-4full-rank 1.000.98
fullrank_l2sp_1e-3full-rank + L2-SP 1.000.97
rank256_a512LoRA r=256 α=512 0.980.94
fullrank_l2sp_1e-4full-rank + L2-SP 0.990.93
fullrank_l2sp_1e-5full-rank + L2-SP 0.990.92
fullrank_lr1e-4full-rank 1.000.86
fullrank_lr5e-5full-rank 1.000.56
fullrank_mixin_20_80full-rank + on-policy mixin 0.990.54
fullrank_mixin_50_50full-rank + on-policy mixin 0.980.47

§10Overnight: Pythia-1.4B cracks p = 0.4

V2 reported that Pythia-1.4B with LoRA r=32 plateaued at test accuracy ~0.37 at ptrain = 0.4 and concluded “concept crystallization disappears at scale”. After the V3 hint that full-rank works at 1.4B when p = 0.6, I left an overnight sweep running across two A100s — 16 configs at p = 0.6 (Batch 1) and 16 configs at p = 0.4 (Batch 2) — covering full-rank LR sweep, L2-SP at four λ, and a LoRA rank ladder.

The earlier “1.4B doesn't crystallize” result was an artifact of LoRA + low LR. Full-rank fine-tuning at lr=2e-4–4e-4 reaches 1.00 test accuracy at p=0.6 (matching 70M) and also lifts non-composable test edges to 0.34 at p=0.4 — the partial-anchor case that stayed at chance at every 70M setting we tested.

Batch 1: p = 0.6 confirms full-rank works at 1.4B

At the denser topology, almost everything works at 1.4B as long as it has enough capacity. Plain full-rank at lr ∈ {2e-4, 4e-4} hits 1.00. L2-SP at every λ ∈ {1e-5, 1e-4, 1e-3, 1e-2} also hits 1.00 — the regulariser is essentially free at this density. Higher-rank LoRA (r ≥ 64) also reaches 1.00; the only LoRA setting that fails is r=16 (stuck at 0.71 even with 20 000 training steps).

Bar chart of 15 configs at Pythia-1.4B p=0.6, almost all near 1.00, two LoRA r=16 stuck around 0.71, lowest-LR full-rank near 0.5
Fig. 9 · Pythia-1.4B at ptrain = 0.6, 15 configs sorted by mean test accuracy. Three regimes: full-rank with adequate LR, high-rank LoRA, and full-rank + L2-SP all top out at 1.00. LoRA r=16 plateaus at 0.71 even at 20 000 steps. Full-rank at lr=5e-5 (LR too low) only reaches 0.52.

Batch 2: p = 0.4 — full-rank lifts non-composable edges above chance

The harder density is where the interesting story emerges. 16 full-rank configs at p = 0.4 covering L2-SP λ ∈ {1e-4, 1e-3, 1e-2, 1e-1} × LR ∈ {1e-4, 2e-4, 4e-4} plus plain-full-rank controls. The best configs reach mean test accuracy 0.67 — roughly 2× the LoRA-r=32 baseline (0.37). What's new vs. the 70M results: non-composable edges lift to 0.30–0.34, where every 70M setting we tested stayed at chance (0.25).

Bar chart of 16 Pythia-1.4B configs at p=0.4, sorted by mean test acc.
Fig. 10 · Pythia-1.4B at ptrain = 0.4, 16 full-rank configs sorted by mean test accuracy. The top of the sweep clusters around 0.65–0.67. The orange bar (plain full-rank lr=2e-4) is near the cluster, showing L2-SP isn't strictly necessary — full-rank with adequate LR is the main driver.
Composable vs non-composable test edges for top 10 configs at Pythia-1.4B p=0.4.
Fig. 11 · The top 10 configs broken down by edge type. Green bars (composable test edges) saturate at 1.00 across the board — the model picks up every 2-hop-composable association cleanly. Orange bars (non-composable) sit at 0.30–0.34, well above the 0.25 chance line. This is the first config in the whole sweep where 1.4B reliably beats chance on the non-composable case. 70M never managed this at p = 0.4 across any setting (rank, regularization, full-rank, or L2-SP).

Two reads of the “non-composable lifts at 1.4B but not at 70M” observation:

I lean towards the second reading because: (i) at p=0.4 plain-full-rank lr=4e-4 with no L2-SP also reaches 0.30 on non-composable, only slightly below L2-SP's 0.34 — so the “stay-near-init” regularisation is a small bonus on top of an already-large effect from full-rank; (ii) at p=0.6, L2-SP, plain-full-rank and high-rank LoRA all converge to the same answer once they have enough capacity. The active ingredient seems to be “don't run out of free parameters during fine-tuning”, and L2-SP is a way to do that while also avoiding catastrophic drift.

Sweep summary

configdensity TRAIN acc TEST mean composable non-composable
full-rank lr=2e-4 + L2-SP λ=1e-3p=0.6 1.001.001.00
LoRA r=64 α=128 dp=0.3p=0.6 1.001.001.00
LoRA r=16 α=32 dp=0.3 (control)p=0.6 1.000.710.71
full-rank lr=2e-4 + L2-SP λ=1e-3p=0.4 1.000.671.000.34
full-rank lr=4e-4 + L2-SP λ=1e-4p=0.4 1.000.671.000.34
plain full-rank lr=4e-4p=0.4 1.000.650.990.30
LoRA r=32 hi-LR (V2 result)p=0.4 1.000.37~0.45~0.27

Caveats unchanged from earlier: single seed, single topology, single density per row. The non-composable lift is a robust signal across the whole top of the p=0.4 sweep (Fig. 11), but I haven't yet confirmed it across topology samples.

A Qwen2.5-32B run with the high-rank-LoRA recipe (4-bit base + LoRA r=256/512 + dropout) is in flight as I publish — code on the branch, results coming in V5.

§11What was built on the branch

Aside from the experimental results, the session left a few pieces of infrastructure that the original code didn't have:

§12What's still on the to-do list

Things still on the docket:

§13V5 ablations: how Qwen-14B went from failing to crystallizing

The Qwen-14B port wasn't “same recipe, scale up.” The first attempt held the 1.4B winning hyperparameters constant and underfit badly. Three ablations triangulated where the fix actually lives.

Ablation 1 — 1.4B recipe held constant (the wrong move)

Qwen-14B, full-rank, L2-SP λ=1e-3, lr=2e-4, embed + lm_head frozen, eff. batch 32, 2000 steps:

stepTRAINTEST
4000.170.34
12000.480.42
20000.450.44

The model doesn't fully memorise the training edges (train tops out at 0.52 at step 1400 then drops back to 0.45 as the cosine LR winds down). Test accuracy mirrors train at 0.44. This is what an underfit run looks like when the L2-SP penalty is fighting the task loss to a draw.

Ablation 2 — is freezing the embeddings hurting? (no.)

Diagnostic: take the Pythia-1.4B configuration that worked at V4 (full-rank, λ=1e-3, lr=2e-4), keep everything identical, but freeze the input embeddings + lm_head. If freezing is what's breaking Qwen-14B, this Pythia-1.4B variant should also break. Result:

stepTRAINTEST
3001.0001.000
15000.9981.000
30000.9980.995

It converges faster than V4's no-freeze run, which itself reached 0.93 test. So freezing the embeddings is essentially free: the matching-game task doesn't need to update token representations because the categories are already lexically distinct in the pretrained vocabulary. We can keep the freeze in the Qwen-14B recipe for the memory win without sacrificing accuracy.

Ablation 3 — scale λ with param count, bump LR, run longer (the fix)

Qwen-14B, full-rank, L2-SP λ=1e-4 (10× smaller, to match the 10× param growth from 1.4B), lr=5e-4, embed + lm_head frozen, eff. batch 32, 4000 steps:

stepTRAINTESTtest composabletest non-comp.
12000.830.66
20000.950.77
40000.940.790.880.33

Concept crystallization at 14B: composable-edge accuracy 0.88, in the same ballpark as Pythia-1.4B's 0.93 in V4. Non-composable accuracy lifts to 0.33 over the 0.25 chance line — comparable to what 1.4B managed at p = 0.4 and consistent with the “full-rank big-enough model preserves enough init structure to leak signal on partial-anchor edges” story.

So the failure mode wasn't “Qwen-14B can't crystallize.” It was “the L2-SP regulariser scales with model size, so you need to retune it.” The lift is large enough (0.44 → 0.79) to be visible after one config; it doesn't need a sweep. The remaining gap to Pythia-1.4B's perfect-score is probably:

Wall-clock — and an aside on bf16=True

The infrastructure side of the V5 work is worth recording too because it surfaces a perennial trap in HuggingFace fine-tuning.

First attempt at Qwen-14B full-param on a single B200 (180 GB): immediate OOM at 145 GB. The model loaded in bf16 at 30 GB, so I expected the rest (grads + paged 8-bit AdamW + activations) to add ∼50 GB at most. The mistake was leaving bf16=True in HF's TrainingArguments — that flag enables mixed-precision AMP, which adds fp32 grads and fp32 master weights even when the model is loaded in bf16. For a 13B-trainable-param run that's ~110 GB on top of the bf16 model. Fix: set bf16=False in TrainingArguments while keeping dtype=torch.bfloat16 on the model load. That's “pure bf16”, the standard recipe for memory-tight runs; HF's bf16=True is a misnomer if you read it as “use bf16”.

The other win was throughput. The first working configuration on B200 was gradient_checkpointing=True, bs=1, grad_accum=16, paged_adamw_8bit — safe, but step time was 5.8 s/step at 48% GPU utilisation. Turning off gradient checkpointing (peak memory was ~75 GB on a 180 GB card, plenty of headroom), bumping to bs=16 / grad_accum=2, and switching from paged to non-paged adamw_8bit (no PCIe traffic on the optimizer state) dropped step time to 0.88 s/step — a 6.6× wall-clock speedup at 94% GPU util. The Qwen-14B v3 run took 58 minutes wall-clock for 4000 steps.

Notes for future me lives in ~/.claude/skills/llm-finetune-perf/SKILL.md. Capsule version: HF's bf16=True is mixed-precision, not pure bf16; turn gradient checkpointing off when memory allows, not on by default; use the largest per_device_train_batch_size that fits and the smallest grad_accum that gives you the effective batch.

code: jonathanbostock/a-is-b-is-c · branch pythia-repro-and-scale · commit pinned in FINDINGS.md.
compute: RunPod — 2× A100 SXM 80GB ($1.49/hr) + 1× H100 SXM 80GB ($3.29/hr) + 1× B200 ($5.89/hr).
last updated 2026-06-30.