Search Tech Journey

Find topics, journeys and posts

6-month learning plan111 / 130
back to blog
llmadvanced 55m read

S111 · Full Transformer Architecture — Encoder + Decoder

Everything you've learned, snapped together. Attention + FFN + residual + layernorm × N — the block that powers every LLM, and why encoder-only vs decoder-only vs encoder-decoder each earn their place.

LLMsM13 · NLP & Transformers· Session 111 of 130 90 min

🎯 Assemble a complete Transformer block from scratch, understand why every component (residual, layernorm, FFN) is non-negotiable, and know when to reach for encoder-only, decoder-only, or encoder-decoder.

Why this session exists

You've now learned every component: tokenization (S106), attention (S107-108), multi-head (S109), positional encoding (S110). This session snaps them together into a complete Transformer block — the atomic unit repeated N times to make BERT, GPT, T5, LLaMA, Claude, and every other modern LLM. You'll also learn the three architectural families (encoder-only, decoder-only, encoder-decoder) and why each survives — because they solve different problems.

You will be able to
  • Draw a Transformer block from memory — attention, residual, layernorm, FFN, residual, layernorm.
  • Explain why every one of those pieces (not just attention) is essential.
  • Distinguish encoder-only (BERT), decoder-only (GPT), and encoder-decoder (T5) and pick the right one.
  • Build a tiny working GPT-style model in PyTorch and generate text.
  • Read any Transformer paper's architecture diagram and mentally implement it.

Prerequisites

  • S106-110 — the entire NLP/Transformers arc. This is the payoff session.
  • S102 · CNNs — for the residual connection intuition (ResNet).
  • S100 · PyTorch Fundamentals — nn.Module, forward, backward.


(a) Intuition · 5 min

A skyscraper made of identical floors
🌍 Real world

A 100-story skyscraper is remarkably simple: one floor plan repeated 100 times, plus a foundation and a roof. Each floor is identical structurally — same plumbing, electrical, walls — but each has different tenants, different furniture, different work happening.

A Transformer is the same. Twelve (or 96, or 480) identical blocks stacked on top of each other. Each block has the same architecture — attention + FFN with residual + layernorm — but during training each learns to specialise in different aspects of the input.

💻 Code world

The block itself is TINY compared to the whole model: multi-head attention (~4·d² params) + a 2-layer feed-forward network (~8·d² params) + a couple of layer norms (~2·d params). Everything else about a Transformer — depth, width, vocab, context length — is just scaling those knobs.

Everything you've learned so far — tokenization, embeddings, attention, positional encoding, multi-head — is the ingredient list. This session is the recipe.

The five pieces of a Transformer block

Every block has EXACTLY these five things
  • Multi-head self-attention — lets tokens communicate with each other.
  • Residual connection around attention — gradient highway, preserves original signal.
  • Layer normalisation — keeps activations at unit scale, essential for stable deep training.
  • Feed-forward network (2-layer MLP) — per-token computation that transforms each vector individually.
  • Second residual + layernorm around the FFN — same reasons as around attention.

A short history

  1. 2017
    Vaswani et al. · Attention Is All You Need
    Full encoder-decoder Transformer for translation. Post-LN, sinusoidal PE.
  2. 2018
    GPT-1 · Radford et al.
    Drop the encoder. Decoder-only for language modelling. First large-scale generative pretraining.
  3. 2018
    BERT · Devlin et al.
    Drop the decoder. Encoder-only with masked LM. Bidirectional context, dominates NLU benchmarks.
  4. 2019
    T5 · Google
    Full encoder-decoder revival. 'Everything is text-to-text.' Uses relative position bias, prenorm.
  5. 2020
    GPT-3 · OpenAI
    Decoder-only, 175B params. In-context learning surprises everyone.
  6. 2023
    LLaMA · Meta
    Decoder-only, RoPE, SwiGLU, pre-RMSNorm. The reference open-source recipe.

(b) Visual walkthrough · 15 min

The Transformer block

This is the Pre-LN variant (layer norm BEFORE the sublayer). Modern Transformers all use this. The original 2017 paper used Post-LN (layer norm AFTER), which is harder to train stably at scale.

The full architecture — decoder-only (GPT)

1input
Tokenizer + embedding

