LLMModule 08 · Stage 02

Scaling laws & data mixing

Before you train, decide three things: how big the model should be, how many tokens to feed it, and what those tokens should be. Scaling laws answer the first two. Deduplication, domain weights and the learning-rate schedule decide how much each token is worth.

PrerequisitesStage 01, training a small Transformer (nanoGPT level), log-log plots
Time30–60 h part-time, most of it waiting for training runs
HardwareColab GPU for the small version · one rented A100/H100 for the 100M-parameter runs
Key papersKaplan et al. (2020) · Chinchilla, Hoffmann et al. (2022) · DoReMi, Xie et al. (2023) · FineWeb, Penedo et al. (2024)

Source: Suraj Sharma's roadmap post on X, stage 2 · all 12 stages

Start here (no maths needed)

Training an AI costs a fixed amount of computer time. You have to choose how to spend it: a bigger brain that reads less, or a smaller brain that reads more. And you have to choose what it reads.

Think of it like this

Imagine you have exactly one month to get ready for a big exam. You could buy a huge, thick textbook and only get through a quarter of it. Or a thin one you can read cover to cover. The best choice is somewhere in the middle. Then there's the question of what you read. If half your notes are the same page photocopied twenty times, you're wasting time. If the exam is half maths and you only studied history, the mix was wrong. Scaling laws are the rules for the first choice. Data mixing is the second.

How it works

  1. Train many small practice models of different sizes, each for a different amount of time.
  2. Plot how good each one got. The points fall on surprisingly smooth curves.
  3. Use the curves to predict the best size and reading amount for your big, expensive model.
  4. Clean the reading pile: throw out copies and near-copies, and junk pages.
  5. Choose the recipe: what share of the reading is web pages, code, maths and so on. Then train, slowing the learning down gently towards the end.

Why robots care

Robot "brains" are trained on a mixture too: videos from many different robots, some simulation, some internet pictures with captions. If a robot learns mostly from one kind of arm, it may be clumsy with another. Choosing how much of each kind of data to use is the same puzzle as choosing between web text, code and maths.

Try it

Words to know

Parameter
One adjustable number inside the model. Big models have billions.
Token
A chunk of text, roughly three-quarters of a word, that the model reads.
Compute
The total amount of maths spent on training, counted in operations.
Deduplication
Removing copies and near-copies from the training data.
Data mix
The recipe: what share of training data comes from each source.

Going deeper: everything below is the full version, with some maths and code. On a first read, skim the headings and the "Why it matters for robotics" box, and come back for the rest later.

1. What this stage covers

TL;DR

For a fixed training budget C ≈ 6·N·D (N parameters, D tokens), Chinchilla found that N and D should grow in roughly equal proportion, which works out to about 20 tokens per parameter. That's the compute-optimal point for training cost alone. Then the data itself: near-duplicates waste compute and increase memorisation, so you remove them with MinHash. The share of each domain (web, code, maths, books) shifts what the model is good at, and can be tuned by hand, by small-proxy ablations, or by methods like DoReMi. Finally, the learning-rate schedule (warmup then cosine decay, with restarts or a warmup-stable-decay variant) has to match the run length, or every comparison above is biased.

2. Concepts, explained

Chinchilla-optimal compute

Kaplan et al. (2020) showed that loss falls as a power law in model size, data and compute, and concluded that extra compute should go mostly into a bigger model. Their setup, though, used one learning-rate schedule length for runs of different lengths. Hoffmann et al. (2022) redid the study with over 400 models (70M to 16B parameters, 5B to 500B tokens) and schedules matched to each run's length. Their answer: scale parameters and tokens equally. Double the model, double the data. They tested it by training Chinchilla (70B parameters, 1.4T tokens) on the same compute as Gopher (280B parameters, 300B tokens). Chinchilla beat Gopher on a wide range of tasks.

The paper used three estimation methods. The third fits a parametric loss surface:

L(N, D) = E + A / N^α + B / D^β
published fit: E = 1.69, A = 406.4, B = 410.7, α = 0.34, β = 0.28

C ≈ 6·N·D           (training FLOPs: ~2N forward + ~4N backward per token)
rule of thumb:  D ≈ 20·N   →   N ≈ √(C / 120)

