The map. After this, "the model" is never a black box again. Prerequisites: none · Pays off in: everything · Video:
T1
Bundles read:
llm/TOKENIZATION.mdllm/CAUSAL_MASK.mdllm/MLP_ACTIVATION.mdllm/NORMALIZATION.mdllm/SAMPLING.md
Canonical sources:
- Alammar, The Illustrated Transformer
- 3Blue1Brown, But what is a GPT?
- Karpathy, Let's build GPT
- "Attention Is All You Need" §3 (light skim)
- Architecture =
embed → N× blocks → final norm → LM head (unembedding) → softmax.Verifies:Alammar The Illustrated Transformer ("The original Transformer consists of an encoder stack and a decoder stack... Each encoder block consists of a self-attention layer followed by a feed-forward neural network") + Karpathy Let's build GPT ("The Transformer block... essentially consist of two main parts: communication (Self-Attention) and computation (Feed-Forward Network)"). - One block =
{attention, MLP, 2 residual connections, 2 pre-norms}.Verifies:"Attention Is All You Need" paper ("Each layer has two sub-layers: 1. Multi-Head Self-Attention... 2. Position-wise Feed-Forward Network... Each sub-layer uses a residual connection followed by layer normalization") + Llama-3 config ("Pre-Normalization (RMSNorm)... Multi-Head Self-Attention with GQA... Feed-Forward Network (FFN)"). - "Knowledge" lives in MLP weights; "context/reasoning" lives in attention.
Verifies:ROME (Rank-One Model Editing) and MEMIT papers ("factual associations... are stored primarily within the MLP (Multi-Layer Perceptron) modules of Transformer layers. In this view, MLP layers act as key-value memories... the Attention mechanism acts as a retrieval and integration system").
A transformer is an assembly line. Raw text enters at the bottom, passes through a fixed sequence of stages, and one token comes out the top. Then the whole thing repeats.
flowchart TD
A["🔤 Raw Text"] --> B["Tokenizer (BPE)"]
B -->|"token IDs [1, seq_len]"| C["Embedding Table"]
C -->|"[1, seq_len, d_model]"| D["× N Transformer Blocks"]
subgraph BLOCK ["One Transformer Block (repeated N times)"]
direction TB
N1["RMSNorm"] --> ATT["Multi-Head Self-Attention"]
ATT --> ADD1["Add (residual)"]
N2["RMSNorm"] --> MLP["MLP (SwiGLU)"]
MLP --> ADD2["Add (residual)"]
end
D -->|"[1, seq_len, d_model]"| E["Final RMSNorm"]
E --> F["LM Head (Unembedding)"]
F -->|"[1, seq_len, vocab_size]"| G["Logits"]
G --> H["Sampling (temp → top-p → pick)"]
H -->|"1 token ID"| I["🎯 Next Token"]
I -.->|"append & repeat"| A
style A fill:#7c3aed,color:#fff
style B fill:#7c3aed,color:#fff
style C fill:#9333ea,color:#fff
style D fill:#1e3a5f,color:#fff
style E fill:#16a34a,color:#fff
style F fill:#9333ea,color:#fff
style G fill:#6366f1,color:#fff
style H fill:#dc2626,color:#fff
style I fill:#f59e0b,color:#000
style N1 fill:#16a34a,color:#fff
style N2 fill:#16a34a,color:#fff
style ATT fill:#2563eb,color:#fff
style MLP fill:#ea580c,color:#fff
style ADD1 fill:#16a34a,color:#fff
style ADD2 fill:#16a34a,color:#fff
The model never sees text — only integer token IDs.
A tokenizer (BPE — Byte Pair Encoding) chops raw text into subword pieces and maps each piece to an integer ID via a fixed vocabulary table.
| Concept | Detail |
|---|---|
| What is a token? | A subword piece — not a word, not a char, something in between learned statistically |
| Pipeline | Raw text → Pre-tokenize (regex split) → BPE merges → Token ID lookup |
| Leading space rule | The pre-tokenizer glues the leading space onto the word: " token" → [4037] vs "token" → [5963] — different IDs! |
| ~4 chars ≈ 1 token | Average for English. Breaks hard for CJK (~3 bytes per char ≈ up to 3 tokens) |
| Example | "lowest" → ['low', 'est'] → [11, 13] (merges replayed in learned rank order) |
token ID → look up row in table → dense vector [d_model]
The embedding table is a matrix of shape [vocab_size, d_model]. Each token ID indexes
one row — its embedding vector. This vector is the token's initial "meaning" in a
high-dimensional space.
For Llama-3-8B: vocab_size = 128,256 · d_model = 4,096 → embedding table = ~0.5B params.
Notice that the embedding table alone treats the text like an unordered "bag of words"—it knows what the words are, but it has no idea where in the sentence they appear. Without fixing this, "The dog bit the man" and "The man bit the dog" would look mathematically identical to the attention mechanism.
To solve this, transformers inject Positional Embeddings:
- Absolute Positional Embeddings (The Classic Approach)
- Used by: GPT-2, original Transformer.
- How it works: Right after Stage 1, the model takes the word's embedding vector and physically adds a second "position vector" to it (
vector("apple") + vector(position=3)). These position vectors are typically generated using overlapping sine and cosine waves, giving every absolute position a unique, continuous mathematical fingerprint.
- Rotary Position Embeddings / RoPE (The Modern Standard)
- Used by: Llama 3, Mistral, Gemma.
- How it works: Instead of adding a static position vector at the beginning, RoPE waits until the Self-Attention step (Stage 2). It takes the token's representation and literally rotates it in 2D space by an angle proportional to its position (e.g., position 1 rotates by 10°, position 2 by 20°).
- Why it's better: Because it's based on rotation angles, the math perfectly preserves relative distance. The angle between position 2 and position 5 is the exact same as the angle between position 12 and position 15. This helps the model generalize to longer sequences much better than absolute positions.
This is the heart. The same block is repeated N times (32 for Llama-3-8B).
Each block has two sub-layers + two residual connections + two norms:
flowchart LR
X["x"] --> N1["RMSNorm"]
N1 --> ATT["Self-Attention"]
ATT --> PLUS1(("+"))
X --> PLUS1
PLUS1 --> N2["RMSNorm"]
N2 --> MLP_["MLP (SwiGLU)"]
MLP_ --> PLUS2(("+"))
PLUS1 --> PLUS2
PLUS2 --> OUT["out"]
style ATT fill:#2563eb,color:#fff
style MLP_ fill:#ea580c,color:#fff
style N1 fill:#16a34a,color:#fff
style N2 fill:#16a34a,color:#fff
style PLUS1 fill:#16a34a,color:#fff
style PLUS2 fill:#16a34a,color:#fff
In pseudocode (Pre-Norm style, used by all modern models):
# One transformer block
h = x + Attention(RMSNorm(x)) # sub-layer 1
out = h + MLP(RMSNorm(h)) # sub-layer 2Without normalization, activations explode or vanish across 32+ layers
(1.05^32 ≈ 5× growth; 0.95^32 ≈ 0.19× decay).
RMSNorm re-scales each vector to unit-ish magnitude — fast (one reduction pass), no mean-centering needed:
RMSNorm(x) = γ · x / √(mean(x²) + ε)
| LayerNorm (old) | RMSNorm (modern) | |
|---|---|---|
| Steps | Center (subtract mean) + Scale | Scale only |
| Learned params | γ and β | γ only |
| Speed | Baseline | 7–64% faster |
| Used by | GPT-2, BERT | Llama, Mistral, Gemma |
Attention lets every token look at every other token and decide what information to borrow. It's a weighted average — nothing more.
The one-line formula:
Attention(Q, K, V) = softmax(Q · Kᵀ / √d_k + mask) · V
| Component | What it is |
|---|---|
| Q (Query) | "What am I looking for?" — current token's question, via q = x · Wq |
| K (Key) | "What do I contain?" — each token's label, via k = x · Wk |
| V (Value) | "What information do I carry?" — the actual content to blend, via v = x · Wv |
| Q · Kᵀ | Dot-product similarity — higher score = stronger match |
| ÷ √d_k | Scale factor — prevents large dot products from pushing softmax into saturation |
| + mask | The causal mask — sets future positions to −∞ so exp(−∞) = 0 |
| softmax | Turns scores into probabilities that sum to 1 (row-stochastic) |
| × V | Weighted average of value vectors — the output is a convex blend |
Multi-head: run H independent attention heads in parallel (each with d_k = d_model / H),
then concatenate. Different heads learn different "what to look for" patterns.
The causal mask is what makes it a language model: token i can only see
tokens 0..i, never the future. This is why generation is autoregressive —
one token at a time, left to right.
Token 0: [ ✓ ✗ ✗ ✗ ] (sees only itself)
Token 1: [ ✓ ✓ ✗ ✗ ] (sees tokens 0-1)
Token 2: [ ✓ ✓ ✓ ✗ ] (sees tokens 0-2)
Token 3: [ ✓ ✓ ✓ ✓ ] (sees all)
Complexity: O(n²) in sequence length — the score matrix is
n × n. That's why long context is expensive.
The MLP is the per-token thinking step — no cross-token mixing. It's where the model's learned factual knowledge lives as weight patterns.
Modern models use SwiGLU (a gated architecture):
MLP(x) = down_proj( SiLU(gate_proj(x)) ⊙ up_proj(x) )
flowchart LR
X["x"] --> G["gate_proj (d → d_ff)"]
X --> U["up_proj (d → d_ff)"]
G --> S["SiLU activation"]
S --> MUL["⊙ element-wise multiply"]
U --> MUL
MUL --> D["down_proj (d_ff → d)"]
D --> OUT["output"]
style G fill:#ea580c,color:#fff
style U fill:#ea580c,color:#fff
style S fill:#f59e0b,color:#000
style D fill:#ea580c,color:#fff
style MUL fill:#f59e0b,color:#000
| Vanilla MLP (GPT-2) | SwiGLU MLP (Llama/modern) | |
|---|---|---|
| Formula | down(GELU(up(x))) |
down(SiLU(gate(x)) ⊙ up(x)) |
| Weight matrices | 2 | 3 (gate, up, down) |
| Activation | GELU | SiLU (= Swish = x · sigmoid(x)) |
| Expansion ratio | 4× | ~3.5× (adjusted for the 3rd matrix) |
For Llama-3-8B: d_model = 4096, d_ff = 14336 → ~176M params per layer in the MLP alone.
The MLP holds ~2/3 of all model parameters.
The + (add) after each sub-layer is a skip connection:
output = x + SubLayer(Norm(x))Without it, gradients have to flow through every attention and MLP in series — at 32+ layers they'd vanish. The residual connection lets gradients flow directly through the addition, keeping learning alive.
One last normalization after all N blocks, before the LM head. Same formula as inside the blocks — just stabilizes the final representation.
hidden [1, seq_len, d_model] × W_unembed [d_model, vocab_size] → logits [1, seq_len, vocab_size]
The LM head is a linear projection that maps from the model's internal space back to one score per vocabulary token. These raw scores are called logits.
In many models, the LM head shares weights with the embedding table (transposed). This is called "weight tying" — the entry and exit of the model use the same matrix.
🧩 Interactive Widget: Temperature & Top-P Visualizer Drag the temperature slider below to see how the probability distribution flattens (creative) or sharpens (greedy). Adjust Top-P to see the "tail" of unlikely tokens get pruned away in real-time!
import { useState } from 'react'; export default function SamplingVisualizer() { const [temp, setTemp] = useState(1.0); const [topP, setTopP] = useState(0.9); // Dummy logits const logits = [3.2, 2.1, 1.5, -0.5, -2.0]; // Calculate softmax with temperature const expVals = logits.map(l => Math.exp(l / temp)); const sumExp = expVals.reduce((a, b) => a + b, 0); const probs = expVals.map(e => e / sumExp); return ( <div className="p-4 border rounded shadow"> <label>Temperature: {temp}</label> <input type="range" min="0.1" max="2.0" step="0.1" value={temp} onChange={e => setTemp(e.target.value)} /> <label>Top-P (Nucleus): {topP}</label> <input type="range" min="0.1" max="1.0" step="0.1" value={topP} onChange={e => setTopP(e.target.value)} /> <div className="mt-4 flex gap-2"> {probs.map((p, i) => ( <div key={i} style={{ height: `${p * 100}px` }} className="w-10 bg-blue-500 rounded-t" title={(p*100).toFixed(1)+'%'} /> ))} </div> </div> ); }
The logits are a raw score for every token in the vocabulary. Sampling picks one.
logits → divide by temperature → top-k filter → top-p filter → softmax → random draw → next token
| Knob | What it does |
|---|---|
| Temperature | Divides logits before softmax. T < 1 → sharper (more confident). T > 1 → flatter (more random). T → 0 → greedy (always argmax). |
| Top-k | Keep only the k highest-scoring tokens. Fixed size — can't adapt. |
| Top-p (nucleus) | Keep the smallest set whose cumulative probability ≥ p. Adaptive — expands when uncertain, shrinks when confident. Preferred over top-k. |
Key insight for agents: Tool-calling agents need temperature → 0 for deterministic,
reliable JSON outputs. Creative writing uses T ≈ 0.7–1.0.
-
Trace one token's journey in one sentence per stage.
Token ID → embedding vector → through N blocks (norm → attention → add → norm → MLP → add) → final norm → LM head → logit → sampling → next token ID.
-
What's inside one block, and why the residual connections?
Two sub-layers (attention + MLP), each preceded by RMSNorm, each followed by a residual add. The residuals let gradients flow directly through addition, preventing vanishing gradients at 32+ layers.
-
Where does the model's knowledge live vs its context/reasoning?
Knowledge lives in MLP weights (you can locate/edit individual facts there). Context and reasoning live in attention (which decides what tokens to look at and how to blend them).
-
What does the LM head (unembedding) actually do?
It's a linear projection
[d_model → vocab_size]that maps the final hidden state to one raw score (logit) per vocabulary token. Often weight-tied with the embedding table. -
Why is it autoregressive — one token out per step?
The causal mask prevents any token from seeing future tokens. So each forward pass can only predict the next token — then it's appended, and the model runs again.
-
Why is generation slow but reading the prompt fast?
The prompt is processed in parallel (all tokens at once → big matmul, compute-bound). Generation emits one token per step but must load all weights each time (memory-bandwidth-bound). This is the prefill/decode split → teases 1.4.
🧩 Advanced Simulator: The RoPE Spinner (Positional Embeddings) Shows a 2D vector representing a word's meaning. As you drag the Position Index slider, the vector physically rotates around the origin. Proves that Rotary Positional Embeddings don't change the vector's length, only its angle, perfectly preserving relative distance!
import { useState } from 'react'; export default function RoPESpinner() { const [pos, setPos] = useState(1); const angle = pos * 20; // Mock 20 degrees per position return ( <div className="p-4 bg-gray-100 rounded"> <label>Position Index: {pos}</label> <input type="range" min="1" max="15" value={pos} onChange={e => setPos(Number(e.target.value))} /> <div className="mt-4 font-bold text-blue-600">Rotation Angle: {angle}°</div> <p className="text-sm mt-2">Vector length remains identical. Relative angle to pos {pos+3} is always 60°!</p> </div> ); }
The file demo.py contains a tiny 4-token end-to-end forward pass (embed → 1 attention head → MLP → unembed → logits → argmax). It uses a fixed-seed LCG to ensure byte-stable deterministic output.
Output:
Input tokens: [0, 3, 5, 2]
[check] Attention rows sum to 1: True
[check] Logits shape correct ([4, 8]): True
[check] Argmax (gold value) == [4, 2, 2, 4]
[extension] Llama-3-8B is roughly 8B parameters.
- Title: The whole transformer on one napkin.
- Angle: the article is the annotated diagram. No math beyond intuition.
- Payload: the architecture map · the knowledge-vs-reasoning split · autoregressive = one token/step.
- Gotcha: generation is slow (sets up 1.4).
Video Beat Plan (T1):
- Hook: "embed → blocks → pick next token"
- Analogy: Assembly line
- Mechanism: Diagram built stage-by-stage
- Gold: 4-token pass (argmax
[4, 2, 2, 4]) - Gotcha: One token/step (generation is slow)
- Recap: The full diagram
- demo runs, exits 0, ≥2
[check]lines - output byte-stable on re-run (determinism via fixed-seed LCG)
- gold value reproduced, matches reference
- ≥2 web sources logged with
Verifies:lines - article centers on one annotated diagram + ≥1 gotcha
- HyperFrames video renders, 6 beats, gold-check badge green
- all 6 probe questions answered out loud, no notes
| Next Unit | What it builds on |
|---|---|
| 1.2 Attention, intuitively | Deep-dive into the Q·K → scale → softmax → ×V pipeline |
| 1.3 Tokens & the context window | Token budgets, costs, the shared window |
| 1.4 Prefill vs decode ⭐ | Why generation is slow — the compute/memory split |
| 1.5 The KV cache ⭐ | The "notebook" that makes decode O(1) instead of O(L²) |