Text → token IDs → nn.Embedding lookup → [B, T, d_model]. Add positional encoding (or apply RoPE inside attention).

2core
N Transformer blocks

Stack N (typically 12-96) identical blocks. Each processes [B, T, d] and outputs [B, T, d]. Each block adds one layer of abstraction.

3norm
Final LayerNorm

One more norm before the output head. Standard in pre-LN Transformers.

4head
Linear head → vocab logits

nn.Linear(d_model, vocab_size). Often tied with the input embedding matrix (weight tying) to save params.

5output
Softmax → probabilities

Or use raw logits and let cross-entropy handle it. Argmax for greedy, sample for creative.

Encoder-only vs decoder-only vs encoder-decoder

Encoder-only (BERT)

Bidirectional understanding

  • Full attention — every token sees every token
  • Pretrained with masked LM
  • Great for classification, NER, retrieval
  • BERT, RoBERTa, DeBERTa, ModernBERT
Decoder-only (GPT)

Causal generation

  • Causal mask — token t sees only 1..t
  • Pretrained with next-token prediction
  • Generation, in-context learning, few-shot
  • GPT-3/4, LLaMA, Claude, Mistral
Encoder-decoder (T5)

Seq-to-seq translation

  • Encoder processes input, decoder generates output
  • Cross-attention: decoder queries encoder outputs
  • Best for translation, summarisation
  • T5, mT5, BART, Whisper, Flan-T5
Prefix-LM (UL2, PaLM)

Middle ground

  • Bidirectional on prefix, causal on suffix
  • One model, both understanding and generation
  • Rare in practice
  • UL2, some PaLM variants

Anatomy: what each component adds

Why each piece is non-negotiable

Multi-Head Self-Attention
How tokens communicate. Without this you have a per-token MLP — no context. The whole point of the architecture.
communicate
Residual connections
Gradient highways. Without residuals, gradients vanish through 12+ layers — training collapses. Introduced by ResNet (2015). Non-negotiable.
gradient
LayerNorm (pre-LN variant)
Keeps activation magnitudes at ~unit scale despite residual accumulation. Pre-LN (before sublayer) trains stably; post-LN needs warmup + tiny LR.
stability
Feed-Forward Network (FFN)
Per-token computation. Attention MIXES tokens; FFN TRANSFORMS each token individually. Where facts and features live. 4d hidden dim is standard.
compute
Activation function (GELU / SwiGLU)
Non-linearity in the FFN. Original: ReLU. Modern default: GELU (Gaussian Error Linear Unit) or SwiGLU (Gated variant). Small quality bump.
activation
Dropout
Regularisation on attention weights + FFN output. Standard 0.1-0.2. LLaMA uses 0 for pretraining (data is enough) but you'd add it back for fine-tuning small datasets.
regularise

Why pre-LN beat post-LN

In post-LN, the residual has NO norm, so as depth grows the residual magnitude blows up — training becomes unstable. Pre-LN normalises the sublayer input, keeping magnitudes bounded regardless of depth. Nobody trains post-LN Transformers past 12-24 layers.


Common misconception
✗ What most people think

"LayerNorm goes after the sublayer — that's what the original Transformer paper does. Pre-LN vs post-LN is a cosmetic detail."

✓ What is actually true

It decides whether the model trains at all beyond modest depth. Post-LN puts a normalisation on the residual path, so the identity shortcut is destroyed at every block and gradient magnitude compounds with depth — requiring a learning-rate warmup to avoid divergence. Pre-LN normalises the branch input and leaves the residual path clean, giving an unobstructed gradient highway from loss to embedding. Every large modern model is Pre-LN.

Why the myth is so sticky

Because the canonical diagram in the paper — the one everyone has memorised — shows "Add & Norm" after each sublayer, and the original 6-layer models trained fine that way. At 6 layers the difference is small and hidden by warmup. At 48+ layers it is the difference between converging and diverging, and the fix was discovered only after people tried to scale.

Prove it to yourself

Compare gradient norm reaching the first block in a deep stack under both orderings — same init, same input:

import torch, torch.nn as nn
class Blk(nn.Module):
    def __init__(s, d, pre):
        super().__init__(); s.pre=pre; s.n=nn.LayerNorm(d); s.f=nn.Linear(d,d)
    def forward(s,x):
        return x + s.f(s.n(x)) if s.pre else s.n(x + s.f(x))
