TAI BUI
Back to curriculum
BuildPython

The Full Transformer — Encoder + Decoder

Attention does the work. Residuals, normalization, the feed-forward layer, and cross-attention are the scaffolding that lets you stack it deep.

TIME: ~75 minutesPREREQUISITES: Phase 7 · 02 (Self-Attention), Phase 7 · 03 (Multi-Head Attention), Phase 7 · 04 (Positional Encoding)

Learning Objectives

  • Name the six pieces of a transformer block and say what each one does.
  • Wire a pre-norm encoder block and a decoder block (masked self-attention, cross-attention, FFN), and explain where encoder information enters the decoder.
  • Describe the 2026 block (RMSNorm, SwiGLU, RoPE, GQA, no biases) as substitutions into the same 2017 skeleton.
  • Estimate parameters per block and per stack from d_model and the FFN expansion ratio.

The Problem

A single attention layer is a feature extractor, not a model. One matmul per layer doesn't give you enough capacity for language, so you need depth, and depth breaks without the right plumbing.

The 2017 Vaswani paper packaged six design decisions that turned one attention layer into a stackable block. Encoder-only (BERT), decoder-only (GPT), and encoder-decoder (T5) models all inherit that skeleton. By 2026 the parts have been refined (RMSNorm, SwiGLU, pre-norm, RoPE), but the skeleton is the same.

This is the skeleton. The next lessons specialize it: 06 for encoders, 07 for decoders, 08 for encoder-decoder.

Warm-up0/3
1. Self-attention compares every position with every other position and ignores where each one sits. What does adding a positional signal to the embeddings fix?Warm-up
Two rows, A and B, each with the words dog, bites, man in a different order. Each word becomes an embedding vector, passes through a dashed box with a question mark, then into a self-attention box.
Typed example sentences for illustration, not from the lesson.
2. A multi-head attention layer has d_model = 512 and 8 heads. How wide is each head?Warm-up
A 512-wide input X is projected to Q, which fans out into eight head boxes, head 1 to head 8, each marked with an unknown width.
3. A decoder generates text one token at a time. What does a causal mask on its self-attention prevent?Warm-up
A 4 by 4 grid of attention scores. Rows are queries and columns are keys for the tokens The, cat, sat, down. The cell for query cat and key sat is highlighted with a question mark.
Typed tokens for illustration. The grid shows where scores exist; no values are drawn.

The Concept

Encoder and decoder block internals, wired

A transformer block is an attention sublayer and a feed-forward sublayer, each wrapped in a residual connection and a normalization step. (A decoder block adds a cross-attention sublayer between the two.) A transformer is a stack of these blocks on top of an embedding and positional signal. Encoder-decoder models like T5 run two such stacks.

The six pieces

  1. Embedding + positional signal. Tokens become vectors. Position is injected via RoPE (modern) or sinusoidal encoding (classic).
  2. Self-attention. Each position attends to every other position. Decoders mask it.
  3. Feed-forward network (FFN). A position-wise two-layer MLP: W_2 · activation(W_1 · x). The expansion ratio is 4× by default.
  4. Residual connection. x + sublayer(x). Without it, gradients vanish past ~6 layers.
  5. Layer normalization. LayerNorm or, in modern stacks, RMSNorm. It stabilizes the residual stream.
  6. Cross-attention (decoder only). Queries come from the decoder, keys and values from the encoder output.

Watch a vector flow through one block: attention mixes across positions, the residual carries it forward, the FFN transforms it, and norm keeps the stream stable.

Encoder block (used by BERT, T5 encoder)

x → LN → MHA(self) → + → LN → FFN → + → out
                     ^              ^
                     |              |
                     └── residual ──┘

The encoder is bidirectional. There is no masking, so all positions see all positions.

Decoder block (used by GPT, T5 decoder)

x → LN → MHA(masked self) → + → LN → MHA(cross to encoder) → + → LN → FFN → + → out

