Files
microgpt/README.md
T

94 lines
4.6 KiB
Markdown

# 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
```bash
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 on `m - 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 last `block_size`
tokens. 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