Deep Learning · Module 18 of 26 · 8 concepts · about 19 min

Transformers

The residual, normalized attention-and-MLP blocks behind modern language and vision models.

At work: The architecture behind ChatGPT-style assistants, modern search, code assistants and many vision models.

Transformer blocks, residual connections and positional information

What it is

A Transformer stacks blocks containing multi-head self-attention and a position-wise MLP, each wrapped in residual connections and normalization. Attention lets tokens exchange information; the MLP then transforms each token independently using its newly contextualised representation. Residual additions preserve the incoming representation beside the block's new contribution, while normalization keeps layer scales manageable. Positional embeddings or encodings supply order because attention alone is permutation-invariant; an unpositioned model sees the same set of token vectors regardless of order.

Why it matters

Residual paths preserve an easy route for signals and gradients through many layers; position is what lets the model distinguish 'dog bites man' from the reverse. This repeated block structure also makes Transformer configuration legible: depth sets how often contextualisation is refined, width sets representation capacity, head count divides attention into multiple subspaces, and context length sets the available evidence and computational bill.

In the real world: The repeating unit behind ChatGPT-style assistants, modern search engines and code assistants.

Visual example

Deep learning

Residual paths wrap the two transformations repeated at every depth.

Transformer block

  1. token embeddings + positionone vector per token
  2. LayerNorm → multi-head attentiontokens exchange context
  3. residual addpreserves the incoming signal
  4. LayerNorm → MLP → residual addeach token transforms independently

A full Transformer is this block repeated many times.

import torch
import torch.nn as nn

layer = nn.TransformerEncoderLayer(
    d_model=512, nhead=8, dim_feedforward=2048,
    batch_first=True, norm_first=True,        # pre-norm
)
encoder = nn.TransformerEncoder(layer, num_layers=6)
encoder(torch.randn(4, 20, 512)).shape        # (4, 20, 512)

Assuming multi-head attention means multiple independent models. Heads are smaller learned projections trained jointly, and their usefulness depends on the whole block around them.

The block, part by part

What it is

Written out, one pre-norm block is four lines and every line came from an earlier module. h = x + Attn(LN(x)) mixes information *across* positions — module 17's self-attention, wrapped in module 10's residual and module 11's LayerNorm. y = h + MLP(LN(h)) then transforms *each* position — module 12's MLP, wrapped identically. The two sublayers alternate roles deliberately: attention is the only operation in the whole architecture that moves information between positions, and the MLP is the only one that gives each position substantial nonlinear processing of its own. Everything a Transformer computes is those two steps repeated.

Why it matters

Seeing the block as an assembly rather than a monolith is what makes it modifiable and debuggable. Almost every variant in the literature is a substitution into one of these slots — a different attention (sparse, sliding-window, linear), a different feedforward (gated, mixture-of-experts), a different norm (RMSNorm), a different position scheme — and the surrounding structure is unchanged. It also makes the shapes checkable: the block is shape-preserving, so (batch, n, d_model) goes in and comes out.

In the real world: Knowing the parts helps a team choose a model size that fits its latency and cost budget.

Visual example

Two sublayers

Attention is the only part that moves information between positions; the MLP is the only part that processes a position deeply.

one pre-norm block, d_model = 512
stepshapewhat it does
1LN → MultiheadAttention(B, n, 512)mixes across positions
2x + attention out(B, n, 512)residual: module 10
3LN → Linear 2048 → GELU → Linear 512(B, n, 512)processes each position
4x + mlp out(B, n, 512)residual: module 10

Common misconception

Wrong idea: a transformer block is a new kind of layer.

Correct idea: it is self-attention (module 17) and an MLP (module 12), each wrapped in a residual connection (module 10) and a layer norm (module 11).

import torch.nn as nn

class Block(nn.Module):
    def __init__(self, d_model, n_heads, mult=4):
        super().__init__()
        self.ln1 = nn.LayerNorm(d_model)
        self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True)
        self.ln2 = nn.LayerNorm(d_model)
        self.mlp = nn.Sequential(
            nn.Linear(d_model, mult * d_model), nn.GELU(),
            nn.Linear(mult * d_model, d_model),
        )

    def forward(self, x, mask=None):
        h = self.ln1(x)
        h, _ = self.attn(h, h, h, attn_mask=mask, need_weights=False)
        x = x + h                       # across positions
        return x + self.mlp(self.ln2(x))  # within each position

