Lab 08 — Scaling laws¶
Build: a power-law fitter, the Chinchilla loss surface, the closed-form compute-optimal allocation,
and inference-aware model sizing. Then run your own IsoFLOP study on your GPU and fit your own exponent.
Time: 3–4 h for the tests + an overnight scale-up run · Reads first: pretraining §3
Run: pytest labs/08_scaling_laws (your code) · pytest labs/08_scaling_laws --impl=solution (reference) · CPU only, seconds
Why this matters¶
Every large training run starts with a scaling-law argument: how big a model, how many tokens, what loss to expect, and whether a change that helps at 100M parameters will still help at 10B. Getting it wrong is expensive: by Chinchilla's analysis, GPT-3 (175B parameters, 300B tokens) and Gopher (280B parameters, 300B tokens) were much larger than optimal for their compute. This lab gives you the tools to make and check these arguments yourself.
The math¶
The loss surface (Hoffmann et al. 2022, approach 3):
Fitting a power law with an offset. \(L(N) = E + A N^{-\alpha}\) is nonlinear in \(E\), but for a fixed \(E\) it is linear in log space:
So grid-search the single nonlinear parameter \(E\) over \([0, \min L)\), solve ordinary least squares for \((\log A, \alpha)\) at each grid point, and keep the \(E\) with the smallest residual. Fitting in log space weights relative errors equally, which is what you want when losses span a range. This is more robust than a generic nonlinear optimizer, which often lands in a bad local minimum with \(E\) and \(A\) trading off against each other.
Compute-optimal allocation. With \(C = 6ND\), substitute \(D = C/(6N)\) and set \(dL/dN = 0\):
Inference-aware sizing (Sardana et al. 2023). A model that will serve \(I\) tokens costs \(6ND\) to train and about \(2NI\) to serve. For a target loss \(\ell\), the tokens needed at size \(N\) are
and you minimize \(6N D(N) + 2NI\) over \(N\). More inference pushes the optimum to smaller models trained on more tokens.
What to implement¶
| function | contract | test |
|---|---|---|
fit_power_law(N, L, n_grid) |
return (E, A, alpha); grid over E in [0, min(L)), least squares in log space |
test_fit_recovers_known_law: recovers E = 1.8 (±0.1) and α = 0.34 (±0.04) from 12 points over 4 decades with 0.2% noise |
chinchilla_loss(N, D, ...) |
E + A/N^α + B/D^β, vectorized over NumPy arrays |
test_loss_decreases_in_both_axes |
compute_optimal(C, ...) |
closed form above; return (N*, D*) with 6·N*·D* = C |
test_closed_form_matches_grid_search at C = 1e20 and 5.76e23: within 1% of a 20,000-point grid search |
inference_aware_optimum(target_loss, inference_tokens, ...) |
over n_grid (default 1e7–1e12), skip N that cannot reach the target, solve D(N), minimize 6ND + 2NI; ValueError if nothing reaches the target |
test_more_inference_means_smaller_models_trained_longer: I = 1e13 gives smaller N and larger D than I = 0, and the loss hits 2.2 to 1e-6 |
Worked numbers (check yours against these)¶
compute_optimal with the default constants:
| C (FLOPs) | N* | D* | tokens/param |
|---|---|---|---|
| 1e20 | 0.645B | 25.9B | 40 |
| 1e22 | 5.16B | 323B | 63 |
| 5.76e23 | 32.2B | 2.98T | 93 |
Chinchilla itself (5.76e23 FLOPs) was 70B parameters on 1.4T tokens. The gap is real: the published approach-3 fit implies \(N^* \propto C^{0.45}\) and a drifting tokens/param ratio, while Hoffmann et al.'s approaches 1 and 2 give \(\approx C^{0.5}\) and ~20 tokens/param. A replication by Epoch AI (Besiroglu et al. 2024) traced the problem to the approach-3 fit itself. See curriculum 05 §3.
inference_aware_optimum(2.2, I):
| I (tokens served) | N | D | tokens/param |
|---|---|---|---|
| 0 | 3.54B | 204B | 58 |
| 1e12 | 1.73B | 511B | 295 |
| 1e13 | 0.97B | 1.80T | 1,856 |
| 1e14 | 0.65B | 8.17T | 12,628 |
Common bugs¶
- Grid reaching min(L).
log(L − E)is undefined at \(E = \min L\). Stop the grid just below it. - Fitting in linear space. A least-squares fit of \(L\) itself is dominated by the largest losses (the smallest models); fit \(\log(L - E)\).
- Sign of α. The slope of \(\log(L-E)\) against \(\log N\) is \(-\alpha\).
- Forgetting the 6. \(N^*\) uses \(C/6\), and \(D^* = C/(6N^*)\), not \(C/N^*\).
- Unreachable targets. For small \(N\), \(\ell - E - AN^{-\alpha} \le 0\): no amount of data reaches the target. Skip those N instead of taking the log or power of a negative number.
- Inference cost of 6N per token. Serving is a forward pass only: about \(2N\) FLOPs per token.
Tip
Write the brute-force version first (a dense grid over N for compute_optimal, as the test
does) and use it to check the closed form. Keep both: the grid is how you will sanity-check any
scaling-law claim you meet later.
Scale-up run S2: your own IsoFLOP curves (RTX 5060, overnight)¶
Train a family of byte-level models from lab 05 at three compute budgets, find the loss-minimizing size at each budget, and fit the exponent \(a\) in \(N^* \propto C^a\). Chinchilla's approaches 1 and 2 give \(a \approx 0.5\); what does your setup give?
1. Define compute with the script's own count. labs/05_transformer/train.py prints
MFLOPs/token, including attention and the LM head, which matter at this size (attention is ~13% of
the total for the smallest model below). Use \(C = \text{FLOPs/token} \times \text{tokens}\), not \(6N\).
2. Pick sizes and budgets. Byte-level vocabulary (256) keeps embeddings negligible. With
--block-size 256 --batch-size 64 (16,384 tokens/step), steps = C / (FLOPs/token × 16,384):
--d-model / --n-layer / --n-head |
params | MFLOP/token | steps at C = 1e15 | 3e15 | 1e16 |
|---|---|---|---|---|---|
| 128 / 4 / 2 | 885,888 | 6.1 | 10,003 | 30,009 | 100,029 |
| 192 / 6 / 3 | 2,705,856 | 18.0 | 3,390 | 10,170 | 33,900 |
| 256 / 8 / 4 | 6,492,416 | 42.1 | 1,450 | 4,349 | 14,498 |
| 384 / 8 / 6 | 14,260,608 | 90.3 | 676 | 2,028 | 6,760 |
| 512 / 10 / 8 | 32,254,464 | 201.4 | 303 | 909 | 3,031 |
Each budget needs points on both sides of its minimum. Drop cells with very few steps (under ~500) and add a size if a budget's minimum sits at the edge of your range.
3. Run each cell with its own schedule. train.py warms up for --warmup steps and then decays
the LR with a cosine that ends exactly at --max-steps, so setting --max-steps per run gives each model a
full schedule sized to its own run length, as in Chinchilla's IsoFLOP protocol. Keep --warmup
small relative to the shortest run (e.g. 100 steps): Porian et al. found overlong warmup biases
scaling fits against small models. Set --eval-interval to --max-steps so the script evaluates
only at the start and the end, and raise --eval-iters (e.g. 50) for a less noisy final
validation loss. Give each run its own --out directory.
python labs/05_transformer/train.py --data data/TinyStoriesV2-GPT4-train.txt \
--d-model 256 --n-layer 8 --n-head 4 --block-size 256 --batch-size 64 \
--max-steps 4349 --warmup 100 --eval-interval 4349 --eval-iters 50 \
--out runs/isoflop/C3e15_d256
4. Predict the wall-clock time first. The whole grid is about \(5 \times (1 + 3 + 10)\times10^{15} = 7\times10^{16}\) FLOPs. Divide by your measured throughput (from S1) to get hours; tiny models run at low MFU, so expect the 100k-step cells to be dominated by per-step overhead.
5. Fit. For each budget, fit a parabola to final validation loss against \(\ln N\) and take the
vertex as \(N^*(C)\). Then fit a line to \(\ln N^*\) against \(\ln C\); the slope is \(a\). Also fit
fit_power_law to the lower envelope of loss against N. Report the exponent with a bootstrap
interval (resample the runs within each budget) and at least two seeds for one budget, so you know
your noise floor.
6. Write it up with the experiment template: setup, your predicted exponent, the measured one with its interval, and what limits the comparison with Chinchilla (byte-level tokens, one dataset, tiny models, a fixed batch size and LR).
Tip
Before the full grid, run the smallest and largest model of one budget at two learning rates each. If the best LR differs across sizes, per-size LR tuning matters for your fit, which is one of the three causes of the Kaplan–Chinchilla disagreement.
CPU vs GPU notes¶
The four functions and their tests are NumPy and run on any CPU in seconds. S2 needs a GPU for the
budgets above. On CPU, scale everything down about 30x (budgets of 3e13–3e14 FLOPs, the three
smallest sizes, a shorter --block-size), and expect noisier fits; the method is the same.
Check yourself¶
- Why grid-search E rather than fit all three parameters with a generic nonlinear optimizer?
- Derive \(N^*(C)\) from the loss surface and the constraint \(C = 6ND\).
- With the default constants, what is the exponent \(a\) in \(N^* \propto C^a\), and why does it make tokens/param drift upward with compute?
- Why must each IsoFLOP run have its own LR schedule sized to its own length?
- Your fitted exponent is 0.62 with a bootstrap interval of ±0.15. What do you report, and what do you change?
- Why does
inference_aware_optimumsometimes skip grid points, and when does it raise? - A startup will serve 1e13 tokens. The compute-optimal model for their training budget is 3.5B. What does the inference-aware view suggest, and what does it cost in training?
Answers
- For fixed E, the problem is linear least squares in log space with a closed-form solution, so a 1-D grid over E finds the global optimum reliably. Joint nonlinear fits are sensitive to initialization because E, A and α trade off, especially when N spans few decades.
- Substituting \(D = C/(6N)\) and setting the derivative to zero gives \(\alpha A N^{-\alpha} = \beta B (C/(6N))^{-\beta}\), so \(N^{\alpha+\beta} = (\alpha A/\beta B)(C/6)^\beta\) and \(N^* = G(C/6)^{\beta/(\alpha+\beta)}\).
- \(a = 0.28/0.62 = 0.45\), so \(D^* \propto C^{0.55}\) and \(D^*/N^* \propto C^{0.10}\), which rises slowly with compute (40 at 1e20, 93 at 5.76e23).
- A run cut from a longer cosine schedule is evaluated before its LR has decayed, so its loss is worse than a properly finished run of that length. Shorter (larger-model) runs are penalized more, which biases the optimum toward smaller models. Hoffmann et al. suspected this biased Kaplan et al.'s fits; Porian et al. later found it was not the main cause, but matched schedules remain the clean protocol.
- Report 0.62 ± 0.15 and say it is consistent with 0.5. To tighten it: add budgets to widen the compute range, add sizes near each minimum, run more seeds, and increase
--eval-iters. - For small N, \(E + AN^{-\alpha}\) is already above the target, so no finite D reaches it; those N are skipped. If no N on the grid can reach the target, it raises
ValueError. - At loss 2.2 the table gives ~0.97B trained on ~1.8T tokens instead of 3.5B on ~204B. Training FLOPs rise (6·0.97e9·1.8e12 ≈ 1.05e22 vs 6·3.54e9·2.04e11 ≈ 4.3e21), but serving 1e13 tokens costs 2·0.97e9·1e13 ≈ 1.9e22 instead of 7.1e22, so total cost falls from ~7.5e22 to ~3.0e22 FLOPs, about 60% less.
Stretch¶
- Approach 3 on your own data. Fit the full \(L(N, D)\) surface to all S2 runs by minimizing a Huber loss on \(\log L\) (as Hoffmann et al. did), and compare its implied exponent with your IsoFLOP estimate.
- Reproduce the Kaplan effect in miniature. Rerun a few S2 cells with
--tokenizer gpt2, where the embedding table (50,257 × d) is most of a small model's parameters. Fit \(N^*(C)\) once counting all parameters and once counting only non-embedding parameters. Which exponent looks more like Kaplan's? - WSD instead of cosine. Copy
train.py, changelr_atto warmup–stable–decay, and branch cooldowns from one long run at several token counts. Compare the cost of your IsoFLOP study both ways. - Data-constrained scaling. Restrict the data file to 20M bytes and train past 4 epochs. Where does repetition start to hurt compared with Muennighoff et al.'s findings?