E is the irreducible loss of the data. The A term is the penalty for a model that's too small. The B term is the penalty for too little data. Two caveats matter. First, the methods don't agree exactly: the first two give roughly equal exponents, while the published third fit leans toward smaller models. Besiroglu et al. (2024) tried to replicate the third method and found the published fit inconsistent with the paper's own data. Their refit agrees with the first two methods. Second, the loss constants depend on the dataset and tokenizer, so they don't transfer to your data. Fit your own on small runs (that's CS336 Assignment 3).

"Compute-optimal" means optimal for training cost. If a model will serve billions of queries, a smaller model trained on far more tokens is cheaper overall. Meta's Llama 3 8B, for example, was trained on about 15T tokens, nearly 2,000 tokens per parameter. Read Chinchilla as the starting point for an ablation budget, not a law about production models.

Interactive: compute-optimal model size

The curve is predicted loss against model size for one fixed compute budget (an "IsoFLOP" curve), using Chinchilla's published parametric fit. Every point spends the same compute: a bigger model gets fewer tokens. Ring: the fit's minimum. Square: the 20-tokens-per-parameter rule. Filled dot: your choice.

Tokens D = C / 6N
–
Tokens per parameter
–
Predicted loss
–
20× rule: N, D
–
Fit minimum: N, D
–
Loss above minimum
–

Try this: the default, about 1.2 × 1018 FLOPs, is the budget for the 100M-parameter practice project at 20 tokens per parameter (2B tokens). The fit's minimum sits a little lower, near 90M parameters, and the two predicted losses differ by less than 0.002. Now move N a factor of 3 either way and see how slowly the loss rises. Push C to about 5.8 × 1023 (Gopher and Chinchilla's budget): the rule says ~69B parameters, while the published fit says ~32B. That gap is the inconsistency Besiroglu et al. pointed out. The losses are only meaningful for the paper's own dataset and tokenizer.

Data deduplication with MinHash

Web crawls are full of exact and near-exact copies: boilerplate, mirrored pages, templated spam. Lee et al. (2021) found a single 61-word sentence repeated over 60,000 times in C4. They showed deduplication cut verbatim memorised output roughly tenfold, needed fewer training steps for the same or better accuracy, and reduced train/test overlap. That overlap had affected over 4% of some validation sets.

Exact duplicates are easy: hash each document. Near-duplicates need MinHash (Broder, 1997). Turn each document into a set of shingles (for example word 5-grams). Similarity is the Jaccard index |A ∩ B| / |A ∪ B|. For a random hash function, the probability that two sets have the same minimum hash value equals their Jaccard similarity. So a signature of k minimum hashes estimates it. To avoid comparing every pair, split the signature into b bands of r rows and only compare documents that match exactly on at least one band (locality-sensitive hashing):

P(candidate pair | Jaccard s) = 1 − (1 − s^r)^b

with b = 16 bands, r = 8 rows (k = 128 hashes):
s = 0.5 → 0.06    s = 0.7 → 0.61    s = 0.8 → 0.95    s = 0.9 → 1.00

That S-shaped curve is the point: pairs above the threshold are almost always caught, pairs well below it almost never. You tune b and r to move the threshold. The FineWeb team (Penedo et al., 2024) ran ablations on MinHash settings and found that deduplicating each Common Crawl snapshot separately worked better than deduplicating across all snapshots at once.

Domain weighting

A pre-training corpus is a mixture of domains: web pages, code, maths, books, papers, other languages. The weights decide what the model is good at, and they interact with how much unique data each domain has. Upweighting a small domain means repeating it. Three ways to set them:

Quality filtering interacts with all of this. FineWeb-Edu, a 1.3T-token subset of FineWeb chosen by an educational-quality classifier, gave much better results on knowledge and reasoning benchmarks like MMLU and ARC. Often "which documents" matters as much as "which domain ratio".

Learning-rate schedules: cosine, warm restarts and WSD

Standard LLM pre-training uses a short linear warmup, then cosine decay to a fraction of the peak. Loshchilov & Hutter (2016) introduced SGDR, cosine annealing with warm restarts. The rate decays along a cosine, jumps back to the peak, and repeats, often with each cycle longer than the last. They showed it on CIFAR image models. In LLM pre-training, restarts are less common than a single decay. Chinchilla noted that letting the cosine cycle run much longer than the actual training run gives a worse model. That's one reason Kaplan's allocation was off: a run cut short in the middle of a long schedule never gets its final decay.

A popular alternative is warmup-stable-decay (WSD), described in the MiniCPM paper (Hu et al., 2024). Hold the rate constant, and decay sharply only at the end. You can branch a decay off any checkpoint, which makes it cheap to measure loss at several data sizes from one run.

def lr(step, peak=6e-4, warmup=500, total=20_000, floor=0.1):
    if step < warmup:
        return peak * step / warmup
    t = (step - warmup) / (total - warmup)
    return peak * (floor + (1 - floor) * 0.5 * (1 + math.cos(math.pi * t)))

def sgdr(step, peak=6e-4, t0=2_000, mult=2):
    t, T = step, t0
    while t >= T:              # find the position inside the current cycle
        t, T = t - T, T * mult
    return peak * 0.5 * (1 + math.cos(math.pi * t / T))

3. The practice project