Reusing one nn.LayerNorm instance for both sublayers. It has learned γ and β parameters, so sharing it ties two different normalizations together — it runs without error and quietly constrains the model.

The feedforward sublayer

What it is

The MLP inside a block is a plain two-layer network — expand d_model to typically 4 × d_model, apply a nonlinearity, project back — applied identically and independently to every position, with the same weights at every position. It exists because attention, on its own, only takes weighted averages: it can gather the right information from elsewhere in the sequence but performs little nonlinear computation on it. The MLP is where that gathered representation is actually transformed. Its 4× expansion means it holds roughly two thirds of a transformer's parameters, which is why mixture-of-experts architectures target this sublayer specifically when scaling parameter count without scaling compute per token.

Why it matters

It answers a question the attention-centred presentation leaves open — what the rest of a Transformer is for. The division of labour is clean and worth keeping: attention routes, the MLP computes. It also explains a shape fact that surprises people, which is that the widest tensors in a Transformer are inside the feedforward sublayer rather than in attention.

In the real world: Holds much of the model's knowledge, and much of its compute cost.

Visual example

Where the parameters are

The feedforward sublayer's 4× expansion holds about two thirds of a block's parameters.

one block at d_model = 512, 8 heads
componentparametersshare
attention (Q, K, V, O)1,050,62433%
feedforward (512↔2048)2,101,76066%
2 × LayerNorm2,048<1%
block total3,154,432100%
import torch.nn as nn

d_model = 512
mlp = nn.Sequential(
    nn.Linear(d_model, 4 * d_model),   # 512 -> 2048
    nn.GELU(),
    nn.Linear(4 * d_model, d_model),   # 2048 -> 512
)
sum(p.numel() for p in mlp.parameters())        # 2,101,760

attn = nn.MultiheadAttention(d_model, 8)
sum(p.numel() for p in attn.parameters())       # 1,050,624 — half as many
# the same weights are applied at every position, hence 'position-wise'

Reading 'position-wise' as 'a separate MLP per position'. It is one MLP, applied to each position's vector in turn — which is exactly what nn.Linear does to the last axis of a (B, n, d) tensor, and why the parameter count does not depend on the sequence length.

Encoder-only, decoder-only, encoder-decoder

What it is

Three families, differing only in which sequences exist and which masks apply. An encoder-only model (BERT-style) runs bidirectional self-attention over one sequence with no causal mask, so every token sees the whole input; it is trained with a masked-token objective and suits classification, retrieval, tagging and embedding. A decoder-only model (GPT-style) runs causal self-attention over one sequence, predicts the next token at every position, and generates autoregressively. An encoder-decoder (T5-style, and the original 2017 architecture) encodes a source bidirectionally and decodes causally, with cross-attention from the decoder into the encoded source — the natural shape for translation and summarisation, where input and output are distinct sequences of different lengths.

Why it matters

The choice follows from the task's information structure, not from fashion: if the whole input is available and you need a representation, an encoder is right and a causal mask would only hide legitimate context; if you must produce tokens one at a time, a decoder is required. Knowing the families also makes checkpoints legible — a model's name usually tells you which one it is, and therefore what it can and cannot be asked to do.

In the real world: Encoders for search and classification, decoders for chatbots, encoder-decoders for translation and summarisation.

Visual example

Three families

Same block. What differs is how many sequences there are and which masks apply.

picking a family from the task
familyattentionbest-suited task
encoder-only (BERT)bidirectional selfclassification, retrieval, tagging
decoder-only (GPT)causal selfgeneration, chat, completion
encoder-decoder (T5)bidirectional + causal + crosstranslation, summarisation
import torch
import torch.nn as nn

enc_layer = nn.TransformerEncoderLayer(512, 8, batch_first=True, norm_first=True)
encoder = nn.TransformerEncoder(enc_layer, 6)          # no causal mask

dec_layer = nn.TransformerDecoderLayer(512, 8, batch_first=True, norm_first=True)
decoder = nn.TransformerDecoder(dec_layer, 6)         # self + cross attention

n = 20
causal = nn.Transformer.generate_square_subsequent_mask(n)
encoder(torch.randn(4, n, 512), mask=causal).shape    # decoder-only, in effect

Fine-tuning a decoder-only model for sentence classification and reading the *first* token's representation. Under a causal mask, position 0 has seen only itself; the summary of the whole sequence lives at the last position. Encoder models are the opposite — that is what BERT's [CLS] at position 0 is for.

Why Transformers scale

What it is