for pre in (True, False):
    net = nn.Sequential(*[Blk(128, pre) for _ in range(48)])
    x = torch.randn(4, 128, requires_grad=True)
    net(x).pow(2).mean().backward()
    print('pre-LN' if pre else 'post-LN', x.grad.norm().item())
From first principles
Start with the question

Why is the FFN hidden dimension conventionally 4× the model dimension, and why is there an FFN at all when attention already mixes information?

  1. 1
    Attention's output at each position is a convex combination of value vectors, and each value is a linear map of a token. So attention alone is linear in its values.
    forced by · the only nonlinearity in attention is the softmax, and that acts on the mixing weights, not on the content being mixed
  2. 2
    A stack of linear mixings collapses: you would gain depth in routing but no ability to compute nonlinear functions of token content.
    forced by · composition of linear maps is a linear map
  3. 3
    So the block needs a position-wise nonlinearity. It must be position-wise (not across tokens) because attention already owns cross-token movement, and duplicating that would waste parameters and break the clean separation.
    forced by · separating "move information" from "process information" lets each be optimised and scaled independently
  4. 4
    The cheapest useful nonlinear map is up-project → nonlinearity → down-project. Its expressive power comes from the width of the hidden layer, which sets how many distinct features can be independently thresholded.
    forced by · with a ReLU-family activation, the hidden width is the number of half-space detectors available
  5. 5
    Wider is better up to the point where FFN parameters (2×d×dff) dominate attention parameters (4×d²). Setting dff=4d makes the FFN 8d² against attention's 4d² — a two-thirds/one-third split that empirically balances routing capacity against processing capacity.
    forced by · the ratio is a parameter-allocation decision between the two jobs, and 4× is where the tradeoff was found to sit
⇒ Therefore

Therefore the transformer block is deliberately two specialists: attention moves information between positions, the FFN computes on it in place. 4× is the allocation that keeps the second from starving the first.

And note what this predicts: since the FFN holds roughly two-thirds of a block's parameters and runs independently per position, it is the natural place to add sparse conditional compute. That is exactly what Mixture-of-Experts does — it replaces the FFN, never the attention — and why MoE models report huge parameter counts with modest active FLOPs. The derivation tells you where the parameters were sitting.

Mental modelResidual stream as a shared bus

Think of the residual stream as a wide bus running the full depth of the model, carrying the evolving representation of each token. Every block reads from the bus (through LayerNorm), computes something, and adds its result back. Nothing overwrites; everything accumulates.

Attention writes information copied from other positions. The FFN writes information computed from this position. The final layer just reads the bus and projects to vocabulary.

  • Block = Pre-LN → attention → add · Pre-LN → FFN → add. Two sublayers, two residual adds, two norms.
  • Residual connections exist so gradients reach layer 1 unattenuated; they are what makes depth trainable, not a regularisation trick.
  • Encoder: bidirectional attention. Decoder: causal self-attention plus (if encoder-decoder) cross-attention where Q comes from the decoder and K,V from the encoder.
  • Parameter split per block is roughly 1/3 attention, 2/3 FFN at dff=4d. Memory at inference, however, is dominated by the KV cache, not by weights.
🔔 Fires when you see

Fire this model the moment you see: a deep model that will not train without warmup · a question about where to add adapters or LoRA (the bus tells you what each site affects) · MoE proposals · a residual connection accidentally omitted · an interpretability claim about "features in the residual stream".

The tradeoff

Encoder-only, decoder-only, or encoder-decoder for a new system?

Encoder-only
+ you gain every token sees every other token in both directions, which is strictly more information per token for understanding tasks; one forward pass produces all representations, so it is cheap and trivially batchable
− you pay cannot generate — there is no autoregressive factorisation to sample from; needs a task-specific head and usually task-specific fine-tuning data
pick when classification, retrieval embeddings, token tagging, reranking — anything where the output is a label or a vector, not text, and throughput matters
Decoder-only
+ you gain one objective (next token) covers every task expressible as text, so it benefits from all data and scales without task engineering; the KV cache makes incremental decoding efficient
− you pay causal masking means early tokens never see later context, which is genuinely worse for pure understanding tasks; and generation is sequential, so latency scales with output length
pick when anything generative, and — given scale — most tasks, because the ecosystem, checkpoints and tooling are overwhelmingly here
Encoder-decoder
+ you gain bidirectional encoding of the input plus autoregressive output — the right structure for transduction, and cross-attention gives an explicit, inspectable input–output interface
− you pay two stacks to train and serve, a more complex architecture, and far fewer strong open checkpoints; the input must be a bounded, well-defined "source"
pick when true sequence-to-sequence with a clean source/target split and fixed-ish input length — translation, structured document transformation, speech transcription
What a senior engineer actually does

