minGPT: a whole GPT in about 300 lines of PyTorch
Karpathy's clean, teaching-first GPT. It's the same kind of model as GPT-2, and small enough to learn two-digit addition on a laptop in four minutes. It's semi-archived now, and nanoGPT is its successor.
model.py 310 lines, trainer.py 109, bpe.py 319, utils.py 103 (counted with wc -l)bpe.py also needs regex and requests. A MacBook CPU is enough for the demos.Start here (no maths needed)
minGPT is a real GPT, the same design as the model behind early ChatGPT, written so plainly that you can read all of it in one evening.
Think of it like this
Picture a class where every pupil holds one word of a sentence. Before guessing the next word, each pupil may look at the pupils to their left, never the right, and decide whom to listen to most. They pass notes through a few rounds of this. Then the last pupil shouts a guess for the next word. That "who should I listen to?" step is called attention. A GPT is a stack of these rounds.
How it works
- Text is cut into small pieces called tokens, and each token becomes a number.
- Each number is looked up as a list of numbers (an embedding), plus a note of its position.
- A few Transformer blocks let every position look back at earlier positions (attention), then think on its own (a small neural network).
- The last layer gives a score for every possible next token. Pick a likely one, add it, repeat.
- Training means showing it real text and nudging its weights whenever it guesses wrong.
Why robots care
Robots are starting to use the same machinery. Some robot models turn camera pictures and commands into tokens, then write out actions as tokens, one at a time. On this page we train a tiny minGPT to read Robo-Helper's commands letter by letter and write its plan.
Try it
- Write
85 + 50 = 135as the adder project does: drop the signs and write the answer backwards,8550531. Why might backwards be easier for a model that writes left to right? (Hint: when you add on paper, which digit do you work out first?) - Type a sentence with a spelling mistake. Read it out. You still understand it, because you read letters, not only whole words. Our character-level robot brain tries to do the same.
Words to know
- Token
- One piece of text the model reads: a letter, part of a word, or a whole word.
- Transformer
- The design inside GPTs: blocks of attention plus small neural networks.
- Attention
- Each position deciding how much to listen to each earlier position.
- BPE
- Byte Pair Encoding: building tokens by repeatedly gluing together the most common pairs of pieces.
- Iteration
- One training step: one batch of examples, one nudge of the weights.
Going deeper: everything below is the full version, with code. On a first read, skim the headings, the Run it section and the Robo-Helper results, and come back for the rest later.
1. Summary
TL;DR
minGPT is a PyTorch re-implementation of GPT in three library files: model.py (the Transformer, about 300 lines as the README says; we counted 310), bpe.py (GPT-2's byte-pair tokenizer) and trainer.py (a general training loop). We ran projects/adder on a MacBook CPU. After 6,001 iterations (243 s) the 0.09M-parameter model got all 500 held-out two-digit sums right. We also trained a character-level minGPT as a Robo-Helper brain, with honest results in section 5.
- Semi-archived since Jan 2023. Karpathy kept it stable because books and courses cite it, and moved new work to nanoGPT (page 3).
- Every piece is explicit. Attention is written out by hand instead of calling
nn.MultiheadAttention, "to show that there is nothing too scary here" (from the code's docstring). - Same model at every size. One
model_typeswitch goes fromgpt-nano(3 layers, 48 wide) togpt2-xl(48 layers, 1600 wide).from_pretrainedloads OpenAI's GPT-2 weights. - Adder result on our Mac: test accuracy 0% at the start, 74% after 1,000 iterations, 96.2% after 2,000 (37 s), 100% from 4,500 onwards.
- Robo-Helper result: a character-level minGPT brain scored 7–8/12 in the bake-off (tinylm 8, distilled 10). It shrugs off typos, but 350 examples are too few for a Transformer.
- Install gotchas on 2026 tooling:
regexandrequestsare missing fromsetup.py, and a--system.work_dir=./out/xoverride crashes. Both are explained, with fixes, in section 4.
2. How the code works
model.py: four small classes
NewGELU: the activation function GPT-2 uses (a smooth ReLU).CausalSelfAttention: queries, keys and values for all heads at once, scores scaled by 1/√(head size), a triangular mask so no position can see the future, softmax, then a weighted sum.Block: LayerNorm → attention → add back, then LayerNorm → MLP (4× wider, GELU) → add back. The "add back" lines are the residual connections.GPT: token embedding + position embedding → N blocks → final LayerNorm → a linear layer that scores every vocabulary entry.forwardreturns logits and the cross-entropy loss.generatesamples one token at a time.
The heart of attention is these lines, quoted from CausalSelfAttention.forward:
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float('-inf'))
att = F.softmax(att, dim=-1)
y = att @ v
From karpathy/minGPT, mingpt/model.py (MIT). The dropout line between softmax and att @ v is left out here.
This is Lesson 15's attention, done for every position, every head and every example in the batch at once. Score = dot product ÷ √size, softmax, weighted average. The one new idea is the mask. Setting future scores to −∞ makes their softmax weight exactly 0, so position 5 can only read positions 0–5. That is what lets one forward pass train on every next-token prediction in a sequence at the same time. The README says most of the complexity is "just being clever with batching".
The block, quoted whole
def forward(self, x):
x = x + self.attn(self.ln_1(x))
x = x + self.mlpf(self.ln_2(x))
return x
From karpathy/minGPT, mingpt/model.py (MIT).
Each sub-layer adds a correction to x instead of replacing it. That's the GPT-2 "pre-norm" layout the README describes: LayerNorm moved to the input of each sub-block.
trainer.py and the projects
Trainer.run() is a plain loop: get a batch, forward, loss.backward(), clip the gradient norm to 1.0, AdamW step, call any callbacks, stop at max_iters. Nothing in it is specific to GPT. The projects/adder script defines a dataset of n-digit sums. The answer is written reversed and the loss is masked (target −1) on the question digits, so the model only learns to write answers.
3. bpe.py versus our word tokenizer
bpe.py is a lightly refactored copy of OpenAI's GPT-2 encoder. It first splits text with a regex (words with their leading space, contractions like 's, numbers, punctuation). Then it maps every byte to a printable character and repeatedly merges the adjacent pair with the best rank from OpenAI's list of 50,000 merges. The vocabulary is 50,257 tokens: 256 bytes, 50,000 merges and one end-of-text token. Our Module 18 tokenizer (Lesson 8's tokenize) is one regex: lowercase, then whole words and full stops.
We ran both on the same commands (the GPT-2 merge files are 1.4 MB, downloaded by bpe.py):
# 7 Nov 2026 · MacBook (Apple M4), CPU
'put teh red blok in the bin'
BPE ['put', 'Ġte', 'h', 'Ġred', 'Ġbl', 'ok', 'Ġin', 'Ġthe', 'Ġbin'] (Ġ = a leading space)
ours ['put', 'teh', 'red', 'blok', 'in', 'the', 'bin'] unknown to tinylm: ['teh', 'blok']
"where's the green one?"
BPE ['where', "'s", 'Ġthe', 'Ġgreen', 'Ġone', '?']
ours ["where's", 'the', 'green', 'one'] unknown to tinylm: ["where's"]
'Put the RED block!'
BPE ['Put', 'Ġthe', 'ĠRED', 'Ġblock', '!'] (ids differ from 'put', ' red')
ours ['put', 'the', 'red', 'block']
GPT-2 BPE (bpe.py) | Our word tokenizer (Lesson 8) | |
|---|---|---|
| Vocabulary | 50,257, fixed, learned from web text | Whatever words appear in our training commands |
| Unknown words | Never. Anything falls back to smaller pieces, down to single bytes | Dropped: "teh" and "blok" are simply ignored by the tiny LM |
| Capitals, punctuation | Kept: "RED" and "red" are different tokens | Lowercased, punctuation removed except "." |
| Spaces | Part of the token ("Ġred") | Thrown away |
| Good for | Any text in any language | A tiny, known command vocabulary |
For the Robo-Helper brain in section 5 we used neither. We used characters: 30 symbols (a–z, space, apostrophe, question mark, and two control symbols). That's the simplest way to never meet an unknown word, which BPE gets with a big learned vocabulary.
4. Run it, tested
# 7 Nov 2026 · MacBook (Apple M4) · Python 3.14.5 · torch 2.15.0
git clone --depth 1 https://github.com/karpathy/minGPT mingpt-src/minGPT
python3 -m venv venv
venv/bin/pip install torch
venv/bin/pip install -e mingpt-src/minGPT
venv/bin/pip install regex requests # needed by bpe.py, but not listed in setup.py
mkdir -p runs/cpu6k && cd runs/cpu6k
../../venv/bin/python ../../mingpt-src/minGPT/projects/adder/adder.py \
--trainer.max_iters=6001 --trainer.device=cpu
The adder trains gpt-nano (3 layers, 3 heads, 48 wide: 0.09M parameters) on 9,500 of the 10,000 two-digit sums and tests on the other 500. It scores the whole train and test sets every 500 iterations. Real output, trimmed:
# 7 Nov 2026 · MacBook (Apple M4), CPU · adder, 6,001 iterations
number of parameters: 0.09M
iter 0: train loss 2.31454
train final score: 17/9500 = 0.18% correct test final score: 0/500 = 0.00% correct
iter 1000: train loss 0.53160
train final score: 7323/9500 = 77.08% correct test final score: 370/500 = 74.00% correct
iter 2000: train loss 0.22252
GPT claims that 6 + 94 = 90 but gt is 100
GPT claims that 96 + 4 = 90 but gt is 100
GPT claims that 37 + 92 = 139 but gt is 129
train final score: 9181/9500 = 96.64% correct test final score: 481/500 = 96.20% correct
iter 3000: train loss 0.22115
train final score: 9489/9500 = 99.88% correct test final score: 499/500 = 99.80% correct
iter 5000: train loss 0.03020
train final score: 9500/9500 = 100.00% correct test final score: 500/500 = 100.00% correct
iter 6000: train loss 0.02295
train final score: 9500/9500 = 100.00% correct test final score: 500/500 = 100.00% correct
real 242.67 s
Look at the mistakes at iteration 2000: they are nearly all carry errors (6 + 94 = 90, missing the 1 that should carry into the hundreds). The model learns digit-by-digit addition first and carrying last, much as a child does. From iteration 4,500 to the end it scored 100% on both sets at every check.
CPU or MPS?
# 7 Nov 2026 · same adder, 2,001 iterations each
--trainer.device=cpu real 37.25 s test 481/500 = 96.20%
--trainer.device=mps real 183.67 s test 478/500 = 95.60%
On the Apple GPU (MPS) it was about 5× slower. With batches of 64 sequences of 6 tokens and a 0.09M-parameter model, the cost of sending each small job to the GPU outweighs the maths. The two runs also differ slightly in accuracy because GPU and CPU arithmetic don't give bit-identical results.
What failed, and the fixes
ModuleNotFoundError: No module named 'regex'onfrom mingpt.bpe import get_encoder.setup.pyonly liststorch(the README's todo list even says "i probably should have a requirements.txt file..."). Fix:pip install regex requests.SyntaxError: invalid syntaxwhen passing--system.work_dir=./out/adder6k.merge_from_argstriesliteral_evalon the value and only catchesValueError, but a path like./out/xraisesSyntaxError. Fix: run from a separate folder and leavework_dirat its default, or quote the value as a Python string (--system.work_dir="'out6k'").SyntaxWarning: "\p" is an invalid escape sequencefrom a docstring inbpe.pyon Python 3.14. It's harmless today, but the warning says it "will not work in the future".- MPS is never picked automatically.
device='auto'chooses CUDA or CPU only. Pass--trainer.device=mpsyourself. You'll also see a harmlesspin_memorywarning.
5. Connect to Robo-Helper (Module 18)
Lesson 15's tiny language model is minGPT's little cousin. It reads the command's words, writes a plan one word at a time ("put red blue"), and has its backprop written by hand in the standard library. Its attention stretch computes exactly minGPT's score → softmax → weighted average, for one head and one query. minGPT is the next rung up: many heads, every position at once, a causal mask, residual blocks, and PyTorch working out the gradients.
The experiment: a character-level minGPT as Robo-Helper's brain
We built a brain called minigpt. It's a minGPT with 2 layers, 4 heads and 64-wide embeddings (110,208 weights), reading characters. It trains on exactly the same 350 examples as the tiny LM (lesson15_tiny_lm.training_data(), as (command, plan words) pairs). Each example becomes one string, with the loss masked on the command part, as in the adder:
put the red and blue blocks away|put red blue⏎
At run time it writes characters greedily until ⏎, turns the words into a plan with Lesson 15's words_to_plan, and passes every action through Lesson 10's validate_action, just like TinyLMBrain. Because it reads characters, we could also add typo augmentation: half the training commands get one random typo (swapped, dropped, doubled or neighbouring-key letter). Training: 1,500 iterations, batch 64, on the Mac CPU, about 75–185 s per model. We trained 3 seeds of each version and scored them on the held-out test_data() (50 plans), typo'd copies of it (150 with one typo, 150 with two), 10 hand-written typo commands, and the Lesson 19 bake-off.
# 8–9 Nov 2026 · MacBook (Apple M4), CPU · scripts/eval_minigpt.py · "+clean" = Lesson 8's typo fixer runs first
brain held-out 1-typo 2-typo hand-10 bake-off decide p50
words (Lesson 9) 34% 34% 33% 70% 5/12 0.05 ms
tinylm (Lesson 15) 86% 86% 81% 100% 8/12 0.24 ms
attention (Lesson 15) 92% 92% 87% 100% 8/12 0.25 ms
distilled (Lesson 17) 94% 93% 90% 100% 10/12 0.25 ms
minigpt, no typo-aug, seed 0 28% 25% 27% 50% 5/12 6.6 ms
minigpt, no typo-aug, seed 1 28% 28% 26% 60% 7/12 3.5 ms
minigpt, no typo-aug, seed 2 28% 27% 23% 60% 5/12 9.7 ms
minigpt, typo-aug, seed 0 60% 61% 61% 90% 7/12 6.7 ms
minigpt, typo-aug, seed 1 58% 58% 59% 100% 8/12 8.9 ms
minigpt, typo-aug, seed 2 52% 53% 51% 80% 7/12 10.1 ms
We also asked: how much do the word-level brains lean on Lesson 8's typo fixer? We scored them on the same typo sets with the fixer switched off:
# 9 Nov 2026 · MacBook (Apple M4), CPU · scripts/raw_typos.py
held-out 1-typo 2-typo
tinylm no fixer 86% 73% 53%
attention no fixer 94% 77% 59%
distilled no fixer 94% 76% 59%
minigpt (typo-aug) no fixer, mean of 3 seeds: 57% 58% 56%
What the numbers say
- It did not beat the robot's best brains. Bake-off 7, 8 and 7 out of 12 (mean 7.3), against tinylm 8, attention 8 and distilled 10. Only the word counter (5) is clearly worse. On held-out plans it gets 52–60%, far below the tiny LM's 86%.
- Why: too little data for a Transformer. 350 examples, most from 8 templates. With no augmentation the model learns the training strings almost by heart and gets only 28% of new phrasings right. The tiny LM averages word embeddings, and that simple shape suits this tiny dataset better.
- The character idea does work, on its own terms. With typo augmentation, typos cost minigpt nothing: 57% clean, 58% with one typo, 56% with two. Without Lesson 8's fixer, the word-level models drop from 86–94% to 53–59% on two typos. So reading characters really does make a model robust to spelling mistakes. But Lesson 8's fixer already gives the word models the same robustness, for free.
- It is about 30× slower to decide: 3.5–10 ms per command against 0.25 ms. It runs a whole Transformer once per character and has no KV cache. Lesson 16's speed habits apply here.
- The typical mistakes: "put the purple block in the bin" →
put red blue(it invents colours where the tiny LM refuses), and "where's the green one?" →find redon one seed.
The honest verdict: as a Robo-Helper brain, minGPT is a step sideways, not up. It shows why real LLMs need huge datasets. The architecture is not the bottleneck here. The data is.
Your next step
- Give minigpt more data the cheap way. Add Lesson 17's teacher labels (
training_data(extra=...), as the distilled brain does). Then write 5 newPUT_TEMPLATESof your own. Retrain and check whether held-out accuracy climbs towards the tiny LM's 86%. - Then shrink it: try
n_layer=1, n_embd=32and see whether a smaller Transformer overfits less on 350 examples.
6. Why it matters for robotics
- Actions can be tokens. Our brain writes "put red blue" one character at a time. Many vision-language-action models do the same with discretised actions, using a GPT-style decoder like this one.
- The causal mask suits robot time. A controller may use the past but never the future. The same triangular mask applies when a Transformer reads a stream of joint angles or camera frames.
- Small data is the real limit. Our robot has only 350 example commands, and with that little data our minGPT did worse than Lesson 15's small network (52–60% against 86% on new phrasings). Robot foundation models need large datasets for the same reason.
- Latency matters on a robot. Writing a plan character by character took milliseconds per command here, against a fraction of a millisecond for the tiny LM. That gap grows with model size (see Lesson 16).
7. Reality check
- Semi-archived. Fine for learning, but not where new ideas land. For training use nanoGPT, and for the full path to a chatbot use its successor nanochat (both on page 3).
- Old-style attention. It builds the full T×T attention matrix by hand. Modern code calls PyTorch's fused
scaled_dot_product_attention(flash attention), which nanoGPT does. - No KV cache.
generatere-runs the whole sequence for every new token. That's fine for 20 characters and slow for long text. - Small packaging gaps (missing
regex/requests, theliteral_evalpath crash, no MPS inauto). See section 4. - Don't expect a GPU to help a tiny model. On our Mac the CPU beat MPS by about 5× for the adder.
8. Self-check
What does the causal mask do, and why is it needed for training?
It sets attention scores to the future to −∞, so their softmax weight is 0. Each position can only use earlier tokens, so one forward pass can train every next-token prediction in the sequence at once without cheating.
Why does the adder write the answer backwards?
When you add, the lowest digit is worked out first and the carry moves left. Writing the answer reversed lets a left-to-right model produce digits in the same order the carries happen.
What does the target value −1 mean in the adder's dataset?
It is ignore_index for the cross-entropy loss. Positions marked −1 add nothing to the loss, so the model isn't trained to predict the question digits, only the answer.
Why can't GPT-2's BPE ever meet an unknown word?
Its base vocabulary includes all 256 bytes. Anything it can't merge into bigger pieces falls back to single bytes.
Why was MPS slower than CPU for the adder?
The model and batches are tiny. The fixed cost of launching work on the GPU is larger than the arithmetic saved.
9. Further reading
- karpathy/minGPT: the repo, with
demo.ipynb(sorting) andgenerate.ipynb(GPT-2 sampling) - Attention Is All You Need (2017), the Transformer paper the README links
- openai/gpt-2 (the TensorFlow model definition) and openai/image-gpt, both cited in the README
- Gaussian Error Linear Units (GELU), cited in
model.py - huggingface/transformers, the full-featured reference the README compares against
- In this course: Module 06, Day 13: attention, Day 14: the annotated Transformer, Lesson 15's attention stretch
- Next: nanoGPT, the rewrite "that has teeth"