Exercises — overview
A set of self-study exercises that rebuild a complete GPT-2 training system from the ground up, starting with tokenisation and ending with multi-GPU training. Each one is a layer of the stack; later ones depend on earlier ones.
The codebase is llm.c by Andrej Karpathy.
Function bodies have been replaced with // TODO (A?) stubs, each followed by a
// HINT: line. The job is to fill them in, and ./grade tells you whether you
got it right.
The exercises
| Exercise | Topic | GPU? | Files |
|---|---|---|---|
| A0 | Data Pipeline and PRNG | no | llmc/rand.h, llmc/tokenizer.h, llmc/dataloader.h, llmc/sampler.h |
| A1 | GPT-2 Forward Pass (CPU) | no | train_gpt2.c |
| A2 | Backpropagation and AdamW (CPU) | no | train_gpt2.c |
| A3 | CUDA Kernels | 1 GPU | llmc/encoder.cuh, llmc/layernorm.cuh, llmc/attention.cuh, llmc/matmul.cuh, llmc/gelu.cuh, llmc/fused_classifier.cuh, llmc/adamw.cuh, train_gpt2.cu |
| A4 | Multi-GPU with NCCL/ZeRO-1 | 2+ GPUs | llmc/zero.cuh, train_gpt2.cu |
| A5 | Training Infrastructure | 1 GPU | llmc/schedulers.h, llmc/logger.h, llmc/outlier_detector.h, llmc/global_norm.cuh, train_gpt2.cu |
Each file appears in exactly one exercise, and every TODO tag in a file matches it. train_gpt2.cu is the one exception — it is touched by A3, A4, and A5, but each of its stubs carries its own tag, so grep "TODO (A4)" train_gpt2.cu gives you exactly that part.
Learning Objectives
By the end you will be able to:
- Explain and implement the GPT-2 transformer architecture from first principles
- Derive and implement backpropagation for each component of the network
- Write CUDA kernels that approach hardware-peak memory bandwidth using vectorized loads and warp-level reductions
- Scale training across multiple GPUs using NCCL AllReduce and ZeRO-1 optimizer sharding
- Build production training infrastructure: LR schedules, gradient clipping, structured logging, and bit-exact checkpoint resume
Prerequisites
- Fluency in C (pointers, arrays, structs, malloc/free)
- Linear algebra: matrix multiply, dot products
- Calculus: partial derivatives, chain rule
- A0, A1, and A2 need no GPU. A3 and A5 need one NVIDIA GPU (RTX 3080 or better recommended); A4 needs at least two, plus NCCL. Without the hardware,
./gradereports those checks asSKIPrather than failing them.
Environment Setup
# from the repo root
# Python deps, for data prep and generating the PyTorch reference
pip install -r requirements.txt
# Fetch the GPT-2 checkpoint, tokenizer, debug state, and tinyshakespeare in one go
chmod u+x ./dev/download_starter_pack.sh && ./dev/download_starter_pack.sh
# ...or regenerate everything yourself:
# python dev/data/tinyshakespeare.py
# python train_gpt2.py
# CPU build (A0-A2)
make train_gpt2
make test_gpt2
./test_gpt2
# CPU-side unit tests (A0 dataloader, A5 outlier detector)
cd dev/test && make && ./test_dataloader && ./test_outlier_detector
# CUDA build (A3-A5), requires nvcc
make train_gpt2cu
make test_gpt2cu
./test_gpt2cu
All builds must be warning-free. If make prints ✗ OpenMPI is not found, MULTI_GPU is undefined and A4’s code is not compiled at all.
Command-line flags
train_gpt2cu uses single-character flags, not GNU long options — the parser reads argv[i][1]. --input-bin, --num-steps, --zero-stage and friends do not exist and will be ignored. Run the binary with no arguments to print the full list. The ones you will use most:
| Flag | Meaning |
|---|---|
-i / -j |
train / val data shard glob |
-b / -t |
micro-batch size B / sequence length T |
-d |
total batch size in tokens (drives gradient accumulation) |
-x |
total steps |
-v |
validate every N steps |
-o |
output log / checkpoint directory |
-n / -y |
checkpoint every N steps / resume from newest checkpoint |
-l / -u / -q / -k |
peak LR / warmup / final LR fraction / scheduler type |
-z |
ZeRO stage |
Checking your work
One command checks your work against the scoring table in each exercise:
./grade A0
Run it with no arguments (./grade, or make grade) to score everything that
can be checked on your machine.
A0 — Data Pipeline and PRNG
PASS Code compiles cleanly (no warnings) 10 / 10
PASS randint32: golden vectors (seeds 137, 42) + twist boundary 10 / 10
PASS randfloat32 golden vector (24-bit mantissa) 5 / 5
FAIL sample_softmax + xorshift* PRNG 0 / 10
SKIP Tokenizer init, versions, decode -- / 15 gpt2_tokenizer.bin missing
...
subtotal 60 / 85 (15 points not checkable here)
Three things to know about it:
SKIPis not zero. A check that cannot run here — no GPU, no NCCL, a missing.binfile — is excluded from the denominator, not failed. The summary lists everything that was skipped and why.- It cannot check everything. Performance targets (A3 MFU) and the
multi-node init paths depend on hardware and setup the script cannot inspect;
those stay
SKIPand are yours to verify withncuand the scripts inscripts/multi_node/. - It builds the test harnesses at
-O0with ASan/UBSan. At-O3, an unimplemented function leaves buffers uninitialised, the reads become undefined behaviour, and the compiler is entitled to delete the very checks being graded. That shows up as a false pass, so the grader does not use it.
The lines it prints correspond one-to-one with the Scoring table in each
exercise. Where the harness genuinely cannot separate two functions, the line
says so — see A2’s (inferred) rows.
Assignment Tag Reference
Each stubbed function is tagged with its assignment, followed by a hint:
void encoder_forward(float* out, int* inp, float* wte, float* wpe, int B, int T, int C) {
// TODO (A1) - Add token and position embeddings into the output tensor
// HINT: out[b,t,c] = wte[inp[b*T+t]*C+c] + wpe[t*C+c]
}
To find all functions for a given assignment:
grep -rn "TODO (A3)" llmc/ train_gpt2.cu
Where this handout and the in-file HINT disagree, the code is authoritative. Please report any discrepancy — the handouts are checked against the source, but errors survive.
Testing Philosophy
Each assignment is verified numerically against a reference:
| Exercise | Harness | Reference |
|---|---|---|
| A0 | dev/test/test_dataloader, MT19937 golden vectors |
PyTorch’s MT19937 |
| A1, A2 | ./test_gpt2 |
gpt2_124M_debug_state.bin, exported from PyTorch |
| A3 | ./test_gpt2cu |
the same debug state |
| A4 | loss equality across 1/2/4 GPUs | your own single-GPU run |
| A5 | dev/test/test_outlier_detector, checkpoint resume |
analytic values / your own run |
test_gpt2 and test_gpt2cu compare your analytic gradients against PyTorch’s analytic gradients on a fixed batch. They are not finite-difference checks — there is no (loss(θ+ε) - loss(θ-ε)) / 2ε in the harness.
Tolerances enforced by test_gpt2.c:
| Check | Tolerance |
|---|---|
Logits, element-wise (first V columns only) |
1e-2 absolute |
| Loss, at step 0 and each of 10 steps | 1e-2 absolute |
Every gradient tensor, element-wise (check_tensor) |
2e-2 absolute |
The pass condition is the final overall okay: 1. Note the harness checks the model end to end, not layer by layer — there are no [layernorm_forward] OK lines. If you are within tolerance your implementation is correct regardless of the algorithm used (different loop ordering is fine).
Generating the reference yourself requires python train_gpt2.py, which writes gpt2_124M.bin, gpt2_124M_bf16.bin, gpt2_124M_debug_state.bin, and gpt2_tokenizer.bin.
A note on the reference implementation
The original llm.c is public, and reading it while working through these is
the fastest way to learn nothing. Three sets of files were removed from this
repo for exactly that reason — each contained working implementations of
functions the exercises ask you to write:
| Removed | Contained |
|---|---|
train_gpt2_fp32.cu, test_gpt2_fp32.cu |
reference versions of most A3 kernels, same names |
dev/cuda/ (22 files) |
standalone versions of the A3 kernels, plus global_norm.cu (A5) and nccl_all_reduce.cu (A4) |
doc/layernorm/, dev/cpu/matmul_forward.c |
layernorm_forward, layernorm_backward, matmul_forward |
They are all still in upstream llm.c whenever you want them — the point of removing them here is only that you have to reach for them deliberately rather than trip over them.