(a) Colab version: a ~15M-parameter model, three mixes

  1. Data. Take three public sources from the Hugging Face Hub: a web sample (for example a FineWeb-Edu sample), a code set (for example a small slice of The Stack) and a maths set (for example OpenWebMath). Tokenize all of them with the same tokenizer (GPT-2's via tiktoken is fine). Loss per token isn't comparable across tokenizers.
  2. Clean. Run exact-hash and MinHash deduplication within each source. Then MinHash your validation sets against the training sets and drop overlaps, so you aren't measuring memorisation.
  3. Mixes. Build three training streams with the same total tokens, for example web/code/maths at 80/10/10, 40/40/20 and 40/20/40.
  4. Train. Use a nanoGPT-style model of about 15M parameters for about 300M tokens each (20 tokens per parameter). That's C ≈ 6 × 1.5×107 × 3×108 ≈ 2.7×1016 FLOPs per run, small enough for a Colab GPU in roughly an hour or a few per run, depending on how efficient your code is. Use the same seed, schedule and batch size for all three.
  5. Measure. Every few hundred steps, log validation loss separately on held-out web, code and maths. Plot nine curves (3 mixes × 3 domains), and the difference between mixes per domain over time.

What to expect: each mix should usually do best on the domain it upweights. The interesting parts are how big the gaps are, when they open, and whether one domain helps another (does code help maths?). Run at least two seeds for one mix. If seed-to-seed differences are as big as mix-to-mix differences, you haven't measured anything yet.

A MinHash sketch to start from, standard library only:

import hashlib

def shingles(text, n=5):
    w = text.lower().split()
    return {" ".join(w[i:i + n]) for i in range(len(w) - n + 1)}

def h(s, seed):
    return int.from_bytes(hashlib.blake2b(f"{seed}:{s}".encode(), digest_size=8).digest(), "big")

def minhash(sh, k=128):
    return [min(h(s, i) for s in sh) for i in range(k)]

def est_jaccard(a, b):
    return sum(x == y for x, y in zip(a, b)) / len(a)

def p_candidate(s, bands=16, rows=8):   # LSH: chance two docs share ≥ 1 band
    return 1 - (1 - s ** rows) ** bands

For real volumes, use datatrove, the library behind FineWeb. It has MinHash dedup stages that run in parallel.

(b) The full version from the post: ~100M parameters

(c) Pitfalls

4. Reality check

The post's claim: in 2026, architecture is secondary, and the real advantage is the exact mathematical ratio of the training data.

5. In this course

6. Why it matters for robotics

7. Self-check

Using C ≈ 6ND and D = 20N, what's the compute-optimal model size for C = 1.2 × 1018 FLOPs?

C = 120N², so N = √(C / 120) = √(1016) = 108: a 100M-parameter model on 2B tokens.

Why did Kaplan et al. and Hoffmann et al. reach different allocations?

A major reason: Kaplan's runs used a fixed learning-rate schedule length, so shorter runs were evaluated before their schedule finished decaying, which penalised training on more data. Chinchilla matched the cosine schedule to each run's length and found parameters and tokens should scale roughly equally.

Why is a MinHash collision probability equal to the Jaccard similarity?

Under a random hash, each element of A ∪ B is equally likely to have the minimum value. The two sets share their minimum exactly when that element is in A ∩ B, which happens with probability |A ∩ B| / |A ∪ B|.

With 16 bands of 8 rows, roughly what share of pairs at Jaccard 0.5 become candidates? At 0.9?

About 6% at 0.5, and essentially all at 0.9. The banding makes a sharp threshold around 0.7.

Your code-heavy mix has the lowest average validation loss. Is it the best mix?

Not necessarily. Code tokens are easier to predict, so a code-heavy validation set lowers the average. Compare per-domain losses (and downstream evals) instead.

Why might a team train an 8B model on 15T tokens when Chinchilla says ~160B tokens?

Chinchilla minimises training compute for a given loss. When a model will be served at huge volume, a smaller, over-trained model gives similar quality at much lower inference cost, which outweighs the extra training compute.

8. Courses & content

ResourceTypeUse it for
Stanford CS336 · Assignment 3 (Scaling) + Assignment 4 (Data)University course (free materials)Fit a scaling law; turn Common Crawl into training data with filtering and dedup
FineWeb: decanting the web for the finest text dataLong-form guideDedup (MinHash), quality filtering and the ablations behind each choice
datatroveLibraryThe data-processing pipeline used for FineWeb
Karpathy · build-nanogpt (reproduce GPT-2 124M)Video + code (free)A full small pre-training run with LR warmup and cosine decay
Karpathy · nanochatCode (free)End-to-end small ChatGPT: pre-train, mid-train, SFT, RL, on a small budget

Start with the FineWeb write-up: it walks through deduplication, filtering and mixing with real ablation results, so you see which choices actually moved the numbers before you spend GPU time.

Full course map for all 12 stages →

9. Papers & docs