AI Grimoire
Sheet
statusstandard
difficultyintermediate
time
described2019
revisedtoday

Pre-Norm and Post-Norm

The most consequential one-line difference in the transformer. Put the norm inside the residual branch and the model trains without warmup; put it after the add and it trains better, if it trains at all.

Standing

Current standardPart of the default recipe. Present in most frontier models trained today, and the thing a new design departs from rather than argues for.

Pre-norm is effectively unanimous in models above a few billion parameters. The argument is settled; the variants below are attempts to recover what it gives up.

judged as of 2026-09 · what the labels mean

Theory

Two lines, differing in where one function call goes.

x+1=LN(x+F(x))x_{\ell+1} = \mathrm{LN}\bigl(x_\ell + F(x_\ell)\bigr)
eq. 1 — post-norm, as originally published
x+1=x+F(LN(x))x_{\ell+1} = x_\ell + F\bigl(\mathrm{LN}(x_\ell)\bigr)
eq. 2 — pre-norm, as everything is built now

In eq. 2 there is an unbroken identity path from the embedding to the output: xx passes through additions and nothing else. In eq. 1 every layer’s output goes through a normalisation, so the path from layer 1 to layer 50 traverses fifty non-linear rescalings.

Why post-norm needs warmup

Xiong et al. worked out the gradient at initialisation. For post-norm, the expected gradient norm at the parameters of the final layer is O(dlnd)O(d\sqrt{\ln d}) and does not shrink with depth — the early steps of training take enormous, badly-conditioned steps and the model diverges. For pre-norm the same quantity carries a 1/L1/\sqrt{L} factor, and depth reduces the gradient rather than leaving it unbounded.

Warmup costs more than the steps it consumes. It couples the schedule to the architecture, it interacts with the batch size, and it is one more thing to tune per run. Removing it is most of why pre-norm won.

What pre-norm gives up

The identity path is also the problem. Each block adds its output to a stream that is never rescaled, so the variance of xx_\ell grows roughly linearly with depth. Later blocks see an input whose norm is dominated by the accumulated sum, and their own contribution — normalised before FF, and therefore of bounded size — is a progressively smaller fraction of it.

Var[xL]=1LVar[F()]    FLxL1L\mathrm{Var}[x_L] \approx \sum_{\ell=1}^{L}\mathrm{Var}\bigl[F_\ell(\cdot)\bigr] \;\Longrightarrow\; \frac{\|F_L\|}{\|x_L\|} \sim \frac{1}{\sqrt{L}}
eq. 3 — the stream grows, the contribution does not

The observable consequence is that deep pre-norm models behave like shallower ones: the last layers change the representation less than the first, and at matched parameter count a post-norm model that does converge reaches slightly better loss. This is sometimes called representation collapse, and it is the motivation for every variant below.

Implementation

python · torch
from torch import Tensor, nn


class PreNormBlock(nn.Module):
    """The modern arrangement. Note the norms are inside the branches."""

    def __init__(self, dim: int, **kw):
        super().__init__()
        self.n1, self.n2 = nn.RMSNorm(dim), nn.RMSNorm(dim)
        self.attn, self.ffn = Attention(dim, **kw), FeedForward(dim)

    def forward(self, x: Tensor) -> Tensor:
        x = x + self.attn(self.n1(x))
        return x + self.ffn(self.n2(x))


class Stack(nn.Module):
    def __init__(self, dim: int, layers: int, **kw):
        super().__init__()
        self.blocks = nn.ModuleList(PreNormBlock(dim, **kw) for _ in range(layers))
        # Required: the residual stream itself is never normalised on its path.
        self.final = nn.RMSNorm(dim)

    def forward(self, x: Tensor) -> Tensor:
        for block in self.blocks:
            x = block(x)
        return self.final(x)

The compromises

Sandwich norm normalises both entering and leaving the branch — x+LN2(F(LN1(x)))x + \mathrm{LN}_2(F(\mathrm{LN}_1(x))) — which bounds each block’s contribution without touching the identity path. Gemma 2 ships this, and it costs one extra norm per branch.

Residual scaling multiplies each branch output by a constant, commonly 1/2L1/\sqrt{2L}, so the accumulated variance stays O(1)O(1) instead of O(L)O(L). GPT-2 did a version of this at initialisation rather than as a persistent factor.

DeepNorm goes the other way: keep post-norm, and pick the residual weight and the initialisation scale so that the update to each layer is bounded by construction. It is the only one of the three that recovers post-norm’s quality rather than patching pre-norm’s deficit, and it is the least used, because it requires getting two constants right as a function of depth.

Scale at layer L
O(√(ln d))
Pre-norm equivalent
O(√(ln d / L))
Warmup needed
post-norm only
Post-norm gradient at initialisation

Related

References

[1]Xiong et al. — On Layer Normalization in the Transformer Architecture (2020)arXiv:2002.04745
[2]Wang et al. — DeepNet: Scaling Transformers to 1,000 Layers (2022)arXiv:2203.00555
[3]Nguyen & Salazar — Transformers without Tears: Improving the Normalization of Self-Attention (2019)arXiv:1910.05895