TinyGPT — dissected, piece by piece
A fully interactive walkthrough of a minimal GPT-style language model. Every class, every method, every tensor — explained with diagrams, live demos, and the original code side-by-side. Try the interactive generator at the bottom!
Introduction Overview
What this demo actually builds
This app is a tiny decoder-only Transformer trained from scratch on a 10-sentence English corpus. It learns to predict the next token given a context window of 6 tokens. Despite its size (~few thousand parameters), it demonstrates every core idea behind modern LLMs: token embeddings, positional embeddings, causal self-attention, multi-head attention, feed-forward blocks, layer normalization, and autoregressive generation.
📦 Corpus
10 short sentences, ~42 unique words, <END> token appended.
🧱 Block size
6 tokens of context. The model only ever "sees" this window.
🧠 Embedding
32-dim vectors for tokens and positions — summed before attention.
👁️ Attention
2 heads, 2 layers, causal mask so each token only attends to past.
⚡ Training
AdamW, cross-entropy loss, 1500 steps on random batches.
✨ Generation
Autoregressive sampling with softmax + multinomial.
High-level pipeline
Corpus & Vocabulary Data Prep
Turning sentences into a numbered dictionary
1. The raw corpus
Ten short English sentences, each terminated with a special <END> token so the model knows when a sentence finishes.
2. Build the vocabulary
We split on whitespace, take the set of unique words, and assign each an integer ID. This gives us word2idx (word → int) and idx2word (int → word).
Live vocabulary — click any token to highlight its ID mapping:
Data & Batching Tensors
From words to a 1-D tensor, then to (x, y) pairs
1. Encode the whole corpus
The entire text is flattened into one long 1-D tensor of integer IDs. This is our training "tape".
2. get_batch() — sliding windows
We sample batch_size random starting positions. For each, x is 6 tokens and y is the same window shifted by 1 — the "next token" targets.
Each row = one training example. x is the context, y is what the model must predict.
TinyGPT nn.Module
The top-level model: embeddings → blocks → head
Constructor — what it builds
🔤 token_embedding
A lookup table of shape [vocab_size × 32]. Each word ID maps to a 32-dim learned vector.
📍 position_embedding
A lookup table of shape [block_size × 32]. Tells the model "you are token 0, 1, 2…" — because attention itself is permutation-invariant.
🧱 blocks
A stack of 2 Transformer blocks (attention + FFN + residuals + LayerNorm).
🎯 head
A final linear layer that projects each 32-dim hidden state back to vocab_size logits — one score per word.
Hyperparameters
block_size
6 — context window
embedding_dim
32 — hidden width
n_heads
2 — attention heads
n_layers
2 — transformer blocks
lr
1e-3 — AdamW learning rate
epochs
1500 — training steps
SelfAttentionHead Core
The fundamental "token talks to token" operation
What it holds
🔑 Key (K)
"What do I contain?" — each token advertises itself.
❓ Query (Q)
"What am I looking for?" — each token searches.
💎 Value (V)
"What info do I actually pass on if attended to?"
The forward pass — scaled dot-product attention
Interactive: the causal mask
The lower-triangular mask ensures token i can only attend to tokens ≤ i. This is what makes the model a decoder — it can't peek at the future.
Allowed — token can attend
Masked (−∞) — future, blocked
After softmax, masked cells become exactly 0. The remaining cells in each row sum to 1.
MultiHeadAttention Ensemble
Several attention heads running in parallel, then concatenated
Why multiple heads?
Each head can specialize — one might track subject-verb agreement, another might track nearby adjectives. With embedding_dim=32 and num_heads=2, each head has head_size=16. Their outputs are concatenated back to 32 dims and projected through a linear layer so the next block sees a clean 32-dim signal.
FeedForward Per-token MLP
A tiny MLP applied independently to every token
Think of attention as "gathering information from other tokens" and the FFN as "thinking about what I gathered". The 4× expansion (32 → 128 → 32) is a standard trick that gives the model extra capacity per token without increasing sequence-level cost much.
With n_embd=32:
Transformer Block Composition
Attention + FFN, each wrapped with residual + LayerNorm (Pre-LN)
Why residuals?
Each sub-layer gets a "skip connection": x + sublayer(x). This lets gradients flow directly through the network and lets the model learn small refinements on top of the identity — critical for training deep stacks.
Why Pre-LN (LayerNorm before the sublayer)?
Modern GPTs normalize inputs to each sublayer rather than outputs. It stabilizes training and is the dominant convention since GPT-2.
forward() Inference
One complete pass from token IDs to logits + loss
Step by step
1. Embed tokens
Look up each of the B×T token IDs → shape [B, T, 32].
2. Add position
Broadcast the [T, 32] positional embedding across the batch.
3. Run blocks
Sequentially through 2 transformer blocks.
4. Final LayerNorm
Stabilize the hidden states before the head.
5. Linear head
Project 32 → vocab_size. Output shape [B, T, vocab_size].
6. Cross-entropy loss
Flatten to [B*T, vocab_size] vs [B*T] targets.
generate() Autoregressive
Sampling one token at a time, feeding it back in
Starting from a seed word, the model predicts the next word, appends it, re-runs on the last 6 tokens, predicts again, and so on. Because we sample (not argmax), outputs vary between runs.
Training Loop Optimization
AdamW, cross-entropy, 1500 steps
Simulated loss curve
Typical shape: starts high (≈ vocab-level random), drops quickly, then slowly refines.
Interactive Generation Live Simulation
Enter any of the 42 vocabulary words and watch the LLM simulate autoregressive generation step-by-step, complete with probability distributions.