Transformer Block from Scratch
One block is the unit of every modern decoder LLM: layer norm, multi head attention, residual, MLP, residual. Pre-LN trains stably without warmup, while the post-LN layout from the original paper does not. This lesson builds both side by side and explains which one survives a 12 layer stack at common learning rates (the demo runs six layers).
Learning Objectives
- Build a transformer block in PyTorch from the four moving pieces: LayerNorm, multi head causal attention, residual connections, position wise MLP.
- Place the LayerNorms in two configurations (pre-LN and post-LN) and explain why one trains stably without warmup.
- Implement causal masking inside the multi head attention so token `i` cannot see tokens `j > i`.
- Track gradient flow through both variants on a six layer demo stack and read the result, then reason about why it gets worse at twelve layers.
- Reuse the block as a drop-in unit when the next lesson assembles a 124 million parameter GPT.
The Problem
A transformer is one block repeated. Get the block wrong once, repeat it twelve times, and you ship a model that diverges in the first epoch or leans on warmup hacks for the rest of training. Both failure modes show up the first time someone stacks blocks naively. In one, the attention layer attends to the future. In the other, the LayerNorm sits where it cannot tame the residual signal at depth.
The fix is mechanical once you see it. The block has exactly two residual paths and exactly two normalization positions. Choose the positions correctly and the rest of the stack is bookkeeping.
The Concept
A decoder only transformer block is a function that takes a tensor of shape (batch, sequence, embedding) and returns a tensor of the same shape. Two sublayers do the work inside it.
This is the pre-LN variant. The LayerNorm sits inside the residual branch, before the sublayer, so the residual connection carries the unnormalized signal forward.
The post-LN variant moves the LayerNorm to after the residual add.
The shapes are identical, but training behavior is not. With post-LN, the gradient flowing back through the residual path has to pass through the LayerNorm. At depth twelve (the demo below runs six layers) and learning rate 3e-4, that gradient shrinks fast enough to need a warmup schedule. Pre-LN leaves the residual path unnormalized, so gradients propagate cleanly to the embedding layer. That is why GPT-2 onward ships pre-LN.
Causal multi head attention
The attention sublayer projects the input three ways into query, key, and value tensors. Each is reshaped from (B, T, D) to (B, H, T, D/H), where H is the head count. Scaled dot product attention computes softmax(Q K^T / sqrt(d_k)) per head. The upper triangle is masked to negative infinity before the softmax, and the result multiplies V. Heads are concatenated back into a single (B, T, D) tensor and projected once more. The mask is what makes the model causal. Forget it and you train a model that cheats.
The MLP
The position wise MLP applies the same two layer network to every token independently. The hidden width is four times the embedding width, the activation is GELU, and a dropout follows the second linear. Tokens do not talk to each other inside the MLP. All token mixing happens in attention.
Residual connections do two things
They make the gradient path additive across depth, which keeps the gradient norm in scale through twelve layers. They also let each block learn an additive update to the running representation instead of a full replacement. Both effects are why the block scales.
Build It
code/main.py implements:
class LayerNormwith learnable scale and shift, biased variance witheps=1e-5, applied per token vector.class MultiHeadAttentionwithnum_heads,head_dim = d_model // num_heads, a fused QKV projection, a registered causal mask, and attention and residual dropout.class FeedForwardwith two linear layers, GELU activation, and dropout.class TransformerBlockwith apre_lnflag that toggles between the two variants.- A demo that builds a 6 layer pre-LN stack and a 6 layer post-LN stack with identical inputs, then prints (a) the output shape and (b) the gradient norm at the embedding after one backward pass.
Run it:
python3 code/main.pyThe output is a shape check on both stacks and the gradient norms side by side. The pre-LN stack's embedding gradient is about 200 times larger than the post-LN stack's at the same learning rate (210x in the demo run). That gap is the empirical signal that pre-LN trains without warmup.
Stack
torchfor the tensor math, autograd, andnn.Moduleplumbing.- No
transformers, no pretrained weights. The block is built from primitives.
Production patterns in the wild
Three patterns turn the textbook block into something you can ship.
Fused QKV projection. Three separate linear layers cost three kernel launches and three matmuls. One linear layer of width 3 * d_model does the same work in a single launch, then splits the output along the last axis. The fused path is faster on every accelerator and matches what the reference implementations of GPT-2, LLaMA, and Mistral ship.
Registered causal mask buffer. The mask depends only on the maximum context length. Allocate it once at construction with register_buffer, slice the active window on each forward pass, and skip the per-call allocation. Forget this and the mask becomes an allocator hot spot at long context.
Dropout in three places, none on the residual stream. Dropout belongs after the attention softmax (attention dropout), after the attention output projection, and after the second linear of the MLP (the last two are residual dropout, applied to each sublayer's output before it is added back). A dropout on the residual stream itself breaks the additive identity that lets the gradient flow at depth. Some early implementations got this wrong and paid for it with brittle training.
Use It
- The block plugs straight into the GPT assembly in lesson 35 without modification.
- Pre-LN is what every modern open weights LLM uses. Post-LN is what the original 2017 attention paper used. Knowing both is enough to read any decoder architecture you will meet.
- Swap the GELU for SiLU and you have the LLaMA family activation. Swap the LayerNorm for RMSNorm and you have the LLaMA family normalization. The skeleton stays the same.
Exercises
- Set the existing
use_biasflag inBlockConfigtoFalse. Modern open weights LLMs ship without biases on the linear layers. Measure the parameter savings in a 12 layer 768 dim model. - Replace
nn.LayerNormwith a hand rolled RMSNorm and verify the output shape is unchanged. - Add a flag that returns the attention weights for the first head as a
(B, T, T)tensor. Plot the upper triangle to confirm it is zero after softmax. - Build a sanity check that feeds a
(2, 16, 384)tensor withH=6through both variants and asserts the forward outputs are different (for example,not torch.allclose) when weights are initialized identically and dropout is set to zero.
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Pre-LN | "Pre norm" | LayerNorm inside the residual branch, before each sublayer; the residual carries the unnormalized signal |
| Post-LN | "Post norm" | LayerNorm after the residual add; what the 2017 paper shipped and what needs warmup |
| Causal mask | "Triangle mask" | The upper triangle of the attention logits set to negative infinity so token i cannot read token j when j is greater than i |
| Fused QKV | "Combined projection" | One linear of width 3D instead of three linears of width D; one kernel, one matmul |
| Residual stream | "Skip connection" | The unnormalized tensor that flows top to bottom through every block; what each block adds to |
I'd keep pre_ln=True as the default and leave the flag in place, so the post-LN variant stays one toggle away. Running both stacks on identical inputs is the fastest way to see the gradient gap for yourself, and the demo does exactly that. I'd also keep dropout off the residual stream itself: it breaks the additive identity, and everything the block does at depth leans on that identity.
Further Reading
- Phase 7 lesson 02 (self attention from scratch) for the attention math underneath this block.
- Phase 7 lesson 05 (full transformer) for the encoder decoder version of the same skeleton.
- Phase 10 lesson 04 (pre training mini GPT) for the training procedure that this block plugs into.
- Phase 19 lesson 35 (this track) which stacks twelve of these blocks into a GPT model.
Code
- main.py (python, 253 lines)
- tests/test_block.py (python, 143 lines)