The decoder has three sublayers per block. The middle one, cross-attention, is the only place information flows from encoder to decoder. A pure decoder-only architecture (GPT) omits it, leaving masked self-attention plus the FFN.

Pre-norm vs post-norm

Post-norm, as in the original paper, is LN(x + sublayer(x)). Pre-norm is x + sublayer(LN(x)). Post-norm lost favor around 2019 because it is harder to train deeply without careful warmup. Pre-norm, with LN before the sublayer, is the 2026 default in Llama, Qwen, GPT-3+, and Mistral.

The 2026 modernized block

Vaswani 2017 shipped LayerNorm and ReLU. Modern stacks replaced both. Here is what production blocks look like now:

Component20172026
NormalizationLayerNormRMSNorm
FFN activationReLUSwiGLU
FFN expansion4×2.6× (SwiGLU uses three matrices, total params match)
PositionSinusoidal absoluteRoPE
AttentionFull MHAGQA (or MLA)
Bias termsYesNo

RMSNorm drops the mean-centering step of LayerNorm. That is one fewer subtraction, which saves compute, and it is empirically at least as stable. SwiGLU (Swish(W1 x) ⊙ W3 x) beats the ReLU/GELU FFN by ~0.5 point ppl in the Llama, PaLM and Qwen papers.

Parameter count

For one block with d_model = d and FFN expansion r:

  • MHA: 4 · d² (Q, K, V, O projections)
  • FFN (SwiGLU): 3 · d · (r · d) ≈ 3rd²
  • Norms: negligible

At d = 4096, r = 2.6 (roughly the shape of Llama 3 8B):

  • MHA: 4 · 4096² ≈ 67M
  • FFN (SwiGLU): 3 · 2.6 · 4096² ≈ 131M
  • One block: ≈ 198M

Over 32 layers that is ≈ 6.3B parameters, before embeddings and the output head, which add on the order of a billion more. That lands in the 7-8B range of the published model. The match is approximate, because Llama 3 8B uses GQA (smaller K and V projections) and a different FFN width than this formula assumes.

