Skip to content

Deep DivesLab

Lab · Step 1 · part 4 of 4

Lab, Step 1 (part 4): pull one piece out of a small GPT and see what you lose

Part 4 of Step 1: ablate a tiny GPT one component at a time, measure the damage against a seed-noise baseline, and learn which pieces are load-bearing and which are decoration at this size.

8 min read · 1.9 h hands-on
On this page
  1. The 5-minute version
  2. Do it (about 90 minutes)
  3. Expected results
  4. What to observe
  5. Step complete when
  6. Quiz
  7. Try this
  8. Evidence

Take a transformer block apart and ask which piece you’d miss least. Most people say the heads, or the weight decay, or “some of the norms”. Then they never check.

Part 3 ended with a promise: remove one architectural piece at a time and measure what it was worth. That’s this post. The twist is that the first experiment isn’t an ablation at all. It’s the unchanged model run again with different seeds, because without that you can’t tell a finding from a coin flip.

The 5-minute version

Where we are. Step 1 of the ladder (GPT from scratch). The classic paper is still Attention Is All You Need. Today’s job is method rather than a new paper: how to run an ablation you can defend.

The method in four rules.

  1. Measure the noise first. Same code, same data, two more seeds. If the baseline moves by 0.013 between seeds, a 0.01 improvement from your “idea” is nothing.
  2. Change one thing. Each ablation is a flag that removes exactly one piece; everything else, including the schedule, stays put.
  3. Say what changed besides the thing. Removing the MLP also removes 65% of the parameters. An ablation that shrinks the model is a different question from one that doesn’t.
  4. Report the size and budget. A ranking from 0.8M parameters and 2,000 steps is a ranking for that.
flowchart LR
  B[Baseline, seed 1337] --> N[Same model, seeds 2 and 3]
  N --> W[Noise band]
  B --> A[One flag removes one piece]
  A --> D[Delta vs baseline]
  W --> V{Delta bigger than the band?}
  D --> V
  V -->|yes| R[A finding]
  V -->|no| X[A non-result]

What we found (validation loss after 2,000 steps; lower is better; one run each, so read differences under about 0.02 as noise):

Validation loss after 2,000 steps (our run, 0.8M-parameter GPT)

00.61.31.92.5Val loss, Baseline: 1.8Val loss, Seed 2: 1.9Val loss, Seed 3: 1.8Val loss, No weight decay: 1.8Val loss, One head: 1.8Val loss, No MLP: 2.0Val loss, No position emb.: 2.0Val loss, No LayerNorm: 2.1Val loss, No attention: 2.5BaselineSeed 2Seed 3No weight decayOne headNo MLPNo position emb.No LayerNormNo attentionVariantVal loss
VariantVal loss
Baseline1.8427
Seed 21.856
Seed 31.8478
No weight decay1.8372
One head1.8397
No MLP1.981
No position emb.2.0308
No LayerNorm2.1381
No attention2.4741
Source: our run, labs/lab/2026-11-01/run.json. The first three bars are the same model with seeds 1337, 2 and 3.
0.013spread of val loss across three seeds of the unchanged model
0.63val loss added by removing attention (2.474 vs 1.843)
0.30added by removing every LayerNorm (2.138 vs 1.843)
~0.00difference from one head or no weight decay: inside the noise

Why a senior architect should care. Reviews are full of claims of the form “we removed X and nothing changed” or “X is essential”. Both are ablation claims. Asking for the seed spread, the parameter count of the ablated model and the training budget takes thirty seconds and filters out most of them.

Do it (about 90 minutes)

Rung 13: the baseline and its noise (15 min)

The model is the same pre-norm GPT as parts 1–3: four layers, four heads, width 128, character vocabulary of 65, 816,128 parameters, 2,000 steps of AdamW with warmup and cosine decay. Run it three times:

cd labs/lab/2026-11-01
pip install -r requirements.txt
python3 run.py --results base.json                # seed 1337
python3 run.py --seed 2 --results seed2.json
python3 run.py --seed 3 --results seed3.json

Our three val losses were 1.8427, 1.8560 and 1.8478: a range of 0.013. Hold that number in your head for the rest of the post.

Rung 14: the ablation flags (25 min)

--ablate takes one of six values. Each is a few lines in run.py; read them before you run them:

