Skip to content
The loss curve

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:

  1. Multi-head — let every look at every other .
  2. 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:

FFN(x)=W2GELU(W1x+b1)+b2\text{FFN}(x) = W_2 \cdot \text{GELU}(W_1 x + b_1) + b_2

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:

x=x+attention(LayerNorm(x))x=x+FFN(LayerNorm(x))\begin{aligned} x' &= x + \text{attention}(\text{LayerNorm}(x)) \\ x'' &= x' + \text{FFN}(\text{LayerNorm}(x')) \end{aligned}

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 .

h=xhblocki(h)for i=0..N1hLayerNorm(h)logits=hWunembed\begin{aligned} h &= x \\ h &\leftarrow \text{block}_i(h) \quad \text{for } i = 0..N-1 \\ h &\leftarrow \text{LayerNorm}(h) \\ \text{logits} &= h \cdot W_{\text{unembed}} \end{aligned}

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 X as 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 sat from sat 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_forward applies 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] logits converts 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

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.