Check: The Concept0/26
1. What does RMSNorm do differently from LayerNorm?Define
A row of four numbers, 1, 2, 3 and 6, with two output rows below it labelled LayerNorm(x) and RMSNorm(x), each holding four unknown values.
Typed input row. Outputs are computed in the explanation view with the lesson's functions.
2. What is the feed-forward network in the Six pieces list?Define
Two input rows of four shown values go into a box labelled FFN and come out as two output rows of four shown values.
Real output of the lesson's ffn_relu on two random rows of width 8 (seed 7); the matrices are not shown here.
3. What happened to bias terms in linear layers between the 2017 block and the 2026 block?Define
Two cards. The 2017 block card lists a weight W of size d by d and a bias b of size d. The 2026 block card lists the same weight and a dashed box marked bias b with a question mark.
Layout drawn from the lesson's 2017 versus 2026 table.
4. What is a transformer block?Define
Input rows of shape 6 by 8 feed two dashed boxes, each marked unit with a question mark, which feed an output rows box.
Sizes are the lesson's toy run.
5. What is a residual connection in a transformer block?Define
An input vector x of 1, -0.5, 2 goes into a sublayer f whose output f of x is 0.2, 0.1, -0.3. Below, three dashed boxes with question marks stand for the block output.
Typed numbers for illustration.
6. What does pre-norm mean?Define
A horizontal residual stream from x to an addition circle, with a branch through a box labelled sublayer that rejoins at the circle. A separate chip labelled LN is not yet connected.
Schematic: the position of LN is left open.
7. What is SwiGLU?Define
Two input rows of four shown values go into a box labelled SwiGLU and come out as two output rows of four shown values.
Real output of the lesson's ffn_swiglu on two random rows of width 8 (seed 7); weights not shown.
8. What is cross-attention?Define
Two boxes at the top, encoder output of shape 6 by 8 and decoder stream of shape 5 by 8, and a cross-attention box below with three dashed inputs, Q from ?, K from ? and V from ?.
Shapes are the lesson's toy run; the wiring is left open.
9. What does the FFN expansion ratio describe?Define
A code panel with d equals 8, n_heads equals 2, ffn_expansion equals 2.0 and a BlockParams call. An arrow points from the ffn_expansion line to a dashed box asking what 2.0 describes.
Settings from the lesson's main().
10. Why does a deep stack of attention blocks need residual connections?Explain
A vertical stack of blocks from block N at the top to block 1 at the bottom, with a gradient arrow running back from the loss and a dashed box asking what reaches block 1.
Schematic: the stack depth is not specified by the question.
11. How many sublayers does a decoder block have, and which one carries information from the encoder?Explain
An encoder output box of shape 6 by 8 with an arrow into a large decoder block box. Inside, two dashed boxes ask how many sublayers there are and where the encoder output enters.
Schematic.
12. Attention and the FFN do different jobs inside a block. Which statement is right?Explain
Two panels, attention sublayer and FFN sublayer, each with five input dots at the bottom and five output dots at the top. Output position 3 is outlined and a dashed box asks which inputs it reads from.
Schematic of five positions.
13. How does self-attention differ between an encoder block and a decoder block?Explain
Two empty 5 by 5 grids labelled encoder block and decoder block, standing for attention weights with rows as queries and columns as keys.
Schematic: values are shown in the explanation view.
14. Why does a SwiGLU feed-forward network use an expansion of about 2.6 instead of 4?Explain
Two cards. The 2017 block has a ReLU FFN with 4x expansion. The 2026 block has a SwiGLU FFN with about 2.6x expansion. Below is the question why the 2026 expansion is not 4x.
Values from the lesson's 2017 versus 2026 table.
15. How does the lesson frame the 2026 changes (RMSNorm, SwiGLU, RoPE, GQA, no biases) relative to the 2017 block?Explain
Five chips: RMSNorm, SwiGLU, RoPE, GQA and no biases, with an arrow to a dashed box asking how these relate to the 2017 block.
Chips are the changes named in the question.
16. A block returns `LN(x + sublayer(x))`. Which wiring is this, and what is its known drawback?Explain
A chain x, sublayer, plus, LN, out with a skip from x to the plus, the formula out equals LN of x plus sublayer of x, and a dashed box asking for the wiring's name and drawback.
Formula from the question.
17. At d_model = d, how does a SwiGLU FFN with r ≈ 2.6 compare to the attention projections in one block?Apply
A table of seven weight matrices: W_Q, W_K, W_V and W_O of shape d by d under attention, and W1, W3 and W2 with a 2.6 times wider hidden size under the FFN. The params column holds question marks.
Shapes follow the lesson's parameter-count section.
18. Using the Parameter count formulas, roughly how many parameters do 32 layers of attention and SwiGLU FFN hold at d = 4096, r = 2.6, before embeddings?Apply
Thirty-two numbered block squares in two rows, a note that each block has four 4096 by 4096 attention matrices and three FFN matrices, and a dashed box asking for the total.
Sizes are from the question.
19. At d_model = 512 with 8 heads, roughly how many parameters do the Q, K, V, and O projections of one MHA sublayer hold, using 4·d²?Apply
Four boxes W_Q, W_K, W_V and W_O, each 512 by 512, and a dashed box asking for the parameters in all four.
Sizes from the question.
20. At d_model = 512 and expansion r = 4, roughly how many parameters does a SwiGLU FFN hold, using 3·r·d²?Apply
Three weight boxes: W1 of 512 by 2048, W3 of 512 by 2048 and W2 of 2048 by 512, and a dashed box asking for their total parameters.
Sizes derived from d = 512 and r = 4.
21. Using 4·d² for attention and 3·r·d² for the FFN, roughly how many parameters do 24 layers hold at d = 2048, r = 2.6, before embeddings?Apply
A block card listing attention at 4 d squared and FFN at 3 r d squared, a multiplier of 24 layers, and a dashed box asking for the total.
Constants from the question.
22. A 24-layer post-norm stack diverges early in training unless you tune warmup carefully. Which change does the Pre-norm vs post-norm section point to?Analyze
Twenty-four numbered block squares in two rows, a note that training diverges early unless warmup is tuned, a schematic rising loss curve, and a dashed box asking which change helps.
Schematic: the curve is not measured data.
23. A reviewer finds a decoder block whose cross-attention computes K and V from the decoder's own hidden states. What is wrong?Analyze
A decoder hidden states box feeding three projections Wq_x, Wk_x and Wv_x, which feed an attention box, and a dashed box asking what is wrong with the wiring.
Drawn from the question's description.
24. A block has a residual around its attention but returns the FFN output directly, with no `x +`. What is the most likely consequence in a deep stack?Analyze
A chain x, norm, attention, plus, norm, FFN with a skip from x to the plus after attention and no skip around the FFN. A note says the block output is the FFN output, and a dashed box asks for the effect in a deep stack.
Drawn from the question's description.
25. A formula count for a 32-layer block stack at d = 4096 plus embeddings lands near the published size of Llama 3 8B but not exactly. What does the lesson give as the reason?Analyze
Two horizontal bars. The formula bar has 6.3 billion for 32 blocks plus a dashed segment of about 1 billion for embeddings. The published bar is Llama 3 8B. A dashed box asks why they are near but not exact.
The embeddings segment is the lesson's "on the order of a billion", drawn as an estimate.
26. You are designing a 60-layer decoder-only chat model. Which block follows the lesson's 2026 defaults?Design
A form with eight fields: norm wiring, normalization, FFN, FFN width, positions, attention, biases and cross-attention, each with a dashed question mark.
Blank spec form.