self.pos  = None if a.ablate == "no_pos"  else nn.Embedding(a.block_size, a.n_embd)
self.attn = None if a.ablate == "no_attn" else CausalSelfAttention()
self.mlp  = None if a.ablate == "no_mlp"  else nn.Sequential(
    nn.Linear(a.n_embd, 4 * a.n_embd), nn.GELU(), nn.Linear(4 * a.n_embd, a.n_embd))
norm = (lambda: nn.Identity()) if a.ablate == "no_ln" else (lambda: nn.LayerNorm(a.n_embd))
# one_head sets n_head = 1 at the same width; no_wd sets weight decay to 0.0 on the matrices

Because the blocks add each sublayer back onto the residual stream, deleting a sublayer leaves a valid network. That is also why ablation is easy here and harder in a model with tangled wiring.

Rung 15: run all six and fill the table (30 min)

for v in no_pos one_head no_mlp no_attn no_ln no_wd; do
  python3 run.py --ablate $v --results $v.json
done

For each, write down val loss, the delta against the seed-1337 baseline, and the parameter count. The parameter count is not decoration: it tells you whether you removed a capability or just removed capacity.

Rung 16: say what you expect before you look (20 min)

Before reading our numbers, rank the six by damage and write it down. The exercise is to find where your intuition and the measurement disagree. Then read on.

Expected results

The gates in expected.json check the baseline only; the ablations are measured, not gated, because their sizes depend on the machine’s seed behaviour. The first block is the recorded run of the unchanged model:

CheckGateOur run
Starting val loss near ln(65) = 4.174between 4.0 and 4.44.181
Baseline val loss after 2,000 stepsat most 1.951.8427
Val minus train lossat most 0.250.1408

The measured ablations (our run, CPU, torch 2.14.1; delta against the seed-1337 baseline of 1.8427):

VariantParametersVal lossDeltaRead it as
Baseline, seeds 1337 / 2 / 3816,1281.8427 / 1.8560 / 1.8478noise: 0.013the yardstick
No weight decay816,1281.8372-0.005non-result
One head instead of four816,1281.8397-0.003non-result
No MLP (attention only)289,2801.9810+0.138real, but 65% fewer parameters
No position embeddings807,9362.0308+0.188real
No LayerNorm anywhere813,8242.1381+0.295real
No attention (MLP only)553,9842.4741+0.631large

Your numbers will differ in the third decimal on other hardware or library versions. The ordering, and which rows sit inside the noise band, should not.

What to observe

Attention is the one piece you can’t do without, and the reason is mundane. Without it every position is processed alone, so the model can only learn “which character tends to follow this one”. The MLP-only model’s train and val losses are almost identical (2.468 and 2.474) because there’s nothing to overfit with, and gradient clipping fired on only 65 of 2,000 steps against about 1,950 for the baseline.

LayerNorm costs more than position embeddings, even in a four-layer model. The no-LayerNorm run still trained, but clipping fired on 1,711 steps and it ended 0.30 behind. This is a shallow network with a tuned small learning rate. Xiong et al. (ICML 2020) argue where normalisation sits changes gradient behaviour at initialisation and whether warmup is needed; our run says nothing about deep stacks, where the effect should be larger, not smaller.

The model without position embeddings still learns. It ends 0.19 behind, not at the bigram floor. The causal mask means position t attends to exactly t tokens, which leaks order. Kazemnejad et al. (NeurIPS 2023) find that decoder-only models with no positional encoding (NoPE) can represent both absolute and relative position, and generalise to longer sequences better than several explicit encodings on their reasoning and math tasks. Our block size is fixed at 64, so we measured none of that. For a production angle, Hugging Face’s SmolLM3 write-up (July 2025) drops rotary embeddings in every fourth layer, following Yang et al. (2025), and reports better long-context performance without a short-context cost, on a 3B model trained on 100B tokens.

One head versus four: no difference here. Don’t generalise it. Michel, Levy and Neubig (NeurIPS 2019) report that many heads can be removed from a trained model at test time with little loss; that’s pruning after training, a different question from training with one head. Our result says only that at width 128 and 2,000 steps, four patterns weren’t worth more than one. With a vocabulary of 65 characters and a block of 64, there may not be much for extra heads to specialise on.