Decoder-only won the general case because a single next-token objective consumes any text, which turns architecture choice into a data-scale question — and scale beats inductive bias. But the encoder is not obsolete: at retrieval scale, embedding a billion documents with a bidirectional encoder is orders of magnitude cheaper than anything decoder-based, and it is what actually runs in production search.

The practical read: use a decoder-only LLM for generation and reasoning, an encoder for embeddings and reranking, and reach for encoder-decoder only when you have a genuine transduction task with a bounded source.


(c) Hands-on · 25 min

Build a complete tiny GPT (encoder-only version at the end for comparison) and generate text.

# tiny_gpt.py — a from-scratch decoder-only Transformer.
# Trains on a character-level corpus, then generates.
# Run: uv run tiny_gpt.py path/to/text.txt
import math
import sys
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
 
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
BLOCK_SIZE = 128
BATCH = 32
D_MODEL = 128
N_HEADS = 4
N_LAYERS = 4
D_FF = 4 * D_MODEL
DROPOUT = 0.1
LR = 3e-4
EPOCHS = 5
 
 
class CausalSelfAttention(nn.Module):
    def __init__(self, d_model: int, n_heads: int):
        super().__init__()
        assert d_model % n_heads == 0
        self.n_heads = n_heads
        self.d_head = 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)
        self.dropout = nn.Dropout(DROPOUT)
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        B, T, D = x.shape
        # One big projection, then split — faster than 3 separate matmuls.
        qkv = self.qkv(x)                              # [B, T, 3D]
        Q, K, V = qkv.split(D, dim=-1)
        Q = Q.view(B, T, self.n_heads, self.d_head).transpose(1, 2)
        K = K.view(B, T, self.n_heads, self.d_head).transpose(1, 2)
        V = V.view(B, T, self.n_heads, self.d_head).transpose(1, 2)
        # PyTorch's fused scaled_dot_product_attention with causal mask.
        out = F.scaled_dot_product_attention(Q, K, V, is_causal=True)  # [B, N, T, d_head]
        out = out.transpose(1, 2).contiguous().view(B, T, D)
        return self.dropout(self.proj(out))
 
 
class FeedForward(nn.Module):
    def __init__(self, d_model: int, d_ff: int):
        super().__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(DROPOUT)
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.dropout(self.fc2(F.gelu(self.fc1(x))))
 
 
class Block(nn.Module):
    """One Transformer block — pre-LN variant."""
 
    def __init__(self, d_model: int, n_heads: int, d_ff: int):
        super().__init__()
        self.ln1 = nn.LayerNorm(d_model)
        self.attn = CausalSelfAttention(d_model, n_heads)
        self.ln2 = nn.LayerNorm(d_model)
        self.ffn = FeedForward(d_model, d_ff)
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # Pre-LN: normalise BEFORE sublayer, add residual AFTER.
        x = x + self.attn(self.ln1(x))
        x = x + self.ffn(self.ln2(x))
        return x
 
 
class TinyGPT(nn.Module):
    def __init__(self, vocab_size: int, block_size: int, d_model: int,
                 n_heads: int, n_layers: int, d_ff: int):
        super().__init__()
        self.block_size = block_size
        self.tok_embed = nn.Embedding(vocab_size, d_model)
        self.pos_embed = nn.Embedding(block_size, d_model)  # learned PE for simplicity
        self.blocks = nn.ModuleList([
            Block(d_model, n_heads, d_ff) for _ in range(n_layers)
        ])
        self.ln_f = nn.LayerNorm(d_model)
        self.head = nn.Linear(d_model, vocab_size, bias=False)
        # Weight tying — share weights between token embedding and output head.
        self.head.weight = self.tok_embed.weight
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        B, T = x.shape
        pos = torch.arange(T, device=x.device)
        h = self.tok_embed(x) + self.pos_embed(pos)     # [B, T, d]
        for block in self.blocks:
            h = block(h)
        h = self.ln_f(h)
        return self.head(h)                              # [B, T, vocab]
 
    @torch.no_grad()
    def generate(self, idx: torch.Tensor, max_new: int, temperature: float = 1.0) -> torch.Tensor:
        for _ in range(max_new):
            idx_cond = idx[:, -self.block_size:]         # crop to context window
            logits = self(idx_cond)[:, -1, :]            # last token's logits
            probs = F.softmax(logits / temperature, dim=-1)
            next_tok = torch.multinomial(probs, num_samples=1)
            idx = torch.cat([idx, next_tok], dim=1)
        return idx
 
 