Build It

Step 1: the building blocks

Start with the tiny Matrix class from Lesson 03, copied into this file so it runs on its own. The pieces:

  • layer_norm(x, eps=1e-5): subtract the mean, divide by the std. It is defined for comparison, but the blocks never call it.
  • rms_norm(x, eps=1e-6): divide by the RMS, with no mean subtraction. Every norm in the blocks is this one. Neither norm has a learned scale.
  • silu(x), used inside ffn_swiglu(x, W1, W2, W3): silu(x W1) ⊙ (x W3), then W2. ffn_relu(x, W1, W2) is the classic alternative.
  • BlockParams(d, n_heads, ffn_expansion, rng, use_swiglu=True): the weights for one block. It also allocates the cross-attention weights (Wq_x and friends) whether or not the block uses them, so an encoder block carries four unused d × d matrices.
  • encoder_block(x, params) and decoder_block(x, enc_out, params).

See code/main.py for the full wiring.

Step 2: wire a 2-layer encoder and a 2-layer decoder

Stack the blocks, and pass the encoder output into the cross-attention of each decoder block:

python
enc_out = src
for p in enc_params:
    enc_out = encoder_block(enc_out, p)
 
dec_out = tgt
for p in dec_params:
    dec_out = decoder_block(dec_out, enc_out, p)

The demo runs at toy scale: d = 8, 2 heads, FFN expansion 2.0. It skips token embeddings, positional encoding, a final norm, and the output projection. main() feeds random (len, d) matrices straight into the block loops, so the architecture is the whole point.

Step 3: run forward on a toy example

Feed a 6-row source and a 5-row target through the stack. The encoder output is (6, 8) and the decoder output is (5, 8), which is (tgt_len, d). A full model would add a final norm and a projection to (5, vocab), but this code stops before that. There's no training here. The goal is the architecture, not the loss.

Step 4: swap one component at a time

The default stack already uses pre-norm, RMSNorm, and SwiGLU. The one switch in the code is use_swiglu: build BlockParams(..., use_swiglu=False) to get the ReLU FFN and confirm the shapes still match. The norm is hardcoded, so to try LayerNorm, replace the rms_norm calls inside the blocks with layer_norm. RoPE, GQA, and bias handling are not implemented in main.py; the 2026 table above describes them, but the code does not run them.