Weight decay: no difference, and the reason is the horizon. D’Angelo et al. (2023) argue that for LLM-style, close-to-one-epoch training, weight decay matters for optimisation dynamics and stability, not as a regulariser. Our run is 2,000 steps at batch 12 over a 1M-character corpus, nowhere near that regime, so a null result is the expected one. SmolLM3 reports turning weight decay off on the embeddings only, borrowed from OLMo 2, for stability.

The MLP result needs a footnote. Dropping it removed 527,000 parameters and cost 0.14. Per parameter that’s cheap, but this is not a controlled comparison: the right control is an attention-only model widened to 816k parameters, which this lab doesn’t run. Geva et al. (EMNLP 2021) describe feed-forward layers, about two-thirds of a transformer’s parameters, as key-value memories of patterns in the training data. A 1M-character corpus doesn’t need much memory.

Step complete when

You can, without looking at the code:

  • Reproduce the baseline gates: starting loss between 4.0 and 4.4, val loss at most 1.95, val minus train at most 0.25.
  • Report your own seed-to-seed spread and use it to say which of your six ablations are findings and which are non-results.
  • Explain why the no-position model still beats the no-attention model, and what the experiment does not tell you about long contexts.
  • Name one ablation in this lab that changed the parameter count, and why that muddies the comparison.

If you submit a run with python3 scripts/submit_lab.py submit labs/lab/2026-11-01 --results base.json --notes "...", the gates are checked for you.

Quiz

Check yourself

1 The baseline moves by 0.013 across three seeds and your new idea improves val loss by 0.008. What do you conclude?
Nothing yet. It is inside the noise band. Run more seeds, or a longer or larger setup, before claiming a gain.
2 Removing the MLP cost 0.14 val loss and removed 65% of the parameters. Why isn't that proof the MLP is cheap?
Capacity changed too. A fair control is an attention-only model widened to the same parameter count; otherwise you can't separate 'the MLP is unnecessary' from 'a smaller model is a bit worse'.
3 The model without position embeddings trains fine. Why, and what does that not show?
The causal mask lets position t attend to exactly t tokens, which leaks order. It does not show the model generalises to longer sequences; our block size was fixed at 64.
4 Four heads and one head tie at width 128. Does that mean production models don't need many heads?
No. One size, one budget and a 65-character vocabulary. It says only that this setup has little for extra heads to do.

Try this

Pick one component in a system you own (a reranker, a cache, a guardrail, a prompt section) and run the same protocol: the unchanged system on three seeds or three sample sets, then with the piece removed. If the spread is bigger than the difference, you’ve learned the test is too weak, which is also worth knowing.

Evidence

SourceTypeDateWhat we checked
labs/lab/2026-11-01/run.json, output.txtour run1 November 2026Ran all nine configurations (baseline, two extra seeds, six ablations); every loss, delta and parameter count in this post is copied from it
Attention Is All You NeedprimaryJune 2017Cited as the lab’s classic paper; not re-read for this post
Xiong et al., On Layer Normalization in the Transformer ArchitectureprimaryFeb 2020 (ICML 2020)Read the abstract page: Pre-LN gradient behaviour at initialisation and warmup. We did not reproduce it
Kazemnejad et al., The Impact of Positional Encoding on Length GeneralizationprimaryMay 2023 (NeurIPS 2023)Read the abstract page and Comments field: NoPE claims. We did not test length generalization
Michel et al., Are Sixteen Heads Really Better than One?primaryMay 2019 (NeurIPS 2019)Read the abstract page: test-time head pruning. The abstract page gives no percentage, so we quote none
D’Angelo et al., Why Do We Need Weight Decay in Modern Deep Learning?primaryOct 2023, revised Nov 2024Read the abstract page: the near-one-epoch LLM claim
Geva et al., Transformer Feed-Forward Layers Are Key-Value MemoriesprimaryDec 2020 (EMNLP 2021)Read the abstract page: the key-value memory framing and the two-thirds parameter share
SmolLM3 blog postprimary8 July 2025Read the page: NoPE in every fourth layer and embedding weight-decay change; the post gives no numeric ablation scores

Skipping a rung is fine. The ablations will still be there next Sunday.

Researched and drafted with AI, checked against the linked sources. How we use AI →