Search Tech Journey

Find topics, journeys and posts

back to blog
mlintermediate 120m read

DL S039 · The Full Transformer Block — Encoder, Decoder, and What Order Everything Goes In

Attention + FFN + LayerNorm + residual. Wire them in the right order and you get a transformer block. Stack N of them and you get a transformer. Walk both the encoder-decoder original and the modern decoder-only variant end to end.

🧠SoftwareM07 · Transformers from scratch· Session 039 of 130 120 min

🎯 Assemble a complete transformer block from attention + FFN + LayerNorm + residual, understand pre-LN vs post-LN, and walk the full encoder-decoder model end to end.

Series: Deep Learning & LLMs From Scratch — 80 sessions · Session 39 / 80 · Module M07 · ~2 hours

The story

We have all the ingredients. Multi-head self-attention (S037). Positional encodings (S038). The FFN (a two-layer MLP, S013). LayerNorm (S016). Residual connections (S017). What's left is the recipe: which order do they go in?

The recipe matters more than beginners expect. "Attention Is All You Need" (2017) used post-LN: x + Sublayer(x) first, then LayerNorm. It works, but training is fragile — you need warmup, careful init, and it explodes at deeper depths (past ~12 layers) without special tricks. Every modern transformer (GPT-2, Llama, everything) uses pre-LN: x + Sublayer(LayerNorm(x)). Pre-LN trains stably at 100+ layers, needs less warmup, and is what you'll implement in S040.

This session we build the block twice — once as the paper wrote it (encoder + decoder), once as the modern decoder-only variant used by GPT/Llama. You'll see exactly what changed, why cross-attention exists only in the encoder-decoder version, and why decoder-only won for language modelling. By the end, you can draw the full "Attention Is All You Need" figure on a whiteboard and defend every line.

You will be able to
  • Draw a transformer encoder block and decoder block with every arrow labelled.
  • Explain the difference between pre-LN and post-LN and why modern models use pre-LN.
  • Justify the FFN's 4×d_model hidden dimension in one sentence.
  • Explain the causal mask and produce it in a single line of PyTorch.
  • Walk cross-attention in the decoder: where Q comes from, where K/V come from, and what the shapes are.
  • Implement a full decoder-only transformer block (~50 lines) that matches nanoGPT's block.

Prerequisites

  • S036–S038 — attention, multi-head, positional.
  • S013 — MLP forward.
  • S016 — LayerNorm.
  • S017 — residuals.


1 · The four ingredients

Every transformer block is built from these four:

  1. Multi-head self-attention (MHA) — S037. Mixes information across positions.
  2. Feed-forward network (FFN) — a two-layer MLP applied position-wise (same weights every position).
  3. LayerNorm (LN) — normalises each token's features to unit mean/variance.
  4. Residual connection (skip) — adds the input of a sublayer to its output.
A meeting room with two work phases
🌍 Real world
💻 Code world

The FFN is the compute workhorse. For d_model = 512, it's typically:

FFN(x) = Linear(d_ff, d_model)(GELU(Linear(d_model, d_ff)(x)))

with d_ff = 4 · d_model = 2048. That 4× expansion is universal — even Llama and GPT-4-ish models keep the 4× ratio (SwiGLU variants use ~2.67× · 2 = 5.33× in raw dims to hit the same effective compute, but conceptually still 4×).

Why 4×? The FFN carries ~2/3 of the parameters of a transformer block (attention is only ~1/3). Making it wider gives you more capacity per layer. Empirically, is the sweet spot: bigger doesn't help much, smaller hurts.


2 · Post-LN — the original recipe (and why it's fragile)

The 2017 paper's block:

attn_out = MultiHeadAttention(x)
x = LayerNorm(x + attn_out)          # post-LN: add first, then norm
 
ffn_out = FFN(x)
x = LayerNorm(x + ffn_out)

In code:

class PostLNBlock(nn.Module):
    def __init__(self, d_model, h, d_ff):
        super().__init__()
        self.attn = MultiHeadAttention(d_model, h)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff), nn.GELU(),
            nn.Linear(d_ff, d_model))
        self.ln1 = nn.LayerNorm(d_model)
        self.ln2 = nn.LayerNorm(d_model)
 
    def forward(self, x, mask=None):
        x = self.ln1(x + self.attn(x, mask))
        x = self.ln2(x + self.ffn(x))
        return x

Works fine for the paper's 6-layer encoder + 6-layer decoder. But scale to 24 layers and you'll find gradients at the input layer are wildly larger than at the output layer, training loss oscillates in early epochs, and the model needs a linear-warmup learning rate schedule to survive the first few thousand steps.