Three properties compound. Every sublayer is parallel over the sequence (module 17), so a training step's wall-clock time is set by available compute rather than by sequence length — unlike an RNN's n dependent steps (module 15). The residual stream (module 10) makes very deep stacks trainable, so depth is a usable axis rather than a barrier. And the architecture is uniform: the same block repeated, with no per-layer design work, so scaling is a change to three integers — layers, heads, width. Together these make the relationship between compute, data and loss smooth and predictable enough to extrapolate, which is what scaling-law work depends on.

Why it matters

It explains the historical outcome rather than asserting it. Recurrence was not displaced because attention models language better in principle, but because a fixed hardware budget buys far more training on a parallel architecture, and the models that could consume enormous corpora within a reasonable wall-clock budget were the ones that got large enough to be surprising. It also sets the practical limit: the same argument does not apply to *inference*, which is still one token at a time.

In the real world: Why bigger models trained on more data keep getting better, and why AI budgets keep growing.

import torch.nn as nn

def gpt(n_layers, d_model, n_heads):
    block = nn.TransformerEncoderLayer(
        d_model, n_heads, dim_feedforward=4 * d_model,
        batch_first=True, norm_first=True,
    )
    return nn.TransformerEncoder(block, n_layers)

for layers, d_model, heads in [(6, 512, 8), (12, 768, 12), (24, 1024, 16)]:
    n = sum(p.numel() for p in gpt(layers, d_model, heads).parameters())
    print(f"{layers:2d}L d={d_model:4d} h={heads:2d}  {n / 1e6:6.1f}M")
# scaling is three integers, and nothing else about the code changes

Reading scaling laws as a promise that any model improves with size. They are empirical fits for a given architecture, data distribution and training recipe, and they say nothing about a model trained on a small or repeated corpus — where more parameters buy memorisation rather than capability.

Practise it: Scaled Dot-Product Attention

Parameter count by depth, width and heads

What it is

A block's parameters are dominated by two terms: attention's four d_model × d_model projections, 4d², and the feedforward's two d_model × 4d_model matrices, 8d². So a block is roughly 12 · d_model², and a model is n_layers × 12 · d_model² plus the embedding table, vocab × d_model. Two consequences are worth internalising. Width is quadratic and depth is linear, so doubling d_model costs four times as much as it appears to while doubling the layer count costs exactly double. And head count does not enter the formula at all: 8 heads of 64 and 16 heads of 32 have identical parameter counts, because the head dimension is d_model / n_heads.

Why it matters

It is how a configuration gets sanity-checked before a run. It answers 'will this fit in memory', it makes two models comparable ('deeper' and 'wider' are only meaningful next to a count), and it locates the surprises — that heads are free, that the embedding table dominates a small model with a large vocabulary, and that width is the expensive axis.

In the real world: Estimating the memory and cost of serving a model before choosing it for a product.

Visual example

≈ 12 · d_model² per block

Depth is linear, width is quadratic, and head count is free.

rough parameter counts, vocab 50k
layers × d_modelheadsparameters
12 × 76812124M
24 × 76812209M — 2× depth, ~1.7× params
12 × 153612379M — 2× width, ~3× params
12 × 76848124M — heads cost nothing

Per block at d_model = 768

attention Q, K, V, O: 4 · 768² = 2.36M

feedforward 768↔3072: 2 · 4 · 768² = 4.72M

total ≈ 12 · 768² = 7.08M per block

12 blocks = 85M, plus 39M of embeddings = 124M

def params(n_layers, d_model, vocab=50257):
    per_block = 12 * d_model ** 2 + 2 * 2 * d_model   # weights + 2 LayerNorms
    return n_layers * per_block + vocab * d_model

for layers, d in [(12, 768), (24, 1024), (24, 2048), (48, 1600)]:
    print(f"{layers:2d}L d={d:4d}  {params(layers, d) / 1e6:7.1f}M")
# 12L d= 768     123.6M   (GPT-2 small, near enough)
# 24L d=1024     353.0M
# 24L d=2048    1311.0M   <- 2x the width, ~4x the parameters
# 48L d=1600    1555.0M

Raising n_heads to add capacity. It adds none — the parameter count is unchanged, because each head simply gets a narrower slice of the same d_model. More heads means more distinct attention patterns, not a bigger model.

Stacking blocks without residuals

What it is