def get_batches(data: torch.Tensor, block_size: int, batch_size: int) -> DataLoader:
    n = (len(data) - 1) // block_size
    x = data[: n * block_size].view(n, block_size)
    y = data[1 : n * block_size + 1].view(n, block_size)
    return DataLoader(TensorDataset(x, y), batch_size=batch_size, shuffle=True)
 
 
def main(path: str) -> None:
    text = open(path, "r", encoding="utf-8").read()
    chars = sorted(set(text))
    stoi = {c: i for i, c in enumerate(chars)}
    itos = {i: c for c, i in stoi.items()}
    data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
    print(f"vocab={len(chars)} chars={len(text):,} device={DEVICE}")
 
    loader = get_batches(data, BLOCK_SIZE, BATCH)
    model = TinyGPT(len(chars), BLOCK_SIZE, D_MODEL, N_HEADS, N_LAYERS, D_FF).to(DEVICE)
    params = sum(p.numel() for p in model.parameters())
    print(f"params: {params:,}  ({params/1e6:.2f}M)")
    opt = torch.optim.AdamW(model.parameters(), lr=LR)
 
    for epoch in range(1, EPOCHS + 1):
        model.train()
        total = 0.0
        for step, (x, y) in enumerate(loader, 1):
            x, y = x.to(DEVICE), y.to(DEVICE)
            logits = model(x)
            loss = F.cross_entropy(logits.reshape(-1, len(chars)), y.reshape(-1))
            opt.zero_grad()
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            opt.step()
            total += loss.item()
        # Generate a sample.
        model.eval()
        seed = torch.tensor([[stoi[text[0]]]], device=DEVICE)
        out = model.generate(seed, max_new=200, temperature=0.8)
        sample = "".join(itos[i.item()] for i in out[0])
        print(f"epoch {epoch}  loss={total/step:.3f}")
        print(f"  sample: {sample[:100]!r}...")
 
 
if __name__ == "__main__":
    main(sys.argv[1])

Anatomy of the script

What the interesting lines do

self.qkv = nn.Linear(d, 3d)
Fused Q/K/V projection — one big matmul is faster than three. Split with qkv.split(D, dim=-1). Standard trick in GPT-2 and later.
fused qkv
F.scaled_dot_product_attention(Q, K, V, is_causal=True)
PyTorch 2.0+ primitive that uses FlashAttention automatically on CUDA. is_causal=True applies the mask internally.
flash
Block: pre-LN order
x + attn(ln1(x)), then x + ffn(ln2(x)). Layer norm BEFORE sublayer, residual OUTSIDE the LN+sublayer combo. This is the modern standard.
pre-LN
F.gelu(self.fc1(x))
GELU activation between FFN layers. Smoother than ReLU, small quality boost, standard in every LLM since GPT-2.
gelu
self.head.weight = self.tok_embed.weight
Weight tying — share the vocab embedding matrix with the output projection. Saves ~vocab_size × d_model params (big deal at 100K vocab × 4K d_model = 400M params). Small quality boost too.
tie
idx[:, -self.block_size:]
Crop context to block_size before feeding. Learned PE only knows up to block_size positions.
crop
torch.multinomial(probs, num_samples=1)
Sample from the distribution instead of argmax. Adds diversity. Temperature adjusts sharpness.
sample
clip_grad_norm_(1.0)
Gradient clipping — cap L2 norm at 1.0. Standard for Transformers to avoid occasional huge updates.
safety
Try itSwap decoder-only for encoder-only and see the difference