3 · Pre-LN — the modern recipe

Swap the order: normalise inside the residual, add outside:

x = x + MultiHeadAttention(LayerNorm(x))
x = x + FFN(LayerNorm(x))
class PreLNBlock(nn.Module):
    def __init__(self, d_model, h, d_ff):
        super().__init__()
        self.attn = MultiHeadAttention(d_model, h)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff), nn.GELU(),
            nn.Linear(d_ff, d_model))
        self.ln1 = nn.LayerNorm(d_model)
        self.ln2 = nn.LayerNorm(d_model)
 
    def forward(self, x, mask=None):
        x = x + self.attn(self.ln1(x), mask)
        x = x + self.ffn(self.ln2(x))
        return x

Two lines swapped. Consequences: gradients through the residual go straight through (identity path), the sublayer's LN keeps sublayer inputs well-scaled, and depth scales to 100+ layers without divergence.

Try itVerify a transformer block preserves shape and its residual path is intact

Instantiate the PreLNBlock above and run:

block = PreLNBlock(d_model=64, h=4, d_ff=256)
x = torch.randn(2, 10, 64)                    # (B, T, d_model)
y = block(x)
assert y.shape == x.shape
 
# Kill the FFN and attention entirely
with torch.no_grad():
    for p in block.attn.parameters(): p.zero_()
    for p in block.ffn.parameters():  p.zero_()
y_zero = block(x)
print((y_zero - x).abs().max().item())        # expect ~0 (float noise only)

The second block confirms the residual actually works: when sublayers contribute nothing, the block is the identity. Now do the opposite — keep FFN and attention random but re-init both LayerNorm.weight = 0 and see what happens. This is the litmus test every transformer implementation should pass before you trust it on real data.

💡 Hint · Assert `output.shape == input.shape` and that setting FFN weights to zero yields output == input.

4 · The causal mask (decoder-only or decoder side of enc-dec)

Language modelling is next-token prediction. When training on "the cat sat on the mat", the model must NOT be allowed to peek at "cat" when predicting the next token after "the". Every position i can only attend to positions 0…i.

Enforce this by adding -∞ to attention scores at "forbidden" positions before soft-max. After soft-max, those positions get weight zero.

# T = sequence length
mask = torch.tril(torch.ones(T, T))       # lower-triangular of 1s
# then inside attention:
scores = scores.masked_fill(mask == 0, float('-inf'))

The tril matrix looks like:

[[1, 0, 0, 0],
 [1, 1, 0, 0],
 [1, 1, 1, 0],
 [1, 1, 1, 1]]

Row i has 1s in columns 0…i and 0s beyond. masked_fill(mask == 0, -inf) blocks the future. After soft-max, row 0 attends only to position 0, row 1 splits between 0 and 1, etc.


5 · The encoder-decoder — original transformer

The 2017 paper is an encoder-decoder for machine translation. Left tower processes the source sentence; right tower generates the target token by token, attending to both its own previous tokens (self-attention) and the encoder's outputs (cross-attention).

Encoder block: [LN → MHA(self) → +] → [LN → FFN → +]. Bidirectional attention over source.

Decoder block: three sublayers:

  1. [LN → MHA(self, causal)] — attend to previous target tokens.
  2. [LN → MHA(cross, Q=decoder, K=V=encoder_out)] — pull from source.
  3. [LN → FFN] — position-wise.

Cross-attention is the same math as self-attention but with Q from the decoder and K, V from the encoder. Shapes:

Q: (B, T_dec, d_model)  from LN(x_dec)
K, V: (B, T_enc, d_model)  from encoder_output (already normed)
scores: (B, h, T_dec, T_enc)

The cross-attention mask blocks padding tokens in the source but has no causal structure.


6 · Decoder-only — the modern LM

For pure language modelling (predicting the next token given the past), you don't need an encoder or cross-attention. Just stack N pre-LN decoder blocks with causal self-attention, plus a token+position embedding at the input and a linear head at the output.

class DecoderOnlyBlock(nn.Module):
    def __init__(self, d_model, h, d_ff, dropout=0.0):
        super().__init__()
        self.ln1 = nn.LayerNorm(d_model)
        self.attn = MultiHeadAttention(d_model, h, dropout)
        self.ln2 = nn.LayerNorm(d_model)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff), nn.GELU(),
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout))
 
    def forward(self, x, mask):
        x = x + self.attn(self.ln1(x), mask)
        x = x + self.ffn(self.ln2(x))
        return x

