The transformer block
Assemble multi-head attention, position-wise FFN, residual connections, and layer norm into one transformer block, and trace the tensor shapes through a full forward pass.
Multi-head attention mixes information across positions. But attention is a weighted average of values -- it is linear in the values, and a model made entirely of attention heads is just a large linear transformation. Two additions turn this into a deep nonlinear model: a position-wise feed-forward network and residual connections with layer normalization[1].
Position-wise FFN
After attention, each token's representation is updated by a small MLP applied independently to each position. For input :
where and , with typically.
The FFN is "position-wise" because the same weights are shared across all positions, but applied independently -- no cross-position mixing happens here. Modern variants replace ReLU with SwiGLU or GELU: , which empirically trains better.
Residual connections
Residual connections (He et al., 2016) add the block's input directly to its output:
This creates a direct gradient path from the loss to early layers, mitigating the vanishing-gradient problem in deep networks. It also gives the model an easy way to learn "do nothing" (output zero from the sublayer).
Layer normalization
Layer norm (Ba et al., 2016) normalizes across the feature dimension (not the batch dimension like BatchNorm):
where and are the mean and variance computed over the features of a single token, and are learned affine parameters.
Layer norm stabilizes training across batch sizes and sequence lengths. It is computed per-token, making it independent of other tokens in the batch.
Pre-norm vs post-norm: the original Transformer uses post-norm (). Modern LLMs use pre-norm (), which stabilizes training at scale without learning-rate warmup.
The full transformer block
With pre-norm:
Tracing tensor shapes
For , , , sequence length :
| Tensor | Shape |
|---|---|
| Input | |
| (per head) | each |
| Attention output per head | |
| Concatenated heads | |
| After | |
| After residual + LN | |
| FFN intermediate | |
| FFN output | |
| Block output |
The sequence length dimension is never changed by any operation -- the block is shape-preserving.
Implementation
import numpy as np
def layer_norm(x, gamma, beta, eps=1e-5):
mu = x.mean(axis=-1, keepdims=True)
var = x.var(axis=-1, keepdims=True)
return gamma * (x - mu) / np.sqrt(var + eps) + beta
def gelu(x):
return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))
class TransformerBlock:
def __init__(self, d_model, n_heads, d_ff, seed=0):
rng = np.random.default_rng(seed)
s = 0.02
self.mha = None # placeholder -- use the class from the previous lesson
self.W1 = rng.normal(0, s, (d_ff, d_model))
self.b1 = np.zeros(d_ff)
self.W2 = rng.normal(0, s, (d_model, d_ff))
self.b2 = np.zeros(d_model)
self.gamma1 = np.ones(d_model); self.beta1 = np.zeros(d_model)
self.gamma2 = np.ones(d_model); self.beta2 = np.zeros(d_model)
def ffn(self, x):
return gelu(x @ self.W1.T + self.b1) @ self.W2.T + self.b2
def forward(self, x, mha_out):
"""Pre-norm transformer block. mha_out already computed externally."""
# Attention sublayer
x_normed = layer_norm(x, self.gamma1, self.beta1)
x = x + mha_out # residual (pre-norm: mha_out = MHA(x_normed))
# FFN sublayer
x_normed = layer_norm(x, self.gamma2, self.beta2)
x = x + self.ffn(x_normed)
return x
# Verify shapes
d_model, n_heads, d_ff, T = 32, 4, 128, 6
rng = np.random.default_rng(0)
x = rng.normal(0, 1, (T, d_model))
# Fake MHA output for demonstration
mha_out = rng.normal(0, 0.1, (T, d_model))
block = TransformerBlock(d_model, n_heads, d_ff)
y = block.forward(x, mha_out)
print("Input shape: ", x.shape)
print("Output shape:", y.shape)
print("Max absolute change:", np.abs(y - x).max().round(4))Parameter count of one block
| Component | Parameters |
|---|---|
| MHA () | |
| LN1 () | |
| FFN () | |
| LN2 () |
For GPT-2 (, , 12 blocks): M parameters. For GPT-3 (, , 96 blocks): B parameters.
Exercises
Exercise 1. For and , count the FFN parameters (). Then argue from the pre-norm formula why the block is shape-preserving.
Solution
Step 1. is and is , so the two weight matrices hold parameters. The biases add :
Step 2. Trace the shapes. keeps ; MHA maps to through ; the residual adds two tensors. The FFN sends up to and brings it back to , and its residual adds same-shaped tensors. Every sublayer outputs , so the block does too. No operation touches the sequence axis , so the block is shape-preserving.
Exercise 2. Compute layer norm of with , , by hand, and confirm the result has zero mean and unit variance.
Solution
Step 1. The mean over the four features is and the variance is
so .
Step 2. Normalize each feature as :
Step 3. The four outputs sum to , so the mean is . Their squares are , and dividing by gives variance . With and the affine step leaves this unchanged.
Exercise 3 (code). Implement layer norm and confirm it produces zero mean and unit variance per token, and that it is invariant to a constant shift of the input (adding a constant to every feature of a token leaves the output unchanged).
Solution
import numpy as np
def layer_norm(x, gamma, beta, eps=1e-5):
mu = x.mean(axis=-1, keepdims=True)
var = x.var(axis=-1, keepdims=True)
return gamma * (x - mu) / np.sqrt(var + eps) + beta
rng = np.random.default_rng(0)
T, d = 4, 16
x = rng.normal(3.0, 2.0, (T, d)) # nonzero mean, nonunit variance
g, b = np.ones(d), np.zeros(d)
y = layer_norm(x, g, b, eps=0.0)
assert np.allclose(y.mean(axis=-1), 0.0, atol=1e-6) # zero mean per token
assert np.allclose(y.var(axis=-1), 1.0, atol=1e-6) # unit variance per token
y_shift = layer_norm(x + 7.0, g, b, eps=0.0) # shift every feature
assert np.allclose(y, y_shift, atol=1e-6) # subtracting mu cancels it
print("per-token mean 0, variance 1, shift-invariant")Subtracting the per-token mean removes any constant offset before scaling, so layer norm sees only the shape of each token's feature vector, not its absolute level.
The next lesson adds positional encodings, which tell the model where in the sequence each token sits -- since attention itself is permutation-equivariant.
References
- [1]Vaswani, A., et al. (2017). "Attention is all you need". NeurIPS. arXiv:1706.03762↩