Check: Build It0/7
1. In main.py, a 6-row source goes through the encoder and a 5-row target through the decoder, both with d = 8. What shape does the decoder output have?Apply
Source of shape 6 by 8 goes through two encoder blocks to enc_out. Target of shape 5 by 8 goes through two decoder blocks that also read enc_out. The final output is a dashed box with unknown shape.
Sizes from the lesson's toy run.
2. In main.py, a 6-row source with d = 8 goes through a 2-layer encoder. What is the shape of the final encoder output?Apply
A source of shape 6 by 8 passes through block 1, a dashed unknown shape, block 2, and a second dashed unknown shape.
Sizes from the lesson's toy run.
3. You build `BlockParams(..., use_swiglu=False)` and run the same forward pass. What happens?Apply
A code panel with the default BlockParams call and a second call that adds use_swiglu equals False, then a dashed box asking what changes.
Calls follow the lesson's BlockParams signature.
4. The demo stacks two decoder blocks on a 6-row encoder output. How is enc_out used?Apply
An enc_out box of shape 6 by 8 on the left and two stacked decoder block boxes on the right, each with a dashed cross-attention input asking where K and V come from.
Sizes from the lesson's toy run; the wiring is left open.
5. A teammate says the toy main.py run shows RoPE and GQA working because the shapes match. What is wrong with this claim?Analyze
A quote saying the shapes match so RoPE and GQA are working, above the six-row 2026 table with a column asking whether each item is in main.py, filled with question marks.
Rows from the lesson's 2026 table.
6. Your hand count for an encoder block at d = 512, r = 4 with SwiGLU is 16d², but the weights BlockParams allocates come to 20d². Why the gap?Analyze
Two bars. The hand count is attention 4 d squared plus FFN 12 d squared, 16 d squared or 4.2 M. The BlockParams bar is 20 d squared or 5.2 M, with its last 4 d squared as a dashed unknown.
d squared = 262,144 at d = 512.
7. You port a 2017-style block to the 2026 defaults and want to know which change causes any new problem. What approach does the lesson recommend?Design
A table of a 2017-style block: LayerNorm, ReLU, 4x, sinusoidal, full MHA and bias terms yes, with an arrow to a dashed box asking for the plan.
Values from the lesson's 2017 column.

Use It

PyTorch and TF ship reference implementations: nn.TransformerEncoderLayer and nn.TransformerDecoderLayer. Most 2026 production code rolls its own block anyway, for three reasons:

  • Flash Attention is called inside attention, not through nn.MultiheadAttention.
  • GQA and MLA aren't in the stdlib reference.
  • RoPE, RMSNorm, and SwiGLU aren't the PyTorch defaults.

HF transformers has clean reference blocks worth reading. modeling_llama.py is the canonical 2026 decoder-only block, about 500 lines, and worth one walkthrough.

Encoder vs decoder vs encoder-decoder: when to pick which.

NeedPickExample
Classification, embeddings, QA over textEncoder-onlyBERT, DeBERTa, ModernBERT
Text generation, chat, code, reasoningDecoder-onlyGPT, Llama, Claude, Qwen
Structured input → structured output (translation, summarization)Encoder-decoderT5, BART, Whisper

Decoder-only won language because it scales cleanly and handles both comprehension and generation. Encoder-decoder is still the better fit when the input has a clear "source sequence" identity, as in translation, speech recognition, and structured tasks.

