4.6 KiB
microgpt
A GPT-style language model written from scratch in PyTorch, with the pieces that matter in production LLM systems: rotary position embeddings, RMSNorm, SwiGLU, an incremental-decoding KV cache, nucleus sampling, and a test suite that proves each piece is correct.
No nn.Transformer, no Hugging Face. Attention, masking, caching and sampling are all
written out explicitly (~250 lines of model code) so every line can be reasoned about.
What's inside
| Piece | Where | Why it's here |
|---|---|---|
| Causal self-attention with explicit mask | model.py |
Mask is offset by the cache length, which is the classic place incremental decoding goes wrong |
| RoPE | model.py |
Relative position without a learned table; cached keys keep their absolute rotation |
| RMSNorm + SwiGLU, tied embeddings | model.py |
The Llama-style recipe at toy scale |
| KV cache + sliding window | model.py |
O(T) work per generated token instead of O(T^2) |
| Temperature / top-k / top-p | sampling.py |
Written from first principles, tested against analytic distributions |
| Trainer | train.py |
AdamW with decoupled weight decay (no decay on norms), warmup + cosine LR, grad clipping |
| Benchmark | scripts/benchmark.py |
Measures the cache speedup instead of asserting it |
Quickstart
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # 21 tests, ~3s on CPU
mkdir -p data && curl -sSL -o data/shakespeare.txt \
https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt
python -m microgpt.train --data data/shakespeare.txt --steps 1200 --batch-size 24
python -m microgpt.generate --prompt "ROMEO:" --tokens 200 --top-k 40
python scripts/benchmark.py
Correctness: what the tests actually prove
- No future leakage — perturbing tokens at positions >= 8 leaves logits at positions < 8 unchanged.
- KV cache == full forward — prefill then one-token-at-a-time decoding reproduces the full
forward pass to
1e-5; so does multi-token continuation (which exercises the offset causal mask). - Greedy generation is identical with and without the cache (while within the context window).
- RoPE preserves vector norms, and
<rope(q, m), rope(k, n)>depends only onm - n. - Sampling — top-k and top-p keep exactly the intended tokens; empirical frequencies match the softmax distribution; seeded runs are reproducible.
- Learning — the model can overfit a tiny batch (loss drops by more than 70%).
Results
Measured on an Intel iMac (6 CPU threads, no GPU). Numbers are from this repo's own scripts.
Training — 0.81M parameters, 4 layers, 128-dim, character-level Tiny Shakespeare, 1200 steps (~5.6 min on CPU):
| step | train loss | val loss |
|---|---|---|
| 0 | 4.242 | 4.235 |
| 400 | 1.657 | 1.830 |
| 800 | 1.444 | 1.655 |
| 1200 | 1.348 | 1.596 |
(Initial loss 4.24 ≈ ln 65, i.e. uniform over the 65-character vocabulary, as expected.)
KV-cache decoding — 2.68M-parameter model, greedy, 16-token prompt:
| new tokens | no cache (tok/s) | KV cache (tok/s) | speedup | identical output |
|---|---|---|---|---|
| 32 | 158.3 | 194.2 | 1.2x | yes |
| 64 | 166.2 | 259.3 | 1.6x | yes |
| 128 | 98.2 | 174.4 | 1.8x | yes |
| 256 | 74.5 | 198.8 | 2.7x | yes |
| 480 | 34.7 | 167.4 | 4.8x | yes |
Cached throughput stays roughly flat as the sequence grows while uncached throughput decays,
which is the O(T) vs O(T^2) difference showing up in practice. Timings on a shared CPU are
noisy, so treat the small-length rows as approximate.
Design notes and known limitations
- Sliding window past
block_size. The uncached path always sees the lastblock_sizetokens. The cached path drops its cache when full and rebuilds it from the latest half-window. Outputs are therefore identical only while the sequence fits in the window; beyond that they differ by design. (RoPE makes true cache eviction possible, but keys would need re-rotation.) - Attention is
O(T^2)memory per layer: no FlashAttention, no grouped-query attention. Both are natural next steps and would also shrink the KV cache. - Character-level, small scale. The point is correctness and clarity, not sample quality. Samples are Shakespeare-flavoured but not coherent.
Roadmap
- Grouped-query attention (smaller KV cache) with a parity test against full multi-head
- Batched generation with per-sequence cache lengths
- Speculative decoding with a draft model, verified to preserve the target distribution
- BPE tokenizer