The concrete failure that ties this group back to module 10. Take a correct block, delete the two x + additions so it reads x = MLP(LN(Attn(LN(x)))), and stack it 24 times. Nothing errors and the shapes all agree. What happens is that the gradient to the early blocks now passes through 48 transformations with nothing to bypass them, module 10's product collapses, and the loss falls briefly — the last few blocks are still learning — and then flattens far above where the residual version lands. With post-norm and no warmup the same deletion can instead diverge outright. In either case the model is untrainable at depth, and the cause is two missing characters.

Why it matters

It is the most valuable thing to know about transformer depth: the blocks are not what makes a deep stack work, the residual stream is. Reading the architecture as 'a residual stream that blocks read from and write into' also explains a great deal of interpretability work, and makes it obvious why the block must be shape-preserving — a residual add requires its two operands to have the same shape.

In the real world: Shows why the shortcut connections are essential: without them deep models don't train.

Visual example

24 blocks, with and without

**The same 24-block model, differing only in two x + additions.** The residual version trains; the other plateaus.

with residual connectionsresiduals removed
05101520246810training step (thousands)training lossloss at initialization = ln(vocab)only the last few blocks ever learned

Why this matters

It shows depth is not free: a 24-block stack is only trainable because the gradient has a route that skips all 24.

The red run is not undertrained. The first twenty blocks receive a gradient too small to move them, so they stay at their initialization forever.

import torch.nn as nn

class BrokenBlock(nn.Module):
    """Correct shapes, no residual path. Untrainable past a few layers."""

    def __init__(self, d_model, n_heads):
        super().__init__()
        self.ln1, self.ln2 = nn.LayerNorm(d_model), nn.LayerNorm(d_model)
        self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True)
        self.mlp = nn.Sequential(
            nn.Linear(d_model, 4 * d_model), nn.GELU(),
            nn.Linear(4 * d_model, d_model),
        )

    def forward(self, x):
        h, _ = self.attn(self.ln1(x), self.ln1(x), self.ln1(x), need_weights=False)
        return self.mlp(self.ln2(h))       # the missing 'x +' is the whole bug

Refactoring a block into a tidy nn.Sequential. Sequential has no way to express x + f(x), so the residual paths disappear and the model still runs — which is why the block is written as an explicit forward in every real implementation.

Configuring and checking a Transformer

What it is

Six numbers describe one: n_layers, d_model, n_heads, the feedforward multiplier (nearly always 4), vocab_size, and max_seq_len. Four constraints tie them together. d_model must be divisible by n_heads. The head dimension d_model / n_heads is usually kept at 64-128, so heads and width move together. max_seq_len sets the quadratic attention cost and, for learned positional embeddings, a hard ceiling the model cannot exceed. And the block must be shape-preserving, which the residual additions require. Checking those four before a long run is minutes of work against hours of wasted compute.

Why it matters

Almost every 'why does this not work' in a hand-written Transformer is one of these four, and three of them fail loudly while the fourth — exceeding max_seq_len — can silently index a positional table out of its trained range. Being able to state a model as six numbers is also what makes two models comparable and a configuration reviewable.

In the real world: Setting up a model that fits the company's GPUs and response-time targets.

Visual example

Deep learning

Everything else about a Transformer's shape follows from these.

Six numbers, four checks

  1. n_layers × d_modelparameters ≈ n_layers · 12 · d_model²
  2. d_model % n_heads == 0head_dim = d_model / n_heads, kept near 64-128
  3. mult = 4feedforward width, and two thirds of the parameters
  4. vocab × d_modelthe embedding table, large in small models
  5. max_seq_lenquadratic attention cost, and a hard ceiling

Assert the divisibility and the output shape before starting a run, not after it.

import torch
import torch.nn as nn

cfg = dict(n_layers=12, d_model=768, n_heads=12, mult=4,
           vocab=50257, max_seq_len=1024)

assert cfg["d_model"] % cfg["n_heads"] == 0
head_dim = cfg["d_model"] // cfg["n_heads"]
assert 32 <= head_dim <= 256, f"head_dim {head_dim} is unusual"

model = build_gpt(**cfg)
x = torch.randint(0, cfg["vocab"], (2, cfg["max_seq_len"]))
assert model(x).shape == (2, cfg["max_seq_len"], cfg["vocab"])
print(f"{sum(p.numel() for p in model.parameters()) / 1e6:.1f}M parameters")

Feeding a sequence longer than max_seq_len. With learned positional embeddings this is an index error at best and, if the table was allocated larger than the model was trained on, a silent pass through positions that received no gradient during training — producing degraded output with no error at all.