Check: Use It0/4
1. Why did decoder-only architectures win language?Explain
Three columns: encoder-only with BERT, DeBERTa and ModernBERT; decoder-only with GPT, Llama, Claude and Qwen; encoder-decoder with T5, BART and Whisper. A dashed box asks which one won language and why.
Model names from the lesson's Use It table.
2. Why does most 2026 production code roll its own block instead of using `nn.TransformerEncoderLayer`?Explain
Two cards: PyTorch reference layers nn.TransformerEncoderLayer and nn.TransformerDecoderLayer, and a block class in your own code such as HF modeling_llama.py. A dashed box asks why production code usually writes its own.
Names from the lesson's Use It section.
3. You need document embeddings for classification and retrieval, with no text generation. Which family does the Use It table pick?Design
Three document boxes feed a dashed model box marked with a question mark, which produces one vector per document, with no text generated. The vectors feed a classifier and a retrieval index.
Task setup from the question.
4. You are building a translation system with a clear source sentence and a target sentence. Which family fits best?Design
A source sentence box, a dashed model box with a question mark, and a target sentence box.
Typed example sentences for illustration.

Ship It

The skill in outputs/skill-transformer-block-reviewer.md reviews a new transformer block implementation against the 2026 defaults. It flags missing pieces: pre-norm, RoPE, RMSNorm, GQA, and the FFN expansion ratio.

Check: Ship It0/1
1. What does the Ship It skill, `transformer-block-reviewer`, do?Define
A folder tree with code containing main.py and outputs containing skill-transformer-block-reviewer.md, and an arrow from the skill file to a dashed box asking what this skill does.
Names from the lesson; the file's contents are left out.
Author's Note

If I'm reviewing someone's block, I check the wiring before the math. Where the norm sits relative to the residual, whether each sublayer has its own residual, and where the cross-attention keys and values come from are the first three things I look at. In main.py the encoder and decoder blocks differ by a causal=True flag and one extra sublayer, which is worth noticing before you open a 500-line production file.

My bet is that the 2026 changes (RMSNorm, SwiGLU, RoPE, GQA) read best as substitutions into the same skeleton, not as a new architecture. In the code, the ReLU FFN and SwiGLU swap behind a single use_swiglu flag and the block wiring doesn't change. I'd run the toy forward pass, confirm shapes, then change one component at a time, so when something looks off you know which swap did it.

I'd also treat the parameter-count formula as a sanity check, not a spec. Re-derive it for your own d and r before you trust any number built on it.

Exercises

  1. Easy. Count the parameters in a block at d_model=512, n_heads=8, ffn_expansion=4, swiglu=True, with no biases and no norm scales. By the Parameter count formulas that is 4d² + 3·4·d² = 16d² ≈ 4.2M for self-attention plus the FFN. Then count the weights BlockParams in main.py allocates at the same settings: it adds a second set of four d × d cross-attention matrices, so it holds 20d² ≈ 5.2M even for an encoder block. Validate both counts by implementing the block and using sum(p.numel() for p in block.parameters()).
  2. Medium. Switch from post-norm to pre-norm. Initialize both and measure the activation norm after 12 stacked layers on random input. Post-norm's activations should explode; pre-norm's should stay bounded.
  3. Hard. Implement a 4-layer encoder-decoder on a toy copy task (copy x reversed). Train 100 steps. Report loss. Swap in RMSNorm + SwiGLU + RoPE. Does loss drop?

Key Terms

TermWhat people sayWhat it actually means
Block"One transformer layer"Norm + attention + norm + FFN, each sublayer wrapped in a residual connection.
Residual"Skip connection"The output x + f(x). It lets gradients flow through deep stacks.
Pre-norm"Normalize before, not after"The modern wiring, x + sublayer(LN(x)). It trains deeper without warmup gymnastics.
RMSNorm"LayerNorm without the mean"Divide by the RMS. One fewer op, with the same empirical stability.
SwiGLU"The FFN most modern stacks use"Swish(W1 x) ⊙ W3 x → W2. It beats ReLU/GELU on LM ppl.
Cross-attention"How the decoder sees the encoder"MHA with Q from the decoder and K/V from the encoder outputs.
FFN expansion"How wide the middle MLP is"The ratio of hidden size to d_model, usually 4 (LayerNorm) or 2.6 (SwiGLU).
Bias-free"Drop the +b terms"Modern stacks omit biases in linear layers, for a slight ppl improvement and a smaller model.

Further Reading

Code

Ships With