Module 01 Transformer Architecture
Module 01 Transformer Architecture
AI Engineering Handbook
Who is this for? A Computer Science student who knows how to program but has never
studied AI infrastructure or deep learning internals. Every technical term is explained
when it first appears. Nothing is skipped.
Table of Contents
1. Chapter 1 — What is a Transformer?
2. Chapter 2 — Embeddings
3. Chapter 3 — Positional Encoding
4. Chapter 4 — Self Attention
5. Chapter 5 — Multi Head Attention
6. Chapter 6 — Feed Forward Network
7. Chapter 7 — LayerNorm
8. Chapter 8 — Residual Connections
9. Chapter 9 — Decoder Only Architecture
10. Chapter 10 — Output Layer
11. Mini Project — Build a Tiny Transformer in Python
12. Module Summary
13. Key Takeaways
Long-range Forgot earlier words in long texts Attention directly connects any two
dependencies words
Training speed Sequential — word by word, slow Parallel — all words at once, fast
Real-World Analogy
Imagine you are reading a 300-page novel and someone asks: "In Chapter 20, when John
mentioned the key, what lock was he referring to?"
With an RNN, you would have to read the entire book word by word in order. By the time
you reach page 300, you have mostly forgotten what was on page 1.
With a Transformer, it is as if you laid out every page of the book on a giant table. You can
look at page 20 and page 1 at the same time, directly compare them, and immediately see
the connection — without having to "remember" anything from earlier.
Internal Architecture
The original Transformer had two main parts:
1. Encoder — reads the input and builds an understanding of it
2. Decoder — generates the output word by word
However, modern LLMs like GPT, Claude, and LLaMA use only the Decoder part
(explained in Chapter 9). For now, let us understand the full architecture.
INPUT SIDE
Tokenizer
Embedding Layer
Positional Encoding
Multi-Head Self-Attention
Cross-Attention with
Encoder Output
OUTPUT SIDE
Linear Layer
Softmax
"The cat sat on the mat" → [The, cat, sat, on, the, mat]
→ Token IDs: [464, 3797, 3332, 319, 262, 2603]
Step 2 — Embedding Each token ID is converted to a vector (a list of numbers) of fixed size
(e.g., 512 or 4096 numbers). This vector represents the meaning of the word in a
mathematical space.
Step 3 — Positional Encoding Since the Transformer reads all words at once, it needs to
know the order of words. Positional encoding adds position information to each word's
vector.
Step 4 — Self Attention The model looks at every word and asks: "How much should I pay
attention to every other word when trying to understand this word?"
Step 5 — Feed Forward Network Each word's representation is passed through a small
neural network to add more expressive power.
Steps 4 and 5 repeat for N layers (e.g., GPT-3 has 96 layers).
Step 6 — Output The final layer produces a probability distribution over all possible next
tokens. The highest probability token is selected as the output.
Data Flow
Text Tokens Embeddings + Position Info After Attention After FFN Output Logits Probabilities Predicted Token
'Hello world' [15496, 995] [0.2, -0.5, 0.8, ...] [0.3, -0.4, 1.1, ...] [context-aware vectors] [enriched vectors] [0.01, 0.3, 0.001, ...] [softmax applied] '!'
[0.1, 0.9, -0.3, ...] [0.4, 1.1, -0.1, ...]
Memory Flow
When processing a sentence of length L with embedding dimension d :
Stage Memory Size Notes
The L × L attention matrix is why long contexts are expensive. If L = 100,000 tokens (100K
context), the attention matrix is 100,000 × 100,000 = 10 billion numbers. This is one of the
biggest engineering challenges in modern LLMs.
Tokenization ✅ Yes ❌ No
Production Examples
GPT-4: Transformer with ~1.8 trillion parameters (estimated), 128K context length
Claude 3.5 Sonnet: Decoder-only Transformer with 200K context window
LLaMA 3.1: 128K context, open-source, runs on consumer hardware
BERT: Encoder-only Transformer, used for classification tasks, not generation
Common Misconceptions
Misconception Reality
"Attention means the model pays attention Attention is just a weighted sum — a mathematical
like a human" operation
"Encoder and Decoder are always both Modern LLMs use Decoder only
present"
FAQ
Q: What is a "parameter" in a Transformer? A: A parameter is a number (specifically a
floating-point weight) that the model learned during training. GPT-3 has 175 billion such
numbers. These numbers are what make the model "smart."
Q: What is a "token"? A: A token is a chunk of text. The word "unhappiness" might be split
into ["un", "happiness"] — 2 tokens. The average English word is about 1.3 tokens.
Q: Why not just use a big lookup table instead of a Transformer? A: Language has infinite
combinations. You can't pre-store all possible sentences. A Transformer generalizes — it
learns patterns and applies them to new combinations.
Q: How long does it take to train a Transformer? A: GPT-3 took about 34 days on 1,024
A100 GPUs. LLaMA 3 took millions of GPU-hours.
Interview Questions
1. What problem did Transformers solve that RNNs couldn't? (Parallelization, long-
range dependencies)
2. What are the two main components of the original Transformer? (Encoder and
Decoder)
3. What is the time and memory complexity of self-attention? (O(n²) in sequence
length n)
4. Why do modern LLMs use only the Decoder? (For text generation, a decoder auto-
regressively generates tokens)
5. What paper introduced the Transformer? ("Attention is All You Need," Vaswani et al.,
2017)
Hands-on Exercises
1. Exercise 1: Open [Link] — an interactive 3D visualization of a small
GPT-2 model. Click through each layer and watch how data flows.
2. Exercise 2: Install the transformers library and load a model:
python
tokenizer = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2")
python
Summary
A Transformer is a neural network that processes all input tokens simultaneously using the
attention mechanism. It replaced older sequential models (RNNs/LSTMs) because it is
faster to train, scales better, and handles long texts better. Modern LLMs are decoder-only
Transformers that generate one token at a time.
Chapter 2 — Embeddings
What is it?
An embedding is a way to convert a discrete token (like the word "cat" or the number 4237)
into a continuous vector — a list of floating-point numbers.
In simpler terms: a Transformer only works with numbers. The word "cat" isn't a number.
Embeddings are the bridge between human language and math.
For example, with an embedding dimension of 4 (real models use 512 to 16384):
"cat" → [0.25, -0.13, 0.87, 0.44]
"dog" → [0.27, -0.11, 0.85, 0.41]
"car" → [-0.72, 0.65, -0.31, 0.20]
Notice that "cat" and "dog" have similar vectors (because they are semantically related),
while "car" is very different.
A vocabulary (vocab) is the complete set of all tokens the model knows about. GPT-2 has
50,257 tokens. GPT-4 uses ~100,000+ tokens. The embedding layer is a large lookup table
(a matrix) of size vocab_size × embedding_dimension .
Real-World Analogy
Think of a color picker in a design application. Colors can be described as RGB values: Red =
(255, 0, 0), Green = (0, 255, 0), Blue = (0, 0, 255). Two similar colors like Red (255, 0, 0) and
Dark Red (200, 0, 0) are numerically close to each other.
Embeddings do the same thing for words. "Cat" and "dog" are numerically close (in a 512-
dimensional space). "Cat" and "democracy" are far apart.
Internal Architecture
The embedding layer is simply a matrix — a 2D array of numbers.
Embedding Matrix E: shape = [vocab_size × d_model]
For GPT-2:
vocab_size = 50,257
d_model = 768
E shape = [50,257 × 768]
Memory = 50,257 × 768 × 4 bytes ≈ 154 MB
0: <|endoftext|>
Embedding Matrix
50,257 × 768
...
Explanation: Given the token "The" (ID 464), we simply retrieve row 464 from the
embedding matrix. That row's 768 numbers become the vector representation of "The" that
flows through the rest of the Transformer.
python
python
Step 4 — Feed into next stage (Positional Encoding) The shape [sequence_length,
d_model] is the fundamental shape that flows through most of the Transformer.
Data Flow
Tokenizer
(splits into subword tokens)
Embedding Layer
(lookup table: 50257 × 768)
Token Embeddings
Shape: [3 × 768]
Memory Flow
For a sequence of length L with model dimension d :
The embedding lookup is essentially a gather operation — for each token ID, grab the
corresponding row from the matrix. GPUs are excellent at this because they have high
memory bandwidth (the ability to read large amounts of memory quickly).
Production Examples
GPT-2: d_model = 768, vocab = 50,257
GPT-3: d_model = 12,288, vocab = 50,257
LLaMA 3: d_model = 4,096 (8B model), vocab = 128,256
Claude models: Exact dimensions not public, but similar scale
Weight Tying: In many models, the embedding matrix is shared with the output layer (the
matrix that converts back to token probabilities). This saves memory because you don't
need two large matrices. GPT-2 does this.
Common Misconceptions
Misconception Reality
"Embeddings are permanent — Embeddings are learned during training and change with
they never change" every gradient update
"A larger vocabulary is always Larger vocab = larger embedding matrix = more memory.
better" Trade-offs exist.
"Embeddings capture the true They capture statistical patterns, not true meaning. Context
meaning of words" matters.
"Each token has one fixed The input embedding is fixed per token, but the contextual
embedding" representation changes at every layer
FAQ
Q: What is the difference between "embedding" and "token"? A: A token is a piece of text
(like "cat" or "##ing"). An embedding is the vector of numbers that represents that token.
Q: How are embeddings learned? A: During training, the model starts with random
embeddings and adjusts them using backpropagation — nudging the vectors of similar
words closer together and dissimilar words further apart.
Q: Can I visualize embeddings? A: Yes! Use dimensionality reduction techniques like t-
SNE or UMAP to project 768-dimensional vectors down to 2D. You'll see clusters of related
words.
Q: What is "embedding dimension" (d_model)? A: The number of numbers in each
embedding vector. Larger d_model = more expressive but more memory and computation.
Interview Questions
1. What is an embedding and why is it needed? (Converts discrete tokens to continuous
vectors that neural networks can process)
2. What is the shape of the embedding matrix for GPT-2? (50,257 × 768)
3. What does "weight tying" mean in the context of embeddings? (Sharing the
embedding matrix between input and output layers)
4. Why are embeddings better than one-hot encoding? (Dense, low-dimensional,
semantically meaningful vs sparse, high-dimensional, no semantic information)
5. What is the time complexity of an embedding lookup? (O(1) — just an array index)
Hands-on Exercises
Exercise 1: Inspect GPT-2 embeddings
python
import torch
from transformers import GPT2Model
model = GPT2Model.from_pretrained("gpt2")
E = [Link] # Word Token Embeddings
print(f"Embedding matrix shape: {[Link]}") # [50257, 768]
python
import [Link] as F
cat_vec = E[ids[0][0]]
dog_vec = E[ids[1][0]]
car_vec = E[ids[2][0]]
cos = F.cosine_similarity
Summary
Embeddings are learned lookup tables that convert token IDs into dense floating-point
vectors. The embedding matrix has shape [vocab_size × d_model] and lives on the GPU.
Similar words end up with similar embeddings because they appear in similar contexts
during training. The output of the embedding layer is a matrix of shape [sequence_length ×
d_model] that flows through the rest of the Transformer.
Real-World Analogy
Imagine you write the following on separate sticky notes:
and mix them up in a bowl. You can't tell what the original sentence was, because the order
is gone.
Positional encoding is like writing the note's position on each sticky note in invisible ink:
Now even when mixed up, you can reconstruct the original order.
Internal Architecture
There are two main types of positional encoding used in practice:
Type 1 — Sinusoidal Positional Encoding (Original paper, still used in some models)
For each position pos and dimension i in the embedding:
Sinusoidal Encoding
Learned Encoding
Add to Embeddings
Step 4: The result (same shape as embeddings: [L × d_model] ) flows into the first
attention layer.
Data Flow
Token Embeddings
[L × d_model]
Shape: [3 × 768]
Final Input Embeddings
Element-wise Addition [L × d_model]
⊕
Shape: [3 × 768]
(carries both meaning AND
position)
Positional Encodings
[L × d_model]
Shape: [3 × 768]
(precomputed or learned)
Memory Flow
Item Size
Production Examples
Model PE Type Max Context
Context Length Extension: Modern models use tricks like YaRN (Yet another RoPE
extensioN) or position interpolation to extend beyond their training context length. For
example, training on 4K tokens but later serving 128K tokens by scaling the position
frequencies.
Common Misconceptions
Misconception Reality
"Without PE, the model is completely Without PE, the model treats input as a set (like a bag of
random about order" words) — not random, but order-blind
"Sinusoidal PE is still state of the art" RoPE is now standard for LLMs
"PE is added once and that's it" In RoPE, position information is injected at every
attention layer
"Larger max position = larger memory" For sinusoidal PE, no extra memory. For learned PE, yes.
FAQ
Q: What happens if input is longer than max context? A: The model simply cannot
process it. You get an error or the excess tokens are truncated. This is why "context length"
is such an important spec for LLMs.
Q: What is RoPE and why is it better? A: RoPE (Rotary Position Embedding) encodes
position by rotating query and key vectors before computing attention. It naturally extends
to longer sequences than trained on and handles relative positions more gracefully.
Q: Is positional encoding trainable? A: Sinusoidal PE is not — it's fixed by formula.
Learned PE is trainable. RoPE has no parameters itself but is applied during attention
computation.
Interview Questions
1. Why does a Transformer need positional encoding? (Attention is permutation-
invariant — it processes all tokens simultaneously with no built-in order)
2. What is the difference between absolute and relative positional encoding?
(Absolute: encodes position 0, 1, 2... Relative: encodes distance between tokens)
3. What is RoPE? (Rotary Position Embedding — encodes position by rotating Q/K
vectors)
4. What is the maximum context length of GPT-2? (1,024 tokens)
5. What happens if you remove positional encoding entirely? (Model becomes order-
blind — "cat sat" and "sat cat" look identical)
Hands-on Exercise
python
import numpy as np
import [Link] as plt
pe = sinusoidal_pe(100, 64)
[Link](figsize=(12, 6))
[Link](pe, aspect='auto', cmap='RdBu')
[Link]()
[Link]("Embedding Dimension")
[Link]("Position")
[Link]("Sinusoidal Positional Encoding")
[Link]("positional_encoding.png")
[Link]()
# Each row is a unique "fingerprint" for each position
Summary
Positional encoding solves the Transformer's order-blindness by injecting position
information into each token's embedding. The three main approaches are: sinusoidal (fixed
formula), learned (trained parameters), and RoPE (rotation-based, now standard). Without
positional encoding, the model cannot distinguish "dog bites man" from "man bites dog."
Real-World Analogy
Imagine a meeting room with 10 people, each holding a document. When Person 5
(representing "it") wants to write their report, they ask every person in the room: "How
relevant is your document to mine?" Each person responds with a score (0 to 1). Person 5
then takes a weighted combination of everyone's documents — mostly from Person 1 (who
has the "animal" document) and a little from everyone else.
That weighted combination is "it"'s new, context-enriched representation.
Internal Architecture
Self attention uses three learned linear transformations applied to each token:
Q (Query): What am I looking for?
K (Key): What do I have to offer?
V (Value): What information do I actually give out?
These are all linear projections (matrix multiplications) of the token embeddings:
Where:
X is the input matrix (shape: [L × d_model] )
W_Q , W_K , W_V are learnable weight matrices
d_k is the key/query dimension (typically d_model / num_heads )
The attention output is:
Q = X × W_Q: [L × d_k]
K = X × W_K: [L × d_k]
V = X × W_V: [L × d_v]
Step 2 — Compute Attention Scores For each query (row in Q), compute how much it
"matches" each key (row in K) using dot product:
Each entry Scores[i][j] is the dot product between query of token i and key of token j .
A higher score means token i pays more attention to token j .
Step 3 — Scale Divide by √d_k to prevent scores from becoming too large (which would
cause gradients to vanish in softmax):
Each row sums to 1. Row i tells you how much token i attends to every other token.
Step 5 — Weighted Sum of Values Multiply attention weights by the value matrix:
Each output row is a weighted average of all value vectors, where the weights come from
how much attention was paid.
Input X
Shape: [L × d_model]
Q = X × W_Q K = X × W_K
Shape: [L × d_k] Shape: [L × d_k]
Q × K^T
Shape: [L × L]
(Raw attention scores)
Softmax (row-wise)
Shape: [L × L] V = X × W_V
(Attention weights: each Shape: [L × d_v]
row sums to 1)
×V
Weighted sum of values
Output
Shape: [L × d_v]
(Context-enriched
representations)
Explanation: This diagram shows the complete single-head attention computation. The Q
and K matrices compute compatibility scores, which after scaling and softmax become
attention weights. These weights determine how much of each Value vector contributes to
each output position.
The actual numbers aren't critical for understanding. The point is: after softmax, we get a
4×4 matrix where each row shows how much each word attends to every other word.
Data Flow
3 Projections (learned)
Input Score Computation
Q: [L × d_k]
Output
V: [L × d_v]
Memory Flow
This is the most memory-intensive part of the Transformer:
Tensor Shape Memory (L=2048, d=4096, d_k=128)
This is why FlashAttention (covered in Module 02) was invented: it computes attention
without materializing the full L×L matrix.
W_Q, W_K, W_V matrix multiplications ❌ Too slow ✅ Batched GEMM on Tensor Cores
Production Examples
Causal (Masked) Self Attention: In decoder-only models, a token can only attend to
previous tokens (not future ones — that would be cheating during generation). This is
implemented by setting future positions in the attention score matrix to -∞ before
softmax, so they get weight ≈ 0.
Causal mask for 4 tokens:
I love dogs .
I [ 1, 0, 0, 0 ]
love [ 1, 1, 0, 0 ]
dogs [ 1, 1, 1, 0 ]
. [ 1, 1, 1, 1 ]
Common Misconceptions
Misconception Reality
"Q, K, V are different inputs" Q, K, V all come from the same input X, just transformed
differently
"Attention weights show what They're intermediate computations, not reliable explanations
the model thinks"
"Division by √d_k is optional" Without it, dot products become very large in high dimensions,
causing softmax saturation and vanishing gradients
FAQ
Q: Why use three different matrices (W_Q, W_K, W_V) instead of one? A: They serve
different roles. Q asks "what am I looking for?" K says "here's what I can match." V says
"here's what I'll give you." Using three separate transformations gives the model more
flexibility to learn different relationships.
Q: What does "attention" actually capture? A: In practice, attention heads learn various
things: some track syntactic dependencies (subject-verb), some track coreference ("it" →
"animal"), some track positional proximity, etc.
Q: Is attention always over the full sequence? A: In vanilla attention, yes. But techniques
like Sparse Attention (BigBird, Longformer) only attend to a subset of tokens for efficiency.
Interview Questions
1. Write the self-attention formula. (Attention(Q,K,V) = softmax(QK^T/√d_k)V)
2. Why do we scale by 1/√d_k? (Prevent dot products from growing too large, causing
softmax saturation)
3. What is the time complexity of self attention? (O(L² · d) in sequence length L and
dimension d)
4. What is a causal mask? (A triangular mask that prevents tokens from attending to
future tokens)
5. What is FlashAttention? (A memory-efficient attention algorithm that avoids
materializing the L×L matrix)
Hands-on Exercise
python
import torch
import [Link] as F
# Attention scores
scores = (Q @ [Link](-2, -1)) / (d_k ** 0.5) # [batch, L, L]
# Causal mask
if causal:
mask = [Link]([Link](L, L), diagonal=1).bool()
scores = scores.masked_fill(mask, float('-inf'))
# Softmax
weights = [Link](scores, dim=-1) # [batch, L, L]
# Weighted sum
output = weights @ V # [batch, L, d_k]
return output, weights
# Test
batch, L, d_model, d_k = 1, 4, 8, 4
X = [Link](batch, L, d_model)
W_Q = [Link](d_model, d_k)
W_K = [Link](d_model, d_k)
W_V = [Link](d_model, d_k)
Summary
Self attention lets every token attend to every other token by computing query-key dot
products to get attention weights, then using those weights to compute a weighted sum of
value vectors. This produces a new, context-enriched representation for each token. The
main bottleneck is the O(L²) attention matrix. Modern optimizations like FlashAttention
reduce this cost significantly.
Real-World Analogy
Imagine a team of 8 detectives investigating the same crime scene. Each detective
specializes in different evidence:
Detective 1 looks only at fingerprints.
Detective 2 looks only at footprints.
Detective 3 looks only at financial records.
Detective 8 looks only at phone records.
At the end, all their reports are combined into one comprehensive report. This combined
report is far richer than any single detective's findings alone.
Multi-head attention does the same: 8 (or more) attention "detectives" each find different
relationships, and their findings are combined.
Internal Architecture
Input X
[L × d_model]
Multi-Head Output
[L × d_model]
Explanation: Each head independently performs self attention with its own set of Q, K, V
weight matrices. The individual outputs (each of shape [L × d_k] ) are concatenated along
the last dimension, giving [L × d_model] . A final linear projection W_O brings it back to
shape [L × d_model] .
python
# Weight matrices for all heads (often stored as one big matrix and split)
W_Q: [d_model × d_model] # = h × (d_model × d_k)
W_K: [d_model × d_model]
W_V: [d_model × d_model]
# Project input
Q_all = X @ W_Q # [L × d_model]
K_all = X @ W_K
V_all = X @ W_V
python
# Batched attention across all heads simultaneously
# [h × L × d_k] @ [h × d_k × L] = [h × L × L]
scores = Q @ [Link](-2, -1) / (d_k ** 0.5)
weights = softmax(scores, dim=-1) # [h × L × L]
attended = weights @ V # [h × L × d_k]
python
# Output projection
output = attended @ W_O # [L × d_model]
Why GQA? In MHA, during inference, you store K and V caches for all heads. With 32 heads
and 32 layers, this is huge. GQA reduces K and V to fewer groups (e.g., 8 groups instead of 32
heads), drastically cutting the KV cache size (discussed in Module 02) while retaining most
of the quality.
Grouped Query Attention (h=4, g=2)
Q₁ Q₂ K_group1 Q₃ Q₄ K_group2
Q₁ K₁ Q₂ K₂ Q₃ K₃ Q₄ K₄
Explanation: In GQA with 4 query heads and 2 groups, K and V are shared within each
group. Q₁ and Q₂ share one K/V pair, Q₃ and Q₄ share another. This halves the K/V cache
size.
Data Flow
flowchart TD
X["X: [batch, L, d_model]"]
W_QKV["W_Q, W_K, W_V\n[d_model × d_model each]"]
PROJ["Linear Projections"]
SPLIT["Reshape & Split Heads\n[batch, h, L, d_k]"]
ATTN["Scaled Dot-Product Attention\n(on all heads in parallel)\n[batch, h, L, d_k]"
MERGE["Concatenate Heads\n[batch, L, h × d_k] = [batch, L, d_model]"]
WO_PROJ["W_O Projection\n[d_model × d_model]"]
OUT["Output: [batch, L, d_model]"]
X --> PROJ
W_QKV --> PROJ
PROJ --> SPLIT --> ATTN --> MERGE --> WO_PROJ --> OUT
Memory Flow
For MHA with h=12, L=1024, d_model=768 (GPT-2 small):
d_k = 768/12 = 64
Q, K, V each: [12 × 1024 × 64] = 786,432 floats = 3 MB each
Attention matrix per head: [1024 × 1024] = 1M floats = 4 MB per head = 48 MB for all
heads
Output (after projection): [1024 × 768] = ~3 MB
For a 40B parameter model at inference with L=8192, h=64, d_k=128:
Attention matrices: 64 × 8192 × 8192 × 2 bytes ≈ 8.6 GB per layer!
This is why FlashAttention (not materializing the full matrix) is essential in production.
All operations are on GPU. The GPU processes all heads simultaneously using the batch
dimension.
Common Misconceptions
Misconception Reality
"Each head looks at a different part of Each head looks at the full sentence but attends to
the sentence" different relationships
"More heads = better" Diminishing returns; too many small heads lose capacity
"W_Q, W_K, W_V are separate per Usually stored as one big matrix and reshaped
head"
Interview Questions
1. What is the difference between single-head and multi-head attention? (MHA runs
attention multiple times with different projections, capturing diverse relationships)
2. If d_model=1024 and we have 16 heads, what is d_k? (1024/16 = 64)
3. What is GQA and why is it used? (Grouped Query Attention — shares K/V across
query groups, reducing KV cache size)
4. What is the W_O matrix in MHA? (Output projection that combines all heads' outputs
back to d_model)
5. How does MHA improve over single-head attention? (Captures multiple different
types of relationships simultaneously)
Hands-on Exercise
python
import torch
import [Link] as nn
import [Link] as F
class MultiHeadAttention([Link]):
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.num_heads = num_heads
self.d_k = d_model // num_heads
if causal:
mask = [Link]([Link](L, L, device=[Link]), diagonal=1).bo
scores = scores.masked_fill([Link](0).unsqueeze(0), float('
# Test
mha = MultiHeadAttention(d_model=64, num_heads=8)
x = [Link](2, 10, 64) # batch=2, seq=10, d_model=64
out = mha(x, causal=True)
print(f"Output shape: {[Link]}") # [2, 10, 64]
Summary
Multi-head attention runs self attention multiple times in parallel with different learned
projections. Each head captures different linguistic relationships. Outputs are concatenated
and projected. Modern variants (MQA, GQA) reduce K/V heads to save memory during
inference. This is the core computational bottleneck in Transformer inference.
Where:
W₁ : shape [d_model × d_ff] (expands dimension)
W₂ : shape [d_ff × d_model] (compresses back)
d_ff : the "inner dimension," typically 4 × d_model
Internal Architecture
flowchart LR
X["Token Representation\n[d_model = 768]"]
W1["W₁\n[768 × 3072]\n(Expand)"]
BIAS1["+ b₁"]
ACT["GELU Activation\n(Non-linearity)"]
W2["W₂\n[3072 × 768]\n(Compress)"]
BIAS2["+ b₂"]
OUT["Output\n[d_model = 768]"]
X --> W1 --> BIAS1 --> ACT --> W2 --> BIAS2 --> OUT
This has been empirically shown to improve performance. LLaMA uses this variant.
python
h = x @ W1 + b1 # [d_model] → [d_ff]
# For GPT-2: [768] → [3072]
# This "expands" the representation into a higher-dimensional space
Step 2 — Non-linearity
python
h = gelu(h) # [d_ff] → [d_ff] (same shape, but non-linear transformatio
# GELU: Gaussian Error Linear Unit
# GELU(x) ≈ x * Φ(x) where Φ is cumulative normal distribution
# Smoother than ReLU (which just clips negatives to 0)
Step 3 — Compress
python
Key insight: This is applied independently to each token. There is no interaction between
tokens in the FFN (unlike attention). The FFN is purely per-position.
Data Flow
flowchart TD
X["Attention Output\n[batch × L × d_model]"]
W1m["W₁: [d_model × d_ff]\nshape: [768 × 3072]"]
E1["Expand: [batch × L × d_ff]"]
ACT["Activation (GELU/SwiGLU)\n[batch × L × d_ff]"]
W2m["W₂: [d_ff × d_model]\nshape: [3072 × 768]"]
E2["Compress: [batch × L × d_model]"]
ADD["+ Residual Connection (Chapter 8)"]
OUT["FFN Output: [batch × L × d_model]"]
X --> E1
W1m --> E1
E1 --> ACT
ACT --> E2
W2m --> E2
E2 --> ADD
X --> ADD
ADD --> OUT
Memory Flow
Item Shape Memory (GPT-2, L=1024)
The intermediate activation [L × d_ff] is 4× larger than input and is the memory peak
within the FFN.
For LLaMA 3 70B: d_model=8192, d_ff=28672, L=4096:
The FFN accounts for ~2/3 of the parameters in a standard Transformer (attention is the
other ~1/3). In a 7B parameter model:
~2.3B parameters in attention layers
~4.7B parameters in FFN layers
Activation Functions Comparison
Activation Formula Used In Notes
Production Examples
Mixture of Experts (MoE): A powerful variant where instead of one FFN, you have many
FFNs (called "experts"). For each token, only a small number of experts (e.g., 2 out of 64) are
activated.
flowchart TD
X["Token x"]
G["Gating Network\n(Router)\nOutputs: which 2 of 8 experts to use"]
E1["Expert 1\n(FFN)"]
E2["Expert 2\n(FFN)"]
E3["Expert 3\n(FFN, not selected)"]
E8["Expert 8\n(FFN, not selected)"]
COMB["Weighted Combination\nof selected experts' outputs"]
OUT["MoE Output"]
X --> G
G -->|"weight 0.6"| E1
G -->|"weight 0.4"| E2
G -->|"0 (not selected)"| E3
G -->|"0 (not selected)"| E8
E1 --> COMB
E2 --> COMB
COMB --> OUT
Common Misconceptions
Misconception Reality
"FFN interacts with multiple tokens" FFN is purely per-position — no inter-token interaction
"The FFN is less important than FFN contains ~2/3 of parameters and stores most factual
attention" knowledge
"The expansion ratio is always exactly Varies by model and architecture choice
4×"
Interview Questions
1. What is the FFN's role in a Transformer block? (Non-linear per-position
transformation; stores factual knowledge)
2. What are the shapes of W₁ and W₂ in GPT-2? (W₁: [768×3072], W₂: [3072×768])
3. What is MoE (Mixture of Experts)? (Multiple FFN experts per layer; only top-k are
activated per token)
4. What is SwiGLU? (A gated activation function: SiLU(Wx) ⊙ Vx, used in
LLaMA/PaLM)
5. Does the FFN see other tokens? (No — applied independently to each token)
Hands-on Exercise
python
import torch
import [Link] as nn
import [Link] as F
class FeedForward([Link]):
def __init__(self, d_model, d_ff):
super().__init__()
self.W1 = [Link](d_model, d_ff)
self.W2 = [Link](d_ff, d_model)
class FeedForwardSwiGLU([Link]):
"""LLaMA-style FFN with SwiGLU activation"""
def __init__(self, d_model, d_ff):
super().__init__()
self.W1 = [Link](d_model, d_ff, bias=False)
self.W2 = [Link](d_ff, d_model, bias=False)
[Link] = [Link](d_model, d_ff, bias=False)
# Test
ffn = FeedForward(d_model=768, d_ff=3072)
x = [Link](2, 10, 768) # batch=2, seq=10, d_model=768
out = ffn(x)
print(f"Output shape: {[Link]}") # [2, 10, 768]
Summary
The FFN is the second sublayer in each Transformer block. It expands each token's
representation to a higher dimension, applies a non-linear activation, then compresses
back. It is applied independently per token and contains most of the model's learned factual
knowledge. Modern LLMs use SwiGLU activation. The MoE variant uses multiple sparse
FFN experts for better parameter efficiency.
Chapter 7 — LayerNorm
What is it?
Layer Normalization (LayerNorm) is a technique that normalizes the activations (the
numbers flowing through the network) within each layer to have mean ≈ 0 and variance ≈ 1.
It then applies learnable scale (γ, gamma) and shift (β, beta) parameters.
Formula:
LayerNorm(x) = γ · (x - μ) / √(σ² + ε) + β
Where:
μ = mean of x across the feature dimension
σ² = variance of x
ε = small constant (e.g., 1e-5) to prevent division by zero
γ, β = learnable parameters (initialized to 1 and 0 respectively)
Real-World Analogy
Imagine you are grading students from 5 different countries using different grading scales:
Country A: 0–100
Country B: 0–10
Country C: 0–4.0 GPA
Country D: A, B, C, D, F
Country E: 0–20
To fairly compare them, you "normalize" all grades to a standard scale (mean=0, std=1).
Now a grade of 0 means "average for their system" and +1 means "one standard deviation
above average."
LayerNorm does this for activations within each layer — ensuring all features are on the
same scale, making the model easier to train.
Internal Architecture
flowchart LR
X["Input x\n[d_model]"]
MU["Compute Mean\nμ = mean(x)"]
SIG["Compute Variance\nσ² = var(x)"]
NORM["Normalize\nx̂ = (x - μ) / √(σ² + ε)"]
SCALE["Scale & Shift\nγ · x̂ + β\n(γ, β: learned params)"]
OUT["Normalized Output\n[d_model]"]
X --> MU
X --> SIG
X --> NORM
MU --> NORM
SIG --> NORM
NORM --> SCALE --> OUT
Pre-LN vs Post-LN
The position of LayerNorm in the Transformer block matters significantly:
Post-LN (Original paper):
RMSNorm(x) = x / √(mean(x²) + ε) · γ
Simpler than LayerNorm (no mean subtraction, no β), computationally cheaper, similar
performance. LLaMA uses RMSNorm.
Step 1 - Mean:
μ = (2.0 + (-1.0) + 4.0 + 3.0) / 4 = 8.0 / 4 = 2.0
Step 2 - Variance:
σ² = ((2-2)² + (-1-2)² + (4-2)² + (3-2)²) / 4
= (0 + 9 + 4 + 1) / 4 = 14/4 = 3.5
Step 3 - Normalize:
x̂ = (x - μ) / √(σ² + ε) (ε = 1e-5)
x̂ = ([2-2, -1-2, 4-2, 3-2]) / √3.5
x̂ = [0, -3, 2, 1] / 1.871
x̂ = [0.0, -1.604, 1.069, 0.534]
Now the output has mean ≈ 0 and std ≈ 1. The model can learn γ and β to rescale if needed.
Data Flow
flowchart TD
X["Input x: [batch, L, d_model]"]
X --> M & V
M & V & X --> N --> SS --> OUT
style LN fill:#e8f4f8
Memory Flow
Item Size
In practice, all four steps are fused into a single GPU kernel (fused LayerNorm) for
efficiency.
Production Examples
Model Normalization Notes
"BatchNorm and LayerNorm are BatchNorm normalizes across the batch dimension; LayerNorm
the same" across feature dimension — different!
"LayerNorm learns the right It normalizes to 0 mean/1 variance, then applies learnable γ/β on
mean/variance" top
"RMSNorm is worse than In practice, RMSNorm performs similarly with less compute
LayerNorm"
FAQ
Q: What is the difference between BatchNorm and LayerNorm? A: BatchNorm
normalizes across the batch dimension (requires large batch sizes, doesn't work with
variable-length sequences). LayerNorm normalizes across the feature dimension for each
sample independently. LayerNorm works for any sequence length and batch size.
Q: What happens without LayerNorm? A: Training becomes very unstable. Loss oscillates
wildly or diverges. Deep networks (32+ layers) are nearly impossible to train without
normalization.
Q: Why does RMSNorm omit the mean subtraction? A: The mean subtraction and bias β
add cost with little benefit. Experiments show that just the scale (γ) after RMS
normalization works just as well.
Interview Questions
1. What does LayerNorm normalize across? (The feature/embedding dimension for
each token independently)
2. What is the difference between Pre-LN and Post-LN? (Pre-LN: normalize before
attention/FFN. Post-LN: normalize after. Pre-LN is more stable.)
3. What is RMSNorm? (Simpler LayerNorm without mean subtraction; used in LLaMA)
4. What are γ and β in LayerNorm? (Learned scale and shift parameters — allow the
model to undo normalization if needed)
5. Why is BatchNorm not used in Transformers? (Requires fixed batch statistics;
doesn't work well with variable-length sequences)
Hands-on Exercise
python
import torch
import [Link] as nn
class LayerNorm([Link]):
def __init__(self, d_model, eps=1e-5):
super().__init__()
[Link] = [Link]([Link](d_model))
[Link] = [Link]([Link](d_model))
[Link] = eps
class RMSNorm([Link]):
def __init__(self, d_model, eps=1e-8):
super().__init__()
[Link] = [Link]([Link](d_model))
[Link] = eps
# Test
x = [Link]([[2.0, -1.0, 4.0, 3.0]])
ln = LayerNorm(d_model=4)
rms = RMSNorm(d_model=4)
print(f"Input: {x}")
print(f"LayerNorm:{ln(x).detach()}")
print(f"RMSNorm: {rms(x).detach()}")
print(f"Mean of LN output: {ln(x).mean().item():.4f}") # ≈ 0
print(f"Std of LN output: {ln(x).std().item():.4f}") # ≈ 1
Summary
LayerNorm stabilizes training by normalizing activations to zero mean and unit variance,
then applying learnable scale/shift. Pre-LN (normalizing before each sublayer) is the
modern standard. RMSNorm is a faster variant used in LLaMA and most modern LLMs.
Without normalization, deep Transformer training is nearly impossible.
Chapter 8 — Residual Connections
What is it?
A Residual Connection (also called a skip connection) is when you add the input of a layer
directly to its output:
output = layer(x) + x
This seems almost trivially simple — and it is! But it is one of the most impactful ideas in
deep learning. Introduced in ResNet (2015) for image classification, it was then adopted in
the Transformer architecture.
In a Transformer block:
# After FFN:
x = x + FFN(LayerNorm(x))
The "+1" ensures gradients always flow, even if the layer's contribution vanishes.
Real-World Analogy
Imagine water flowing through a series of filters. Each filter modifies the water but also
clogs up a little. Without bypass pipes, water barely makes it through many filters. With
bypass pipes that go around each filter and rejoin the main flow, water always has a direct
path and the filters can safely do their job without bottlenecking the flow.
Residual connections are the bypass pipes for gradient flow.
Internal Architecture
flowchart TD
X["Input x\n[L × d_model]"]
OUT["Output\n[L × d_model]"]
Explanation: The key feature is the two arrows bypassing the LayerNorm+Attention and
LayerNorm+FFN respectively. These arrows carry the original x directly to the addition
operation. So the total output is x + transformation(x) , not just transformation(x) .
h₁ = layer₁(x)
h₂ = layer₂(h₁)
...
h₁₂ = layer₁₂(h₁₁)
If any layer produces small numbers, the information is gone. If any layer's gradient is small,
earlier layers don't learn.
With residuals: The network computes:
h₁ = x + layer₁(x)
h₂ = h₁ + layer₂(h₁)
...
h₁₂ = h₁₁ + layer₁₂(h₁₁)
Notice that h₁₂ implicitly contains x (the original input) because each step adds to the
previous:
Each layer only needs to learn the residual (the small change needed), not the full
transformation. This is much easier to learn!
Residual stream view: Modern interpretability research views the Transformer as a
"residual stream" — a vector that flows through all layers. Each attention head and FFN
reads from and writes to this stream additively. No information is ever destroyed — it
accumulates.
Data Flow
flowchart LR
subgraph ONE_BLOCK["One Transformer Block"]
IN["x (input)\n[L × d_model]"]
Memory Flow
Residual connections don't add memory — the x tensor that gets added is already in
memory. The addition is element-wise and happens in-place.
However, for gradient computation during training, you need to store the intermediate
activations to compute gradients. For a model with 32 layers, storing all intermediate
activations requires enormous memory. This is why gradient checkpointing (also called
activation checkpointing) is used — it recomputes activations during the backward pass
instead of storing them.
Residual addition is the cheapest operation in the Transformer. It's literally one GPU
instruction.
Production Examples
GPT-2: 12 transformer blocks, each with 2 residual connections (one after attention,
one after FFN)
GPT-3: 96 transformer blocks = 192 total residual connections
LLaMA 3 70B: 80 transformer blocks = 160 residual connections
Initialization: Models like GPT-2 scale the residual connection by 1/√(2N) (where N is the
number of blocks) to keep the variance stable at initialization. LLaMA uses a similar
technique.
Common Misconceptions
Misconception Reality
"Residual connections add complexity" They are literally just + — zero added complexity
"Skip connections mean the layer is The layer still trains; it just learns the residual
optional" (delta)
"You need residuals only in very deep They help even in shallow networks
networks"
"The gradient only flows through the skip" It flows through both paths — skip and through the
layer
Interview Questions
1. What is a residual connection and why is it needed? (Adding input to output: output
= layer(x) + x . Prevents vanishing gradients.)
2. What problem do residual connections solve? (Vanishing gradient problem in deep
networks)
3. What is the "residual stream" in interpretability? (The view that the residual vector
accumulates contributions from all layers additively)
4. How many residual connections does a standard Transformer block have? (2 — one
after attention, one after FFN)
5. What is gradient checkpointing? (A technique that recomputes activations during
backward pass to save memory)
Hands-on Exercise
python
import torch
import [Link] as nn
class TransformerBlock([Link]):
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
[Link] = [Link](d_model, num_heads, batch_first=True
[Link] = [Link](
[Link](d_model, d_ff),
[Link](),
[Link](d_ff, d_model)
)
self.ln1 = [Link](d_model)
self.ln2 = [Link](d_model)
# Test
block = TransformerBlock(d_model=64, num_heads=8, d_ff=256)
x = [Link](2, 10, 64) # [batch=2, seq=10, d_model=64]
out = block(x)
print(f"Input shape: {[Link]}") # [2, 10, 64]
print(f"Output shape: {[Link]}") # [2, 10, 64]
# Same shape — residual connections preserve dimensions
Summary
Residual connections add the layer's input directly to its output ( x + layer(x) ). They solve
the vanishing gradient problem by providing direct gradient paths through deep networks.
They appear twice per Transformer block (after attention and after FFN). They are
computationally free (just addition) but enormously impactful for training stability.
Chapter 9 — Decoder Only Architecture
What is it?
A Decoder-Only Transformer is a Transformer that uses only the decoder portion of the
original encoder-decoder architecture — with one modification: the cross-attention (which
previously attended to encoder outputs) is removed. What remains is a stack of blocks, each
containing:
1. Masked Multi-Head Self-Attention (causal — can only look at past tokens)
2. Feed Forward Network
3. Layer Norm (Pre-LN in modern models)
4. Residual Connections
This is the architecture used by GPT, Claude, LLaMA, Mistral, Gemini, PaLM, and
essentially all modern text-generation LLMs.
Real-World Analogy
A decoder-only model is like an author writing a novel word by word:
When writing word 500, you can see words 1–499.
You cannot see words 501–1000 (they haven't been written yet).
The causal mask enforces this — each word can only be informed by words before it.
An encoder-decoder model is like a translator who:
1. First reads the entire source text (encoder — bidirectional, sees everything)
2. Then generates the translation word by word (decoder — causal)
For a general-purpose AI assistant, you don't need the encoder step. You just generate
responses directly, treating the conversation history as your context.
Internal Architecture
flowchart TD
subgraph INPUT["Input Processing"]
T["Input Tokens\n[token_1, ..., token_n]"]
E["Embedding\n[n × d_model]"]
PE["+ Positional Encoding"]
end
subgraph OUTPUT["Output"]
LNF["Final LayerNorm"]
LINEAR["Linear: [d_model → vocab_size]"]
SOFT["Softmax → probabilities"]
SAMPLE["Sample / Argmax → Next Token"]
end
Position: 0 1 2 3 4
Tokens: The cat sat on mat
Training vs Inference
Training (Teacher Forcing):
Feed the entire target sequence at once.
Use causal masking to prevent future token leakage.
At every position, compute the probability of the next token.
Compute cross-entropy loss across all positions simultaneously.
Very efficient — one forward pass computes loss for all positions.
flowchart LR
subgraph TRAIN["Training: Parallel Across Positions"]
direction TB
IN["Input: [The, cat, sat, on]"]
TGT["Target: [cat, sat, on, mat]"]
MASK["Causal Mask Applied"]
PRED["Predictions at all positions simultaneously"]
LOSS["Cross-Entropy Loss"]
IN & MASK --> PRED --> LOSS
TGT --> LOSS
end
flowchart TD
P["Prompt: 'The cat'"]
S1["Forward pass\n→ predict 'sat'"]
S2["Append: 'The cat sat'\n→ Forward pass\n→ predict 'on'"]
S3["Append: 'The cat sat on'\n→ Forward pass\n→ predict 'the'"]
SN["...repeat until <eos> or max length"]
Data Flow
flowchart TD
subgraph INFERENCE["Autoregressive Inference"]
PROMPT["Prompt Tokens\n[token₁, ..., tokenₚ]"]
PROMPT --> PREFILL --> GEN1 --> GEN2 --> GENN --> OUT
Memory Flow
At inference, for a model with N layers, d_model, and current sequence length L:
Tensor Memory
This is why LLaMA 70B needs 4+ A100 GPUs (each with 80GB VRAM).
CPU vs GPU Responsibilities
Phase CPU GPU
Prompt parsing ✅ ❌
Tokenization ✅ ❌
Sampling ❌ (sometimes) ✅
Production Examples
Model Layers Heads d_model Context Vocab
Common Misconceptions
Misconception Reality
"You need an encoder to understand input" The decoder processes and understands input
and generates output in one forward pass
"Causal masking is only for generation" It's used during training too — to predict every
position's next token simultaneously
"All tokens are generated one by one" Prompt tokens are processed in parallel (prefill);
only generation is sequential
Interview Questions
1. What is a decoder-only Transformer? (A Transformer with only causal self-attention
blocks — no encoder or cross-attention)
2. What is the causal mask? (A triangular mask preventing each token from attending to
future tokens)
3. What is the difference between prefill and decode? (Prefill processes prompt in
parallel; decode generates new tokens one at a time)
4. Why does GPT use decoder-only? (For language modeling — predicting next token
— which naturally fits autoregressive generation)
5. What is teacher forcing? (During training, feeding ground-truth previous tokens as
input rather than the model's own predictions)
Hands-on Exercise
python
import torch
import [Link] as nn
import [Link] as F
class DecoderOnlyTransformer([Link]):
def __init__(self, vocab_size, d_model, num_heads, d_ff, num_layers, max_se
super().__init__()
[Link] = [Link](vocab_size, d_model)
self.pos_embedding = [Link](max_seq_len, d_model)
[Link] = [Link]([
TransformerBlock(d_model, num_heads, d_ff)
for _ in range(num_layers)
])
self.ln_f = [Link](d_model)
[Link] = [Link](d_model, vocab_size, bias=False)
# Weight tying: share embedding and output weights
[Link] = [Link]
x = [Link](token_ids) + self.pos_embedding(positions)
x = self.ln_f(x)
logits = [Link](x) # [B, L, vocab_size]
return logits
Real-World Analogy
Imagine you're a judge on a quiz show. After hearing all the clues, you have a "confidence
state" in your head (complex, hard to describe). When it's time to answer, you must choose
one word from a list of 128,000 options. Your internal confidence → a ranking of all possible
answers → you choose based on that ranking.
The linear projection is "converting your confidence into rankings." Softmax is
"normalizing those rankings into percentages." The sampling strategy is "how you actually
pick the answer."
Internal Architecture
flowchart TD
HS["Last Hidden State\n[batch × L × d_model]\ne.g., [2 × 1024 × 4096]"]
HS --> LN --> LINEAR --> LOGITS --> LAST --> SOFTMAX --> SAMPLE --> TOKEN
python
python
python
last_logits = logits[:, -1, :] # [B, vocab_size] — only the last token's pred
python
python
Sampling Strategies
This is crucial for LLM behavior. The same model with different sampling produces very
different text.
Greedy Decoding
python
python
python
python
flowchart LR
subgraph PROBS["Example Probabilities"]
T1["'cat' = 40%"]
T2["'dog' = 25%"]
T3["'tree' = 15%"]
T4["'car' = 10%"]
T5["'..." = 10%\n(other 128K tokens)"]
end
subgraph GREEDY["Greedy"]
G["Always pick 'cat' (40%)"]
end
Beam Search
A more sophisticated generation strategy (used more in translation than LLMs):
Maintain B best partial sequences (beams) at each step
For each beam, expand with top tokens
Keep top B combined sequences
flowchart TD
S["Start: '<bos>'"]
S --> STEP1
B1_1 --> B2_1 & B2_2
B1_2 --> B2_3 & B2_4
B1_3 --> B2_5
Keep top 3: ["Once upon", "The cat", "A cat"] → continue expanding.
Data Flow
flowchart TD
H["Final Hidden States\n[B × L × d_model]"]
LN["LayerNorm → [B × L × d_model]"]
TEMP["Temperature scaling"]
SF["Softmax: [B × vocab_size]"]
H --> LN
WH --> LOGIT
LN --> LOGIT --> LAST_POS --> TEMP --> SF --> SAMP
Memory Flow
The LM head weight matrix is one of the largest in the model:
Model vocab_size d_model LM head size
With weight tying (LM head = transpose of embedding matrix), this memory is shared.
The logit tensor [B × L × vocab_size] can be huge during training. For batch=32, L=2048,
vocab=50K: 32 × 2048 × 50000 × 2 bytes = 6.5 GB . In practice, the loss is computed
directly from logits without materializing the full softmax.
Production Examples
Logit processors: In production (vLLM, TGI, etc.), "logit processors" are applied before
sampling:
Repetition penalty: Reduce probability of recently generated tokens
Frequency penalty: Penalize frequent tokens
Presence penalty: Penalize any token that has appeared
Stop tokens: If a stop token has high probability, halt generation
Speculative Decoding (preview of Module 02): A small "draft" model generates K tokens
quickly. The large model verifies them all in one parallel forward pass. If accepted, you
generate K tokens with the cost of ~1 forward pass of the big model. Huge speedup.
Common Misconceptions
Misconception Reality
"Softmax always picks the Softmax just converts to probabilities; sampling decides what to
highest probability" pick
"Temperature=0 means greedy" T=0 causes division by zero; instead set T→0 or just use argmax
"Top-p always selects p% of Top-p selects however many tokens are needed to reach p
tokens" cumulative probability — could be 1 or 1000 tokens
"The model knows when to It generates a special <eos> (end-of-sequence) token; the
stop" system then stops generation
FAQ
Q: What is the difference between logits and probabilities? A: Logits are raw,
unnormalized scores (can be any real number). Probabilities are after softmax — they are
positive and sum to 1.
Q: Why not always use greedy decoding? A: Greedy decoding often produces repetitive,
boring text. It always picks the most likely word, which leads to loops like "the the the the..."
Sampling with temperature produces more varied and interesting outputs.
Q: What is a good temperature for coding vs creative writing? A: Coding: 0.0–0.2
(deterministic, precise). Creative writing: 0.7–1.0 (varied, expressive). Very high
temperatures (>1.2) produce incoherent text.
Q: What is the <bos> and <eos> token? A: Beginning-of-sequence and end-of-sequence
tokens. <bos> signals the start of generation. <eos> signals the model has finished
generating. The system monitors for <eos> to stop the generation loop.
Interview Questions
1. What is the LM head? (A linear projection from d_model → vocab_size, converting
hidden states to token logits)
2. What is the difference between top-k and top-p sampling? (Top-k: fixed K
candidates. Top-p: dynamic K based on cumulative probability threshold)
3. What does temperature do to the output distribution? (T<1: sharper, more confident.
T>1: flatter, more random)
4. Why is the LM head often weight-tied with embeddings? (Saves memory; the same
semantic relationships apply — tokens close in embedding space should have similar
logit responses)
5. What is beam search? (Maintains B best partial sequences and expands them, finding
better global sequences than greedy)
Hands-on Exercise
python
import torch
import [Link] as F
# Top-K filtering
if top_k is not None:
values, _ = [Link](logits, top_k)
min_val = values[-1]
logits = logits.masked_fill(logits < min_val, float('-inf'))
python
# Install dependencies
# pip install torch
import torch
import [Link] as nn
import [Link] as F
import math
# ─────────────────────────────────────────────
# 1. DATA PREPARATION
# ─────────────────────────────────────────────
# Encode
data = [Link]([char2idx[c] for c in text], dtype=[Link])
print(f"Vocab size: {vocab_size}")
print(f"Data length: {len(data)} characters")
# Train/val split
n = int(0.9 * len(data))
train_data = data[:n]
val_data = data[n:]
# ─────────────────────────────────────────────
# 2. HYPERPARAMETERS
# ─────────────────────────────────────────────
# ─────────────────────────────────────────────
# 3. DATA LOADER
# ─────────────────────────────────────────────
def get_batch(split):
data = train_data if split == 'train' else val_data
idx = [Link](len(data) - BLOCK_SIZE, (BATCH_SIZE,))
x = [Link]([data[i:i+BLOCK_SIZE] for i in idx])
y = [Link]([data[i+1:i+BLOCK_SIZE+1] for i in idx])
return [Link](DEVICE), [Link](DEVICE)
# ─────────────────────────────────────────────
# 4. MODEL COMPONENTS
# ─────────────────────────────────────────────
class CausalSelfAttention([Link]):
"""Single attention head (we'll use [Link] for multi-head)"""
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.num_heads = num_heads
self.head_dim = d_model // num_heads
# Project to Q, K, V
qkv = [Link](x) # [B, L, 3D]
Q, K, V = [Link](D, dim=-1) # Each [B, L, D]
# Reshape to [B, h, L, d_k]
Q = [Link](B, L, h, d_k).transpose(1, 2)
K = [Link](B, L, h, d_k).transpose(1, 2)
V = [Link](B, L, h, d_k).transpose(1, 2)
# Attention scores
scores = Q @ [Link](-2, -1) / [Link](d_k) # [B, h, L, L]
# Merge heads
out = [Link](1, 2).contiguous().view(B, L, D)
return [Link](out)
class TransformerBlock([Link]):
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
self.ln1 = [Link](d_model)
[Link] = CausalSelfAttention(d_model, num_heads)
self.ln2 = [Link](d_model)
[Link] = [Link](
[Link](d_model, d_ff),
[Link](),
[Link](d_ff, d_model),
[Link](DROPOUT),
)
class TinyGPT([Link]):
def __init__(self):
super().__init__()
self.token_emb = [Link](vocab_size, D_MODEL)
self.pos_emb = [Link](BLOCK_SIZE, D_MODEL)
[Link] = [Link](DROPOUT)
[Link] = [Link](*[
TransformerBlock(D_MODEL, N_HEADS, D_FF)
for _ in range(N_LAYERS)
])
self.ln_f = [Link](D_MODEL)
self.lm_head = [Link](D_MODEL, vocab_size, bias=False)
# Weight tying
self.lm_head.weight = self.token_emb.weight
# Transformer blocks
x = [Link](x)
# Output
x = self.ln_f(x)
logits = self.lm_head(x) # [B, L, vocab_size]
loss = None
if targets is not None:
loss = F.cross_entropy(
[Link](-1, vocab_size),
[Link](-1)
)
return logits, loss
@torch.no_grad()
def generate(self, idx, max_new_tokens, temperature=0.8, top_k=20):
for _ in range(max_new_tokens):
# Trim to BLOCK_SIZE
idx_cond = idx[:, -BLOCK_SIZE:]
# Forward pass
logits, _ = self(idx_cond)
logits = logits[:, -1, :] / temperature # [B, vocab_size]
# Top-K sampling
v, _ = [Link](logits, min(top_k, [Link](-1)))
logits[logits < v[:, [-1]]] = float('-inf')
probs = [Link](logits, dim=-1)
next_token = [Link](probs, num_samples=1)
idx = [Link]([idx, next_token], dim=1)
return idx
# ─────────────────────────────────────────────
# 5. TRAINING LOOP
# ─────────────────────────────────────────────
model = TinyGPT().to(DEVICE)
optimizer = [Link]([Link](), lr=LR)
@torch.no_grad()
def estimate_loss():
[Link]()
losses = {}
for split in ['train', 'val']:
loss_vals = []
for _ in range(50):
x, y = get_batch(split)
_, loss = model(x, y)
loss_vals.append([Link]())
losses[split] = sum(loss_vals) / len(loss_vals)
[Link]()
return losses
print("Starting training...")
for step in range(MAX_ITERS):
x, y = get_batch('train')
logits, loss = model(x, y)
optimizer.zero_grad()
[Link]()
[Link].clip_grad_norm_([Link](), 1.0)
[Link]()
if step % 500 == 0:
losses = estimate_loss()
print(f"Step {step}: train_loss={losses['train']:.4f}, val_loss={losses
# ─────────────────────────────────────────────
# 6. GENERATION
# ─────────────────────────────────────────────
[Link]()
context = [Link]([[char2idx['\n']]], device=DEVICE)
generated_ids = [Link](context, max_new_tokens=300)
generated_text = ''.join([idx2char[[Link]()] for i in generated_ids[0]])
print("\n=== GENERATED TEXT ===")
print(generated_text)
What to observe:
Loss should decrease from ~3.0 to ~1.5+ over 3000 steps
Generated text should start looking like Shakespearean phrases
Increase N_LAYERS, D_MODEL, or MAX_ITERS for better results
Module Summary
Here is a complete picture of how all 10 chapters fit together:
flowchart TD
subgraph CH1["CH1: Transformer Overview"]
ARCH["Architecture Overview\n(Encoder-Decoder / Decoder-Only)"]
end
CH1 --> CH2_3 --> CH4_5 --> CH6 --> CH7_8 --> CH9 --> CH10
Key Takeaways
# Concept One-Line Summary
2 Embeddings Lookup table converting token IDs to dense vectors; shape [vocab ×
d_model]
5 Multi-Head Attention Runs attention h times in parallel; each head captures different
relationships
10 Output Layer LM head (linear) + softmax + sampling gives the next token
Scale More layers + more heads + more d_model + more data = smarter
model
What's Next?
Type NEXT to proceed to Module 02 — LLM Inference, which covers how models like
LLaMA and GPT run efficiently at production scale: KV Cache, PagedAttention,
Continuous Batching, Speculative Decoding, and more.