Chapter 10 · 16 min
The full transformer block
Attention + feed-forward + residuals + LayerNorm, assembled into the block that GPT stacks N times. End-to-end forward pass.
You have every piece. Let's assemble the actual thing.
A block is the unit you stack to make a . Each block does two things, in order:
- Multi-head — let every look at every other .
- Feed-forward network () — process each 's representation independently. Both are wrapped in the + machinery from chapter 9. The block's output has the same shape as its input, which is what lets us stack identical blocks.
This chapter has three runnable cells: build the , assemble one block, stack two blocks and add an unembedding to get a full forward pass. Then you will create the first llm/model.py in your local repo. It will still be tiny and mostly shape-checking, but it will finally look like a .
Same toy sentence as chapters 8–9.
d_model = 8,n_heads = 4,ffn_hidden = 16,n_blocks = 2,vocab_size = 5. Tiny — but the architecture is the same as GPT.
1. The feed-forward network
Each block has a per- that runs independently on each 's representation. It has two linear layers with a non-linearity in between:
The hidden dimension ffn_hidden is usually 4× d_model (so the has a lot more than the sublayer). The non-linearity is the smooth descendant of that modern use; we provide it.
Crucially, the does not let see each other. The only cross- mixing in a block is . The 's job is to think about what pulled in.
Your turn · JavaScript
The output has the same shape as the input — same [seq_len × d_model]. That's the contract every sublayer in a respects.
2. The full block
Now compose and with the pre-norm recipe:
The chapter wires the and with their weights pre-bound; the cell shows the composition directly.
Your turn · JavaScript
This is the entire block. The reason it works is the combination of three properties: the stream stays unchanged in scale ( + add), lets information flow across positions, and the lets each position think about that information independently. The and maintain consistent activation scales.
3. Stack blocks + unembedding
The architecture is just N blocks in sequence, with a final and an unembedding matrix that projects the final hidden state to .
The output is [seq_len × vocab_size]. One logit row per input position. For autoregressive , you sample from the last row's .
Your turn · JavaScript
That's a . The model has random weights, so the bar plot at the bottom is meaningless — but the shape is right. A real is this same architecture with a few corrections (, positional encoding, much bigger numbers) trained on billions of of text.
What we skipped
A real would also have:
- Token . We started with
Xas if it were already embedded. A real model first looks up each 's row in an matrix. - Positional encoding (or RoPE). Attention is permutation-equivariant — without positional information, the model can't tell
the cat satfromsat cat the. Real models add or interleave a position signal. - Causal masking. In a decoder-only (GPT-style), i isn't allowed to attend to j > i during . Implemented by setting future-position scores to −∞ before .
- Dropout during .
- Layer-norm (γ, β) that are learned, not fixed at 0/1.
Most of these are one-line additions. None changes the shape of the architecture. The thing you just built is the core; the rest is plumbing.
4. Create your first model skeleton
Create llm/model.py:
"""A tiny GPT-shaped model skeleton.
Chapter 12 replaces the list math with PyTorch tensors. The architecture stays:
token embedding, position embedding, transformer blocks, final logits.
"""
from __future__ import annotations
from llm.attention import Matrix, causal_attention, matmul
from llm.nn import add, layer_norm, linear, relu
# [1]
def feed_forward(x: Matrix, w1: Matrix, b1: list[float], w2: Matrix, b2: list[float]) -> Matrix:
return [linear(relu(linear(row, w1, b1)), w2, b2) for row in x]
def transformer_block(
x: Matrix,
wq: Matrix,
wk: Matrix,
wv: Matrix,
ffn_w1: Matrix,
ffn_b1: list[float],
ffn_w2: Matrix,
ffn_b2: list[float],
) -> Matrix:
# [2]
attended = causal_attention(layer_norm(x), wq, wk, wv)
# [3]
x = add(x, attended)
# [4]
return add(x, feed_forward(layer_norm(x), ffn_w1, ffn_b1, ffn_w2, ffn_b2))
# [5]
def logits(hidden: Matrix, unembed: Matrix) -> Matrix:
return matmul(layer_norm(hidden), unembed)This skeleton is a map of the full model:
- [1]
feed_forwardapplies the same to each row. It does not mix positions; already did that. - [2] starts the block with pre-norm : normalize first, then route information between .
- [3] adds the attended update back into the stream.
- [4] repeats the same pattern with the : normalize, transform, add back.
- [5]
logitsconverts hidden vectors into scores. One row of means “scores for every possible next at this position”.
This file is intentionally incomplete: no learned initialization, no , no batching. Its job is to make the architecture concrete before the PyTorch version turns it into something fast and trainable.
Recap
- The is a per- : linear → → linear. Wider hidden layer (usually 4× d_model).
- The block = sublayer + sublayer, both pre-norm + . Output has the same shape as input.
- A = N blocks in sequence + final + unembedding. Output is
[seq_len × vocab_size]. - Your local project now has
llm/model.py, the first GPT-shaped skeleton. - The block's invariant — input shape = output shape — is what lets us stack arbitrarily many copies. A modern model has dozens.
- Causal masking, positional encoding, real , are all one-line additions on top of this scaffold. The architectural skeleton is what you just wrote.
Going further
- Vaswani et al., "Attention Is All You Need" (2017). The full paper.
- Karpathy's "Let's build GPT from scratch" — builds the same model end to end in PyTorch.
- The Annotated Transformer — paper-by-line implementation with every modification spelled out.
- Step by Token, chapter 5 covers the from the understanding angle.
Next up: this is the end of part III. Part IV begins with prepare a dataset — your local project already exists, so now we feed it a real dataset.