And the full model:

class GPTLike(nn.Module):
    def __init__(self, vocab_size, max_len, d_model, h, d_ff, n_layers):
        super().__init__()
        self.tok_emb = nn.Embedding(vocab_size, d_model)
        self.pos_emb = nn.Embedding(max_len, d_model)         # learned PE
        self.blocks = nn.ModuleList([
            DecoderOnlyBlock(d_model, h, d_ff) for _ in range(n_layers)])
        self.ln_f = nn.LayerNorm(d_model)                     # final norm
        self.head = nn.Linear(d_model, vocab_size, bias=False)
        # tie weights (optional, saves params and improves quality)
        self.head.weight = self.tok_emb.weight
 
    def forward(self, idx):
        B, T = idx.shape
        pos = torch.arange(T, device=idx.device)
        x = self.tok_emb(idx) + self.pos_emb(pos)
        mask = torch.tril(torch.ones(T, T, device=idx.device))
        for blk in self.blocks:
            x = blk(x, mask)
        x = self.ln_f(x)
        return self.head(x)                                   # (B, T, vocab)

Roughly 50 lines. That is a GPT. We build the full training loop for this in S040.


7 · Why decoder-only won

Decoder-only advantages over encoder-decoder
  • Simpler: half as many block types, no cross-attention, no separate encoder pipeline.
  • Unified pretraining objective: next-token prediction works for ANY text (paper, code, chat, docs). Encoder-decoder needs paired source-target for its original task.
  • In-context learning: prepending 'examples' as prefix tokens is free (they just become part of the context). Encoder-decoder splits input and output into separate paths, less flexible.
  • Scaling behaves cleanly: same architecture from 100M to 1T params. Encoder-decoder needs delicate rebalancing of encoder vs decoder depths.

For dedicated seq2seq tasks (machine translation, summarisation with strict input-output separation), encoder-decoder still wins on FLOPs-per-BLEU. But for general-purpose LMs and chatbots, decoder-only rules.


8 · Common bugs

War story Mask not moved to the right device

torch.tril(torch.ones(T, T)) lives on CPU. If your input is on CUDA and the mask isn't, you get a device-mismatch error. Always create the mask on idx.device (as in the code above) or pre-register it as a buffer with self.register_buffer('mask', torch.tril(torch.ones(max_len, max_len))) and slice [:T, :T] at forward time.

War story Applying LN AFTER the residual add in pre-LN

Very easy to accidentally write x = self.ln1(x + self.attn(x)) — that's post-LN. Pre-LN is x = x + self.attn(self.ln1(x)). The difference is one nesting level; the impact on stability at depth is huge.

War story Forgetting the FINAL LN before the head

self.ln_f after the last block, before the output projection. Without it the head's inputs have wandering scale and the output logits are noisy. Every reference implementation has this final LN; every "why is my transformer not training" post is missing it.


9 · Parameter budget worksheet

For d_model = 512, h = 8, d_ff = 2048, n_layers = 12, vocab = 50257, max_len = 1024:

  • Token embed: 50257 · 512 = 25.7M
  • Position embed: 1024 · 512 = 0.5M
  • Per block: 4 · d_model² (attn) + 2 · d_model · d_ff (FFN) = 4·262144 + 2·512·2048 = 1.05M + 2.10M = 3.15M. Times 12 = 37.8M.
  • Head (tied with token embed) = 0.
  • Final LN etc. = negligible.
  • Total ≈ 64M.

That's about the size of GPT-2 small (117M — differs because GPT-2 uses d_ff = 4·d_model = 3072, n_layers = 12, d_model = 768; let's redo: 12 · (4·768² + 2·768·3072) = 12 · (2.36M + 4.72M) = 85M, plus 50257·768 = 38.6M embed = ~124M, matches "117M" once you account for weight tying reducing the effective count).

Do this arithmetic once. You'll never be intimidated by "how big is this model" again.


10 · Modern-2025 twist — the block that actually ships

The pre-LN block above is the GPT-2 (2019) shape. What every 2024–2026 frontier model actually ships is a small set of drop-in upgrades. Here they are, roughly in order of impact:

1. RMSNorm instead of LayerNorm (Zhang & Sennrich, 2019, arXiv:1910.07467). Drops the mean-subtraction and the bias term; just rescales by root-mean-square. Same-ish quality, ~7–15% faster, one less numerical failure mode. Used in Llama 1/2/3/4, Mistral, Qwen, DeepSeek, Gemma.