Two changes:

  1. In CausalSelfAttention.forward, change is_causal=True to is_causal=False.
  2. Change training loop: randomly replace 15% of input tokens with a [MASK] token, then predict ONLY those positions.

Result: a mini-BERT. Won't generate text (that's not what encoder-only does) but will give great sentence embeddings for downstream classification.

💡 Hint · Remove `is_causal=True` from scaled_dot_product_attention. Now every token sees every other token — bidirectional. Change training loss to masked LM: randomly mask 15% of input tokens and predict them. This is BERT.

(d) Production reality · 15 min

War story Meta · LLaMA architectural choices· 20237B-70B
🔥 What broke

Early LLaMA-1 experiments used vanilla Transformer decoders (like GPT-2). Meta's team ran ablations on architectural choices at 7B scale and found several small tweaks compounded into significant quality + speed gains.

🧯 The fix
LLaMA's reference recipe: (1) RMSNorm instead of LayerNorm — simpler, ~5% faster, no quality loss. (2) SwiGLU activation instead of GELU — ~1% quality boost. (3) RoPE instead of learned PE — better extrapolation. (4) Grouped-query attention (LLaMA-2 34B+) — smaller KV cache. Every open-source LLM since (Mistral, Falcon, Yi, DeepSeek) uses variations of this stack.
🎓 Lesson to steal
The 'reference' Transformer architecture keeps evolving. If you're building an LLM today, start with LLaMA's stack, not the 2017 paper. Small changes compound at scale.
Post-mortem
War story Google · BERT vs GPT choice for search· 2019billions of queries
🔥 What broke
Google's search team debated: should we use GPT-style (decoder) or BERT-style (encoder) for query understanding? Early tests with GPT gave decent results but were much slower — autoregressive decoding is not parallel, and search demands sub-100ms latency.
🧯 The fix
Encoder-only won for search understanding. BERT processes the query in one parallel forward pass, giving a fixed-size representation. Decoder-only (GPT) would need to generate a response — much slower. This is why 2019's "BERT in Search" launch was BERT-based, and why encoder-only models still dominate retrieval, embeddings, and classification tasks.
🎓 Lesson to steal
Encoder-only isn't dead. For tasks that need FIXED-SIZE representations quickly (embeddings, classification, retrieval), encoder-only beats decoder-only on latency by 10-100×. ModernBERT (2024) proves the family is still evolving.
Post-mortem
War story OpenAI · GPT-3 scaling laws· 2020research → 175B model
🔥 What broke
OpenAI's team wanted to know: at fixed compute budget, is it better to make the model larger, train it longer, or use more data? Existing wisdom was 'model size dominates.' They tested this rigorously.
🧯 The fix
The Kaplan et al. 2020 scaling laws paper: loss is a smooth power law in model size, data, and compute. For a given compute budget, there's an optimal (model_size, tokens_seen) trade-off. Chinchilla (2022) revised this — earlier models were UNDER-trained. LLaMA-3 doubled down: 8B params trained on 15 TRILLION tokens. Same architecture, way more data, dramatically better model.
🎓 Lesson to steal
Architecture matters less than you think at scale. The Transformer block from 2017 still works — it's data, compute, and training tricks that produced GPT-4. Fixate on the ingredients, not the recipe.
Post-mortem

Where this shows up in the rest of the plan

The Transformer block is the foundation of every model that follows
S112 · BERT & Pretraining
Encoder-only Transformer with MLM objective. Same block structure, no causal mask.
S113 · GPT-2 & Causal LM
Decoder-only, causal attention, next-token prediction. Everything you built in this session.
S119 · LoRA & PEFT
Fine-tuning tweaks inject small matrices into attention (Q and V projections). Understanding the block = understanding where to inject.
S125 · RAG
Retrieval + a decoder-only LLM. The generation half is exactly what you built.
S127 · Vision Transformer
Same block, image patches instead of tokens. Proves the architecture is modality-agnostic.
S117 · Diffusion Models
Modern diffusion models use Transformer blocks (DiT). Same primitive, different objective.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

If you can't teach these three without notes, redo the session:

  1. Draw a Transformer block from memory and explain why each piece is needed.
  2. When would you pick encoder-only vs decoder-only vs encoder-decoder?
  3. Why did pre-LN replace post-LN, and why is the FFN 4× wider than d_model?

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.