- Explain how byte-pair encoding builds a vocabulary and tokenize a word with a learned merge list
- Derive scaled dot-product attention, including the reason for the square-root scaling, and compute it by hand
- Implement multi-head causal self-attention and a pre-norm transformer block in PyTorch
- Compare sinusoidal, learned and rotary positional encodings and encoder, decoder and encoder-decoder families
- Count the parameters and KV-cache size of a decoder layer and explain the O(n^2) cost of attention
- Explain what MQA, GQA and FlashAttention optimize and when each matters
Before this: Neural Networks and Backpropagation, Training Deep Networks in Practice, Recurrent Networks and Sequence Modelling
Why the transformer matters
The transformer (Vaswani et al., 2017) is the architecture behind essentially every modern large language model, most speech and translation systems, many vision backbones, and the denoisers inside leading image and video generators. If you work in AI engineering you will read transformer code, size transformer deployments, and debug transformer training. This lesson builds the whole thing from tokens to a working attention layer, with every equation defined and a numeric example you can check by hand.
Intuition: a room full of people passing notes
Picture each token in a sentence as a person in a room holding a card with their word on it. In every round, each person writes a query ("what am I looking for?"), a key ("what do I offer?") and a value ("what I will tell you if you pick me"). Each person compares their query against everyone's keys, decides how much to listen to each person, and updates their card with a weighted blend of the values they heard. After a few dozen rounds, the word "bank" in "river bank" has absorbed information from "river", and the card for "it" knows which noun it refers to. That round is self-attention; the room is one layer; stacking rounds gives the deep network. Unlike an RNN, everyone talks at once, which is what makes the transformer fast to train.
Step 1: tokenization with byte-pair encoding
Models operate on integer tokens, not characters or words. Word-level vocabularies cannot handle unseen words; character-level sequences are long. Byte-pair encoding (BPE), adapted for neural translation by Sennrich, Haddow and Birch (2015), finds a middle ground: start from characters (or raw bytes) and repeatedly merge the most frequent adjacent pair into a new symbol.
Worked example. Corpus word counts: hug ×10, pug ×5, pun ×12, bun ×4, hugs ×5. Start with characters.
- Pair counts:
u g= 10 + 5 + 5 = 20,p u= 5 + 12 = 17,u n= 12 + 4 = 16,h u= 15. Mergeu g→ug. - Recount:
u n= 16,h ug= 15,p u= 12 (onlypunstill has a separateu). Mergeu n→un. - Recount:
h ug= 15,p un= 12. Mergeh ug→hug. - Merge
p un→pun.
The learned merge list is applied in order to new text: bugs → b u g s → b ug s, three tokens; hugs → hug s. Production tokenizers run tens of thousands of merges, operate on UTF-8 bytes so that any string is representable (byte-level BPE, used since GPT-2), and pre-split text on spaces and punctuation. Related schemes include WordPiece (BERT) and the Unigram model in SentencePiece. Vocabulary sizes of roughly 32k to a few hundred thousand tokens are typical. Because merges are learned from the training corpus, common English words often become single tokens, while code, numbers, rare words and many non-English languages are split into more pieces per word, which raises their cost and eats context length.
Step 2: embeddings and positional information
Each token id indexes a row of an embedding matrix , where is vocabulary size and is the model width. Many models tie the output projection to to save parameters.
Self-attention, as defined below, is permutation-equivariant: shuffle the input tokens and the outputs shuffle identically. Word order must therefore be injected explicitly.
Sinusoidal encodings (the original transformer) add a fixed vector to each embedding:
for position and dimension index . Each pair of dimensions oscillates at a different frequency, from fast to very slow, giving every position a unique signature. BERT and GPT-2 instead learned a position-embedding table, which cannot extrapolate past its trained length.
Rotary position embedding (RoPE) (Su et al., 2021) rotates the query and key vectors instead of adding to the input. Split a -dimensional vector into pairs; at position , rotate pair by angle with :
Because rotations compose, : the attention score depends only on the relative offset . Mini-check in two dimensions with and : positions give a rotation difference of and a score of ; positions give exactly the same 0.5. RoPE was adopted by GPT-J and GPT-NeoX and then by LLaMA, and is now widely used in open-weight decoder models; many context-extension methods work by rescaling its frequencies.
Step 3: scaled dot-product attention
Stack the token vectors of a sequence as rows of . Learned matrices project them to queries, keys and values:
Then
where the softmax is applied to each row, and is a mask matrix (all zeros for unmasked attention). Row of holds the similarity of query to every key; softmax turns it into weights that sum to 1; multiplying by returns a weighted average of value vectors.
Why divide by ? If the components of and are independent with mean 0 and variance 1, then has variance . For the raw scores have a standard deviation around 11, pushing the softmax into saturation where one weight is near 1 and gradients for the others are near 0. Dividing by restores unit variance.
Worked example: three tokens,
Scores. ; dividing by gives .
Softmax, row 1. , , so the row sums to and the weights are . Row 2 by symmetry: . Row 3: twice and , sum , weights .
Outputs (weights times ):
- Token 1:
- Token 2:
- Token 3:
Each output is a blend of all values, tilted toward the tokens whose keys best match that token's query.
Causal masking
A language model predicting token must not see tokens after . The causal mask sets for and 0 otherwise, so those entries become after the softmax. In our example: token 1 can see only itself, so its output is . Token 2 sees scores , giving weights and output . Token 3 sees everything and is unchanged at . Masking is what lets a decoder train on all positions of a sequence in parallel while each position still only uses its past.
Step 4: multi-head attention
One attention pattern cannot track syntax, coreference and position all at once. Multi-head attention runs attentions in parallel, each with its own projections of width , concatenates the results and mixes them:
with . In code, all heads come from one matrix multiply reshaped into heads, so the cost is the same as one wide attention.
import math, torch, torch.nn as nn, torch.nn.functional as F
class CausalSelfAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
assert d_model % n_heads == 0
self.h, self.dk = n_heads, d_model // n_heads
self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
self.proj = nn.Linear(d_model, d_model, bias=False) # W_O
def forward(self, x): # x: (B, T, D)
B, T, D = x.shape
q, k, v = self.qkv(x).split(D, dim=-1)
q, k, v = (t.view(B, T, self.h, self.dk).transpose(1, 2) for t in (q, k, v))
scores = q @ k.transpose(-2, -1) / math.sqrt(self.dk) # (B, h, T, T)
future = torch.triu(torch.ones(T, T, dtype=torch.bool, device=x.device), 1)
scores = scores.masked_fill(future, float("-inf")) # causal mask
out = scores.softmax(dim=-1) @ v # (B, h, T, dk)
return self.proj(out.transpose(1, 2).reshape(B, T, D)) # merge heads
torch.manual_seed(0)
layer = CausalSelfAttention(d_model=64, n_heads=4)
x = torch.randn(2, 10, 64)
q, k, v = (t.view(2, 10, 4, 16).transpose(1, 2) for t in layer.qkv(x).split(64, dim=-1))
fused = F.scaled_dot_product_attention(q, k, v, is_causal=True) # PyTorch fused kernel
ref = layer.proj(fused.transpose(1, 2).reshape(2, 10, 64))
print(torch.allclose(layer(x), ref, atol=1e-5)) # True
torch.triu(..., 1) marks the strictly upper triangle — the future. F.scaled_dot_product_attention computes the same thing and, on supported GPUs, dispatches to memory-efficient or FlashAttention kernels; use it in real code.
Step 5: the transformer block
A block wraps attention and a position-wise MLP in residual connections and normalization. Modern models use pre-norm, normalizing the input to each sub-layer:
The original paper used post-norm, , which needs careful learning-rate warmup; pre-norm keeps an unnormalized residual "stream" from input to output and trains stably at depth (analysed by Xiong et al., 2020). LayerNorm normalizes each token vector across its features: , with , the mean and variance of 's components and learned. Many LLMs use RMSNorm, which skips the mean subtraction and .
The MLP acts on each token independently: classically with hidden width and GELU activation . Many LLMs use SwiGLU, , with three matrices and to keep the parameter count similar. Roughly: attention moves information between tokens; the MLP processes information within each token and holds much of the model's stored knowledge.
class Block(nn.Module):
def __init__(self, d_model, n_heads, d_ff):
super().__init__()
self.ln1, self.ln2 = nn.LayerNorm(d_model), nn.LayerNorm(d_model)
self.attn = CausalSelfAttention(d_model, n_heads)
self.mlp = nn.Sequential(nn.Linear(d_model, d_ff), nn.GELU(), nn.Linear(d_ff, d_model))
def forward(self, x):
x = x + self.attn(self.ln1(x)) # pre-norm residual around attention
return x + self.mlp(self.ln2(x)) # pre-norm residual around the MLP
A full decoder-only model is: embedding → blocks → final norm → linear projection to logits, trained with cross-entropy on next-token prediction.
Three families
| Family | Attention | Training objective | Classic example | Typical use |
|---|---|---|---|---|
| Encoder-only | Bidirectional (no mask) | Masked-token prediction | BERT (Devlin et al., 2018) | Classification, retrieval embeddings, tagging |
| Decoder-only | Causal | Next-token prediction | GPT series (Radford et al., 2018 onward) | Generation; nearly all chat LLMs |
| Encoder–decoder | Encoder bidirectional; decoder causal plus cross-attention to encoder outputs | Sequence-to-sequence (T5 used span corruption) | T5 (Raffel et al., 2019) | Translation, summarization, speech-to-text |
In cross-attention the queries come from the decoder and the keys and values from the encoder — exactly Bahdanau attention from the previous lesson, in matrix form.
Counting parameters in a decoder layer
Take , 32 heads of size 128, standard multi-head attention without biases, a SwiGLU MLP with , and two RMSNorms.
- Attention: are each : .
- MLP: three matrices of : .
- Norms: .
- Per layer: 202,383,360 ≈ 202M, two-thirds of it in the MLP.
Thirty-two layers give B. Add an input embedding and an untied output head with : , plus a final norm. Total ≈ 6.74B — the shape of the familiar 2023-era "7B" models. Rule of thumb: a block has about parameters with a GELU MLP ( attention + MLP).
Cost: O(n²) attention and the KV cache
For a sequence of tokens, is an matrix per head. Computing it and multiplying by costs per layer, and storing it naively costs memory per head. Doubling context quadruples the attention cost, while the projections and MLP grow only linearly. At short contexts the MLP dominates; at long contexts attention does.
During generation, a decoder caches each past token's keys and values so it never recomputes them. Per token, the KV cache takes . For the model above in 16-bit: bytes = 512 KiB per token, or 16 GiB at a 32k-token context. See kv-cache-and-context-cost and the AI Index methodology for how this feeds memory estimates.
Efficient attention variants
Multi-query attention (MQA) (Shazeer, 2019) shares a single key head and value head across all query heads. Grouped-query attention (GQA) (Ainslie et al., 2023) is the middle ground: query heads share KV heads in groups. With 32 query heads and 8 KV heads, the KV cache above drops 4× to 128 KiB per token (4 GiB at 32k), and shrink from to , cutting attention parameters from 67.1M to 41.9M per layer. Decoding is usually limited by memory bandwidth, so a smaller cache directly means faster generation and larger batches, with little quality loss for GQA. GQA and other KV-compression schemes are now common in open-weight LLMs; check a model's config for num_key_value_heads.
FlashAttention (Dao et al., 2022) does not change the math — it computes exact attention. It changes where data lives. GPUs have large but comparatively slow high-bandwidth memory (HBM) and tiny, fast on-chip SRAM. Naive attention writes the full score matrix to HBM, reads it back for the softmax, and again for the multiply by ; for long sequences this memory traffic, not arithmetic, is the bottleneck. FlashAttention splits , , into tiles that fit in SRAM and computes the softmax incrementally with the online softmax trick: for each new block of scores it keeps a running maximum , normalizer and output accumulator , updating
and dividing by at the end. The matrix is never materialized, memory becomes linear in , and the backward pass recomputes attention tiles instead of storing them. The result is the same output, much less memory, and substantially faster wall-clock time. Later versions improved GPU work partitioning further.
Common pitfalls
- Mask errors: an off-by-one causal mask (masking the diagonal, or not masking the future) gives a model that looks brilliant in training and fails at generation because it was peeking.
- Padding masks forgotten in batched encoders, or combined incorrectly with causal masks; also left- versus right-padding mismatches in batched generation.
- Softmax in low precision: compute attention softmax and LayerNorm statistics in float32 or with fused kernels; fp16 overflow produces NaNs.
- Missing the scale when hand-writing attention, which saturates the softmax.
- Tokenizer mismatch: using a different tokenizer, or different special tokens or chat template, from the one the model was trained with.
- Assuming linear cost in context: prefill attention is quadratic, and KV-cache memory grows linearly per sequence and per concurrent user.
In practice
- Use maintained implementations (PyTorch
scaled_dot_product_attention, Hugging Facetransformers, vLLM and similar serving engines) rather than hand-rolled attention in production; hand-roll only to learn and test. - Pick architecture by task: encoders for embeddings and classification, decoders for generation, encoder–decoders where input and output are clearly separate (speech recognition, translation).
- Budget memory from parameters plus KV cache, and prefer GQA models for long-context or high-concurrency serving.
- When fine-tuning, keep the original tokenizer and template; when extending context, use the model's documented RoPE scaling rather than inventing your own.
- The next lesson, beyond-transformers, covers architectures that attack the cost.
Key takeaways
- Attention computes softmax(QK^T / sqrt(d_k)) V: each token takes a weighted average of value vectors, weighted by query-key similarity.
- A causal mask of minus infinity above the diagonal lets a decoder train on every position in parallel without seeing the future.
- Modern blocks are pre-norm: x + Attn(Norm(x)) then x + MLP(Norm(x)), with the MLP holding about two-thirds of the parameters.
- RoPE rotates queries and keys so attention scores depend only on relative position.
- Attention cost grows with the square of sequence length, and the KV cache grows linearly per token, layer and KV head.
- GQA shrinks the KV cache by sharing key-value heads, while FlashAttention computes exact attention with far less memory traffic by tiling and online softmax.
Terms in this lesson
- Byte-pair encoding (BPE)
- A tokenization algorithm that builds a vocabulary by repeatedly merging the most frequent adjacent pair of symbols in a training corpus.
- Query, key, value
- Three learned linear projections of each token: queries are compared with keys to produce attention weights that are applied to values.
- Scaled dot-product attention
- The operation softmax(QK^T / sqrt(d_k) + M) V, where M is an optional mask.
- Causal mask
- A mask that sets attention scores from each position to later positions to minus infinity so a token can attend only to itself and earlier tokens.
- Multi-head attention
- Running several attention operations in parallel on lower-dimensional projections and concatenating their outputs through an output projection.
- Rotary position embedding (RoPE)
- A positional scheme that rotates pairs of query and key dimensions by position-dependent angles so their dot product depends on relative position.
- Pre-norm
- A transformer block layout that applies normalization to the input of each sub-layer inside the residual branch rather than after the residual addition.
- KV cache
- Stored keys and values of previously processed tokens, reused during autoregressive generation to avoid recomputation.
- Grouped-query attention (GQA)
- An attention variant in which groups of query heads share a single key and value head, reducing KV-cache size.
- FlashAttention
- An exact attention algorithm that tiles the computation in fast on-chip memory and uses an online softmax to avoid writing the full attention matrix to GPU main memory.
Check your understanding
- 1.
Why are attention scores divided by sqrt(d_k)?
- 2.
A model has 40 layers, 8 KV heads, head dimension 128, and stores its KV cache in 16-bit. How large is the KV cache per token?
- 3.
You need sentence embeddings for semantic search over a document collection and will not generate text. Which family is the most natural starting point?
- 4.
What does FlashAttention primarily optimize?
- 5.
In the decoder-layer example (d = 4096, SwiGLU with d_ff = 11,008, standard multi-head attention), roughly what fraction of the layer's parameters are in the MLP?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
Two tokens have , , , , , , with and a causal mask. Compute both attention outputs, showing the scaled scores and softmax weights.
Show solution
Token 1 can attend only to position 1 (position 2 is masked to ). The softmax over a single entry is 1, so the output is .
Token 2 attends to both. Raw scores: , . Scaled by : . Softmax: , , sum , weights . Output: .
Note that token 1's query was never used against ; that is the mask at work.
- Exercise 2Core
A decoder-only model has , 24 layers, 16 query heads of size 128, GQA with 4 KV heads, a SwiGLU MLP with , two RMSNorms per layer, a final RMSNorm, no biases, and tied input/output embeddings with . (a) Count total parameters. (b) Compute the 16-bit KV cache per token and for an 8,192-token context.
Show solution
(a) Per layer:
- : ; : same, .
- , : each, together.
- Attention total: .
- MLP: .
- Norms: .
- Layer total: .
24 layers: . Tied embedding: (counted once). Final norm: . Total: 1,184,630,784 ≈ 1.18B parameters.
(b) KV cache per token bytes KiB. For 8,192 tokens: MiB per sequence. Without GQA (16 KV heads) it would be 4x larger, 1.5 GiB.
- Exercise 3Challenge
Implement RoPE in PyTorch for a tensor of shape (T, d) given integer positions, and verify numerically that the attention score between a rotated query at position m and a rotated key at position n depends only on n - m.
Show solution
import torch def rope(x, pos, base=10000.0): # x: (T, d) with d even; pos: (T,) integer positions d = x.shape[-1] theta = base ** (-torch.arange(0, d, 2, dtype=torch.float32) / d) # theta_i = base^(-2i/d) ang = pos[:, None].float() * theta[None, :] # (T, d/2) cos, sin = ang.cos(), ang.sin() x1, x2 = x[..., 0::2], x[..., 1::2] # the pairs out = torch.empty_like(x) out[..., 0::2] = x1 * cos - x2 * sin out[..., 1::2] = x1 * sin + x2 * cos return out torch.manual_seed(0) q, k = torch.randn(1, 64), torch.randn(1, 64) def score(m, n): return (rope(q, torch.tensor([m])) * rope(k, torch.tensor([n]))).sum().item() print(score(3, 7), score(103, 107), score(0, 4)) # equal up to float error print(score(3, 8)) # different offset, different score print(abs(score(3, 7) - score(103, 107)) < 1e-3) # TrueEach pair is rotated by angle ; the dot product of two vectors rotated by and equals the dot product of the originals with a single rotation by , so offsets of 4 give the same score whatever the absolute positions. Small differences (around ) come from float32 rounding of larger angles. This interleaved-pair layout is the one in the RoFormer paper; some libraries instead pair dimension with , which is equivalent up to a fixed permutation of dimensions but not interchangeable with weights trained the other way.
- Exercise 4Challenge
Implement FlashAttention's online-softmax idea for a single query: process the keys and values in blocks of 128, keeping only a running maximum, normalizer and output accumulator, and verify the result equals standard softmax attention. Explain the memory difference.
Show solution
import torch torch.manual_seed(0) n, d, block = 1000, 64, 128 q, K, V = torch.randn(d), torch.randn(n, d), torch.randn(n, d) scale = d ** -0.5 ref = torch.softmax(K @ q * scale, dim=0) @ V # materializes all n scores m = torch.tensor(float("-inf")); l = torch.tensor(0.0); o = torch.zeros(d) for start in range(0, n, block): s = K[start:start + block] @ q * scale # this block's scores only m_new = torch.maximum(m, s.max()) corr = torch.exp(m - m_new) # rescale old sums to new max p = torch.exp(s - m_new) l = corr * l + p.sum() o = corr * o + p @ V[start:start + block] m = m_new out = o / l print(torch.allclose(out, ref, atol=1e-5)) # TrueThe correction factor rescales everything accumulated so far whenever a larger score appears, so the final equals with the global maximum, which is exactly the softmax-weighted average. On the first block makes
corrzero, which is harmless because and start at zero.Memory: the reference keeps scores per query, for all queries. The blocked version keeps one block of scores plus accumulators per query. On a GPU, FlashAttention does this for tiles of queries in SRAM, so the matrix never touches HBM.
Further reading
- paperVaswani et al., Attention Is All You Need (2017)
- paperSennrich, Haddow & Birch, Neural Machine Translation of Rare Words with Subword Units (2015)
- paperSu et al., RoFormer: Enhanced Transformer with Rotary Position Embedding (2021)
- paperDao et al., FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness (2022)
- paperAinslie et al., GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints (2023)
- bookJurafsky & Martin, Speech and Language Processing (3rd ed. draft), chapters on transformers and LLMs
Go further
Sources
Fact-checked and last reviewed 2026-11-05