class RMSNorm(nn.Module):
    def __init__(self, d, eps=1e-6):
        super().__init__(); self.g = nn.Parameter(torch.ones(d)); self.eps = eps
    def forward(self, x):
        return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.g

2. SwiGLU instead of GELU-FFN (Shazeer, 2020, arXiv:2002.05202). Replace the 2-layer MLP with a gated variant. Consistently ~1–2% lower loss at fixed compute. Universal in modern LMs.

class SwiGLU(nn.Module):
    def __init__(self, d, d_ff):
        super().__init__()
        self.w1 = nn.Linear(d, d_ff, bias=False)   # gate
        self.w2 = nn.Linear(d, d_ff, bias=False)   # value
        self.w3 = nn.Linear(d_ff, d, bias=False)   # out
    def forward(self, x):
        return self.w3(F.silu(self.w1(x)) * self.w2(x))

Because SwiGLU has 3 matrices instead of 2, Llama shrinks d_ff from 4d to ~2.67d so the parameter count matches a 4d GELU FFN.

3. No biases anywhere (Llama). Empirically neutral quality but simpler, slightly faster. All nn.Linear(..., bias=False).

4. RoPE instead of learned/sinusoidal positions — covered in S038 and S043. Baked into attention, not added to embeddings.

5. GQA / MQA / MLA — shrinks KV heads. Covered in S037/S042.

The "modern block" — all five upgrades — is essentially the Llama 3 block. Eight-line class:

class ModernBlock(nn.Module):
    def __init__(self, d, n_head, n_kv_head, d_ff):
        super().__init__()
        self.n1 = RMSNorm(d); self.attn = GQAAttention(d, n_head, n_kv_head)
        self.n2 = RMSNorm(d); self.ffn  = SwiGLU(d, d_ff)
    def forward(self, x, cos, sin, kv_cache=None):
        h, kv = self.attn(self.n1(x), cos, sin, kv_cache)
        x = x + h; x = x + self.ffn(self.n2(x)); return x, kv

That's the block behind every open weights release from mid-2023 on. Read Llama 3's model.py; this is essentially what you'll see.

2025 experimental additions worth knowing about:

  • DeepNorm (post-LN variant, Wang et al., 2022, arXiv:2203.00555) — scale residual by α to stabilise 1000-layer training. Used in some Chinese-lab models.
  • Value residual (modded-nanoGPT, 2024) — add a residual on V inside attention.
  • QK-norm (Chameleon, Grok) — RMSNorm on Q and K separately before the dot product. Kills attention-logit explosions at scale.
  • Tanh logit soft-capping (Gemma 2, Gemma 2 tech report, 2024) — logits = cap * tanh(logits / cap). Prevents softmax saturation.
  • U-net-shaped residual streams (modded-nanoGPT speedrun 2025) — experimental, decoupled residual streams reduce loss further.

Further reading:


Common misconception
✗ What most people think

"Pre-LN versus post-LN is a minor implementation detail. Layer norm normalises the activations either way — moving it inside the residual branch is a small refactor that might help stability a little."

✓ What is actually true

The placement decides whether an identity path exists from the loss to the input at all. In post-LN, every residual add is immediately followed by a normalisation, so the "skip" is not a skip: the gradient passes through L normalisation Jacobians on its way back, and their product shrinks with depth. In pre-LN, the normalisation sits inside each branch, so the residual stream itself is an unbroken sum from input to output and the gradient has a path with no multiplicative factors on it whatsoever. That is the difference between a network that needs a carefully-tuned warmup schedule to train past a certain depth and one that trains without warmup at all. It is not a refactor; it changes the architecture's gradient topology.

Why the myth is so sticky

Because the belief is essentially correct at shallow depth, where the failure it predicts is small enough to be invisible. At six layers a post-LN transformer trains fine, and the original architecture was post-LN precisely because it worked. Both forms also produce the same forward-pass shapes, the same parameter count, and outputs in the same range, so nothing about inspecting the model reveals a difference. It only bites when you stack deep or remove warmup — and when it bites, the symptom is a loss that diverges in the first few hundred steps, which looks like a learning-rate problem and gets treated as one. The fix that "works" is lowering the learning rate, which confirms the wrong diagnosis and hides the structural cause for another few months.

Prove it to yourself

Measure the gradient norm reaching the first block, as a function of depth, under both placements:

import torch

