Building GPT-2 from scratch
A complete GPT-2 training system — tokeniser, forward pass, backprop, CUDA kernels, multi-GPU ZeRO-1, checkpointing — rebuilt one layer at a time, in C and CUDA, with no framework underneath.
Start with A0 All exercises Source on GitHub
What this is
llm.c by Andrej Karpathy trains GPT-2 in plain C and CUDA, with no PyTorch at runtime. It is one of the clearest existing answers to the question what is a training loop actually doing? — but reading a finished implementation and being able to write one are very different skills, and reading it is the easy one.
So I took the codebase apart. Every function body that contains a real idea has been replaced with a stub:
void manual_seed(mt19937_state* state, unsigned int seed) {
// TODO (A0) - Initialize the Mersenne Twister PRNG state from a seed value
// HINT: Set MATRIX_A constants, then fill state_ array using a linear
// congruential recurrence
...
}
What is left is the scaffolding: struct layouts, function signatures, the call graph, the build system, the tests. The architecture is given; the substance is not. Filling it back in is the exercise.
Why stubbing, specifically
A from-scratch rewrite spends most of its time on things that teach you nothing
— argument parsing, file formats, Makefile incantations, which of your four
bugs is the one making the loss curve flat. Stubbing keeps the parts of the
system that are merely work and removes only the parts that are ideas. Every
hour goes into the fused classifier kernel rather than into plumbing.
It also gives you something a rewrite cannot: a reference that is bit-exact.
Your randfloat32 is not “close enough” — it either reproduces PyTorch’s
MT19937 stream draw for draw, or it does not.
How it is graded
The repo ships a grader. ./grade A0 compiles your headers, runs them against
golden vectors captured from the reference implementation, and scores each
function separately:
A0 — Data Pipeline and PRNG
PASS Code compiles cleanly (no warnings) 10 / 10
PASS rand.h manual_seed() seeded state + counters 2 / 2
PASS rand.h next_state() twist, wraparound, last word 3 / 3
FAIL rand.h randint32() golden draws + twist boundary 0 / 4
...
SCORE: 74 / 100
One row per function, in the order the functions appear in the header, because
they build on each other — when six rows fail at once, the topmost one is the
bug and the rest are downstream. Checks that need hardware you do not have
(A4 wants two GPUs) report SKIP rather than FAIL, so the score stays
honest on a laptop.
The six exercises
| Topic | GPU | The idea | |
|---|---|---|---|
| A0 | Data pipeline and PRNG | none | MT19937 bit-exact against PyTorch, BPE decoding, sharded batching |
| A1 | Forward pass | none | The transformer, in C, from the matrix multiply up |
| A2 | Backprop and AdamW | none | Every gradient derived and written by hand |
| A3 | CUDA kernels | 1 | Vectorised loads, warp reductions, chasing peak bandwidth |
| A4 | Multi-GPU | 2+ | NCCL AllReduce and ZeRO-1 optimizer sharding |
| A5 | Training infrastructure | 1 | LR schedules, grad clipping, bit-exact checkpoint resume |
A0–A2 run on any laptop. A3 and A5 want one NVIDIA GPU; A4 wants two.
Working through them yourself
git clone https://github.com/anupriyamranjit/llm.c
cd llm.c
pip install -r requirements.txt
./dev/download_starter_pack.sh # checkpoint, tokenizer, tinyshakespeare
./grade A0 # 0 / 100, to start with
Then grep -rn "TODO (A0)" and begin.
Solutions
My own solutions live in a separate repository, one branch per exercise, each a single commit on top of the stubs — so a solution reads as a diff against exactly what you start from.
They are kept out of this repo on purpose. Clone this one and the exercises are still unsolved, which is the only thing that makes it worth cloning. Go look at the answers when you want them, not by accident.
The underlying implementation, build system and tests are llm.c by Andrej Karpathy, MIT licensed. The stubbing, the exercise text and the grader are mine.