def grad_at_input(depth, pre_ln):
    x = torch.randn(2, 16, 64, requires_grad=True)
    h = x
    for _ in range(depth):
        if pre_ln:
            h = h + branch(norm(h))     # residual stream untouched
        else:
            h = norm(h + branch(h))     # every add passes through norm
    h.sum().backward()
    return x.grad.norm().item()

for depth in (2, 6, 12, 24, 48):
    print(depth, grad_at_input(depth, True), grad_at_input(depth, False))
# pre-LN column: roughly stable as depth grows
# post-LN column: falls away with depth -- and that decay is
# exactly what the warmup schedule was compensating for
From first principles
Start with the question

Why is the feed-forward hidden dimension four times d_model? The factor 4 shows up in nearly every transformer ever published — it looks like a number someone picked once and everyone copied.

  1. 1
    Attention is a mixing operation: each output is a weighted average of value vectors. Between the projections it performs no per-position nonlinear computation at all.
    forced by · the softmax acts on scores, not on the values being combined, so the value path through attention is entirely linear
  2. 2
    A stack of purely linear mixing collapses: a composition of linear maps is a linear map. Something in the block must supply per-position nonlinearity or depth buys nothing beyond a single layer's expressivity.
    forced by · without a nonlinearity between them, N layers are representationally equivalent to one
  3. 3
    The feed-forward network is therefore where all the per-token computation lives. Its job is to take a mixed representation and apply a learned nonlinear transformation — in effect a key-value memory, where the first matrix matches against learned patterns and the second writes out associated content.
    forced by · it is the only sublayer that operates on each position independently with a nonlinearity in the middle
  4. 4
    The number of distinct patterns such a layer can match is bounded by its hidden width. Making the hidden layer narrower than d_model would force a bottleneck, discarding information before the nonlinearity ever sees it; making it equal to d_model gives no room to expand into a sparse, more separable representation.
    forced by · a nonlinearity applied in a compressed space cannot recover distinctions the compression already destroyed
  5. 5
    So the hidden width must exceed d_model, and the multiplier is set by a parameter budget: the feed-forward block holds 2 * m * d_model^2 parameters against attention's 4 * d_model^2. At m = 4 the ratio is 2:1, which is where the transformer places two-thirds of its non-embedding parameters.
    forced by · every extra unit of m costs parameters quadratically in d_model, so the multiplier is a budget allocation between mixing and computing
⇒ Therefore

Therefore 4 is not sacred — it is the point where the field settled the split between mixing capacity and per-token compute capacity, and it survives because that split turned out to be a broad optimum rather than a sharp one.

The derivation makes a sharp prediction you can check against real architectures: since the constraint is on the parameter count of the feed-forward block and not on the literal number 4, any change that adds a third matrix should reduce the multiplier to compensate. Gated variants such as SwiGLU use three matrices instead of two, so holding parameters fixed requires the multiplier to drop by roughly a third — and that is exactly why you find models using a factor near 8/3 rather than 4. Go read the config of any recent open-weight model and check the intermediate size against d_model; the number will not be 4, and now you know why.

Mental modelThe residual stream as a shared bus

Do not picture the block as transforming its input into an output. Picture a wide bus running vertically through the entire network, carrying d_model lanes from input embedding to final logits. Each sublayer reads the bus, computes something, and adds its result back. Nobody overwrites; everyone contributes.

Attention reads across positions and writes back what it gathered. The feed-forward network reads one position and writes back what it computed. Layer norm in the pre-LN arrangement is a lens on the read side only — it standardises what a sublayer sees without ever disturbing the bus itself. That is why the bus is an uninterrupted additive path, and why gradient flows down it unattenuated.

  • Block = attention sublayer + feed-forward sublayer, each wrapped as x = x + sublayer(norm(x)). Attention mixes across positions; the feed-forward mixes across features. Nothing else happens.
  • Pre-LN keeps the residual stream clean and trains without warmup, at the cost of a growing stream variance that needs a final norm before the output head. Post-LN normalises the stream and needs warmup.
  • The causal mask is applied to the scores before the softmax, using a large negative value rather than zero, because zeroing a probability after the softmax breaks normalisation.
  • Roughly two-thirds of non-embedding parameters sit in the feed-forward blocks. When you are counting memory or thinking about where knowledge is stored, that is where to look first — not in attention.
🔔 Fires when you see

Fire this model the moment you see: a loss that diverges in the first few hundred steps and is "fixed" by warmup · a debate about where to put layer norm · a -inf or large negative constant in an attention mask · someone asking where a model stores facts · an intermediate size that is not four times the hidden size · residual connections being described as "helping gradient flow" without saying why.

The tradeoff

You are choosing the normalisation for a new model. LayerNorm or RMSNorm, and where does it go?

Post-LN LayerNorm
+ you gain keeps the residual stream at a controlled scale at every depth, so activations never grow through the network; this is the original published architecture and matches the reference implementations exactly
− you pay no unattenuated gradient path, so deep stacks need learning-rate warmup and careful initialisation to train at all; sensitivity grows with depth, making it the harder configuration to scale
pick when you are reproducing a specific published result, or the model is shallow enough (order ten blocks) that the gradient attenuation is not yet material
Pre-LN LayerNorm
+ you gain an unbroken additive path from loss to input, so it trains without warmup and stays stable as you add depth; this robustness is what made very deep transformers routine rather than delicate
− you pay the residual stream variance grows with depth since every block adds to it unchecked, so a final normalisation before the output head is mandatory; and the deepest blocks contribute relatively less, since their output is added to an already-large stream
pick when the model is deeper than roughly a dozen blocks, or you want to train without babysitting a warmup schedule — which covers essentially all new work
Pre-norm RMSNorm
+ you gain drops the mean-subtraction and the bias term, keeping only the scaling by root-mean-square — fewer operations, fewer parameters, and one less reduction pass over the feature axis, which matters because normalisation is memory-bandwidth bound rather than compute bound
− you pay no re-centring, so it relies on the representation not developing a large mean offset; and it is a deviation from the reference implementation, so any numerical comparison against a LayerNorm baseline will not match bit-for-bit
pick when you are training from scratch and inference throughput matters — the reason it is the default in current open-weight model families
What a senior engineer actually does

Pre-norm with RMSNorm is the current default and it is a defensible starting point for anything new. But hold the reasoning, not the recipe: the reason pre-norm won is the unbroken residual path, and the reason RMSNorm won is that normalisation is bandwidth-bound so removing a pass over the features is nearly free performance.

The one thing to keep from the post-LN era is the diagnostic. If a deep model diverges early in training and warmup rescues it, do not conclude the learning rate was too high — check whether something in your block broke the identity path. A normalisation, a scaling, or a nonlinearity accidentally placed on the residual stream rather than inside the branch reproduces the post-LN failure exactly, and the warmup will keep hiding it from you.


Retention scaffold

Recall questions

1. Pre-LN vs post-LN — write both.

Post-LN: x = LN(x + Sublayer(x)). Pre-LN: x = x + Sublayer(LN(x)). Pre-LN is strictly better for deep models.

2. What is the FFN's hidden dimension typically?

d_ff = 4 · d_model. Two linear layers with a GELU (or SwiGLU in modern models) in between. Applied position-wise (same weights every position).

3. Write the one-line causal mask.

mask = torch.tril(torch.ones(T, T)) — lower triangular of 1s. Fill non-mask positions in the scores with -inf before soft-max.

4. What are the three sublayers of a decoder block in the encoder-decoder transformer?

(1) causal self-attention, (2) cross-attention (Q from decoder, K/V from encoder), (3) FFN. Each wrapped in LN + residual.

5. Why does the decoder-only architecture "win" for LMs?

Simpler (one block type), scales cleanly to any parameter count, and the next-token-prediction objective works on any text corpus with no paired-data requirement. Also enables in-context learning naturally.

Stretch prompt

Implement the encoder-decoder transformer (six blocks each side) and train it on a small English→German dataset (like Multi30k). Compare BLEU to a decoder-only trained on the same data as a concatenated EN <sep> DE sequence. Decoder-only will be within a couple BLEU points despite being architecturally simpler — that's the modern LM story in miniature.

Quick recall · click to reveal
★ = stretch question

In your own words

"Explain to a friend, in 4–5 sentences, what makes a transformer 'a transformer' as opposed to just 'a stack of attention layers'."

Spaced review

  • S016 — LayerNorm, the piece that makes deep transformers train.
  • S017 — residuals, without which nothing above 4 layers would learn.
  • S037 — MHA, the workhorse.

Next-session teaser

S040: Karpathy's nanoGPT, line by line. We take the ~300 lines of model.py and understand every symbol. If you followed today's session, tomorrow you'll be typing it from memory.

Bring back tomorrow

  • Pre-LN order (LN inside the residual).
  • FFN = 4× hidden, GELU or SwiGLU, position-wise.
  • Causal mask via tril.
  • Decoder-only is the default modern LM architecture.

Previous: ← DL S038 · Next: DL S040 →