Transformer & DL Foundations
IC4IC5

Normalization and Activations: RMSNorm, Pre-Norm, and the Gated FFN

The unglamorous plumbing — where you put the norm, whether you subtract the mean, and how you gate the MLP — is what actually lets a 100-layer transformer train without diverging.

15 min read · 14 sections
Prerequisites: /transformers/self-attention, /ml-foundations

1. Quick anchor

Attention gets the headlines, but the reason a modern transformer with 80+ layers trains at all is the boring scaffolding around it: normalization, residuals, and the FFN. A transformer block is x -> x + Attn(Norm(x)) -> x + FFN(Norm(x)). The residual gives gradients a clean highway from the loss all the way back to layer 0. Pre-norm (normalize inside the residual branch, never on the highway itself) is what keeps that highway un-distorted so you can stack depth. RMSNorm is LayerNorm with the mean-subtraction deleted — same stabilization, fewer ops. And the FFN does two-thirds of the parameter work in the model; the modern version is a gated MLP (SwiGLU) that multiplies two projections together for a learned, input-dependent gate. Get these four pieces right and the model converges; get them wrong and a 70-layer model NaNs in the first thousand steps.

2. Why interviewers probe this

  • IC4 — do you understand the mechanics? Can you write LayerNorm and RMSNorm from scratch, name the difference (mean term), and explain why a residual connection's "+1" in the gradient is the whole point? Can you state what an FFN block actually computes and why it's d_model -> 4·d_model -> d_model?
  • IC5 — do you understand the system tradeoffs? Why is pre-norm the default despite post-norm sometimes scoring higher? Why did the field move LayerNorm -> RMSNorm and ReLU -> SwiGLU — is it quality, speed, or both, and can you quantify it? You should be able to reason about activation memory, kernel fusion, and what breaks at 100 layers vs 12.
  • Both levels — taste. These choices are nearly free to get right and catastrophic to get wrong. Interviewers use them to check whether you've actually trained or debugged a large model, or just read about attention. A candidate who shrugs at "where do you put the norm?" has never watched a loss curve diverge.

3. Concept build-up

Beginner explainerNew here? The words first

The words first.

  • Normalization — rescaling a vector so its values have a controlled size (e.g. unit variance), so later layers see inputs in a stable range.
  • LayerNorm — normalize each token's feature vector to zero mean and unit variance, then apply a learned scale/shift.
  • RMSNorm — a cheaper normalization that divides by the root-mean-square only; it skips subtracting the mean.
  • Residual / skip connection — adding a layer's input back to its output (x + f(x)) so information and gradients can bypass the layer.
  • Pre-norm vs post-norm — whether you normalize before the sublayer (inside the skip) or after adding the skip back.
  • FFN / MLP block — the per-token two-layer neural net that follows attention; it expands the dimension, applies a nonlinearity, and contracts back.
  • Activation — the nonlinear function (ReLU, GELU, SiLU) that lets the network represent non-linear relationships.
  • Gating (GLU) — multiplying one projection of the input by a nonlinear "gate" computed from another projection, giving input-dependent control.

Step by step.

  1. A transformer block takes token vectors x.
  2. It normalizes them so each sublayer sees a stable input distribution.
  3. The sublayer (attention or FFN) transforms the normalized input.
  4. The result is added back to the original x (the residual).
  5. Stacking dozens of these blocks needs the residual + pre-norm so gradients survive the depth.
  6. The FFN inside each block holds most of the parameters and does most of the per-token "thinking."
  7. Modern FFNs gate the activation (SwiGLU) for a measurable quality bump.

Remember this: normalize inside the residual branch (pre-norm), keep the skip path clean, and let the gated FFN do the heavy lifting.

3.1 LayerNorm: the baseline

LayerNorm normalizes across the feature dimension of a single token (not across the batch — that's BatchNorm, which is brittle for variable-length sequences). For a token vector xRdx \in \mathbb{R}^d:

ƒ
LN(x)=γxμσ2+ϵ+β,μ=1di=1dxi,σ2=1di=1d(xiμ)2\text{LN}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta, \quad \mu = \frac{1}{d}\sum_{i=1}^{d} x_i, \quad \sigma^2 = \frac{1}{d}\sum_{i=1}^{d}(x_i - \mu)^2

Symbols: xix_i is feature ii of the token; μ\mu is the mean over the dd features; σ2\sigma^2 is the variance over those features; ϵ\epsilon (e.g. 10510^{-5}) prevents divide-by-zero; γ,βRd\gamma, \beta \in \mathbb{R}^d are learned scale and shift; \odot is element-wise multiply. The job: keep each token's activations at a controlled scale so the next sublayer never sees exploding or vanishing inputs, which stabilizes training and lets you use higher learning rates.

3.2 RMSNorm: delete the mean

RMSNorm (Zhang & Sennrich, 2019) makes one observation: most of LayerNorm's benefit comes from the rescaling (dividing by a measure of magnitude), not the re-centering (subtracting the mean). So drop μ\mu entirely:

ƒ
RMS(x)=γx1di=1dxi2+ϵ\text{RMS}(x) = \gamma \odot \frac{x}{\sqrt{\frac{1}{d}\sum_{i=1}^{d} x_i^2 + \epsilon}}

Here the denominator is the root-mean-square of the features. No μ\mu, no β\beta (typically). Why is this safe? A mechanistic finding is that models trained with RMSNorm spontaneously learn representations roughly orthogonal to the uniform (all-ones) vector — and LayerNorm's mean-subtraction is exactly the projection that removes the component along that uniform vector. If the model already avoids putting signal there, subtracting the mean is removing something close to zero: redundant work. Empirically RMSNorm is 7–64% faster than LayerNorm (the range depends on shape and hardware) with no meaningful quality loss — verified across LLaMA, OLMo, and others. That's why it's ubiquitous in 2024–2026 LLMs.

The savings: you skip computing μ\mu, skip the subtraction broadcast, and skip the β\beta add. On a memory-bandwidth-bound norm kernel, fewer passes over the activation tensor matters more than the FLOP count.

RMSNorm vs LayerNorm — on real numbers

Take a tiny token vector x = [2, -2, 4, 0], so d = 4. Set gamma = [1,1,1,1], eps ≈ 0.

LayerNorm path:

  • mean mu = (2 - 2 + 4 + 0)/4 = 1
  • centered x - mu = [1, -3, 3, -1]
  • variance sigma^2 = (1 + 9 + 9 + 1)/4 = 5, so sqrt = 2.236
  • output = [1, -3, 3, -1]/2.236 = [0.447, -1.342, 1.342, -0.447]

RMSNorm path:

  • mean of squares = (4 + 4 + 16 + 0)/4 = 6, so rms = sqrt(6) = 2.449
  • output = [2, -2, 4, 0]/2.449 = [0.816, -0.816, 1.633, 0]

What it did: both rescaled the vector to a unit-ish magnitude. LayerNorm first shifted everything so the mean became 0 (note its outputs sum to 0); RMSNorm skipped that shift, did one fewer pass over the data, and produced a slightly different but equally well-scaled vector. The model learns gamma to compensate for the difference.

3.3 Residual connections: why depth is possible at all

The residual (He et al., 2015) is x_{l+1} = x_l + f(x_l). The magic is in the backward pass. Differentiate:

ƒ
Lxl=Lxl+1(1+fxl)\frac{\partial \mathcal{L}}{\partial x_l} = \frac{\partial \mathcal{L}}{\partial x_{l+1}} \cdot \left(1 + \frac{\partial f}{\partial x_l}\right)

That +1 is everything. In a plain deep network, the gradient at layer ll is a long product of Jacobians, lflxl\prod_l \frac{\partial f_l}{\partial x_l}; if each factor has magnitude < 1, the product decays exponentially with depth and early layers get no signal (vanishing gradients). The residual replaces each factor with (1 + ∂f/∂x_l). Even if the sublayer's Jacobian fxl0\frac{\partial f}{\partial x_l} \to 0, the gradient still flows through the +1 term — an identity highway from the loss back to layer 0. This is the single reason you can stack 80 transformer blocks instead of 6.

A second view: a residual stack computes an additive refinement. Each block reads the residual stream, writes a small update, and adds it. The stream is a shared scratchpad; layers compose by accumulation, not by repeated multiplication. This framing (the "residual stream") is also how mechanistic interpretability reasons about transformers.

3.4 Pre-norm vs post-norm: where you put the norm decides whether you can go deep

The original transformer (Vaswani et al., 2017) used post-norm: x_{l+1} = LN(x_l + Sublayer(x_l)). The norm sits on the residual highway, after the add. This is a problem: the clean identity path is now wrapped in a normalization at every layer, so the gradient highway is repeatedly rescaled and the effective signal can blow up or shrink as depth grows. Post-norm transformers need careful learning-rate warmup and are finicky past a couple dozen layers.

Pre-norm (Xiong et al., 2020) moves the norm inside the residual branch:

ƒ
xl+1=xl+Sublayer(Norm(xl))x_{l+1} = x_l + \text{Sublayer}(\text{Norm}(x_l))

Now the skip path x_l + ... is a pure identity — nothing touches it. The norm only conditions the input that flows into the sublayer. Gradient flow analysis shows pre-norm keeps gradient magnitudes well-behaved across depth without warmup, which is why every large modern LLM (LLaMA, Qwen, Gemma, GPT-style) is pre-norm. The tradeoff: pre-norm lets the residual stream grow in magnitude with depth (each block adds to it, and nothing renormalizes the stream itself), so the final layers' relative contributions shrink and you typically add one final norm before the output head. Post-norm, by contrast, has been reported to give slightly better results in some narrow regimes (e.g. a notable BLEU gain in certain machine-translation setups) precisely because it does renormalize the stream — but it pays for that with training instability you have to fight. At staff scale the verdict is settled: pre-norm, plus a final norm, plus sometimes tricks like scaling residual branches by 1/2N1/\sqrt{2N} to tame the growing stream.

3.5 Activations: ReLU -> GELU -> SiLU -> gated GLU

The FFN's nonlinearity has drifted over time:

  • ReLU: max(0,x)\max(0, x). Cheap, but a hard zero kills gradients for negative inputs and the kink is non-smooth.
  • GELU (Hendrycks & Gimpel, 2016): GELU(x)=xΦ(x)\text{GELU}(x) = x \cdot \Phi(x), where Φ\Phi is the standard-normal CDF. A smooth, probabilistic gate — it scales each input by the probability that a standard Gaussian is below it. Smooth gradients everywhere; used in BERT, GPT-2/3.
  • SiLU / Swish: SiLU(x)=xσ(x)\text{SiLU}(x) = x \cdot \sigma(x) with σ\sigma the logistic sigmoid. Very close to GELU, slightly cheaper; the base unit inside SwiGLU.

The bigger leap is gating (GLU variants, Shazeer 2020). Instead of one projection through a nonlinearity, split into two projections and multiply:

ƒ
SwiGLU(x)=(SiLU(xWg))(xWv)\text{SwiGLU}(x) = \big(\text{SiLU}(xW_g)\big) \odot (xW_v)

WgW_g produces the gate, WvW_v produces the value; the gate (squashed by SiLU) decides, per dimension and per token, how much of the value passes. GeGLU is identical with GELU instead of SiLU. Because gating consumes the hidden dimension across two matrices, implementations shrink the hidden width to 234dmodel83dmodel\frac{2}{3} \cdot 4 d_{model} \approx \frac{8}{3} d_{model} to keep parameter count matched to a standard FFN. The payoff is consistently lower perplexity and better downstream scores at equal parameters — small but reliable, which is why SwiGLU/GeGLU is standard in LLaMA, OLMo, Gemma. The cost is one extra matmul (three weight matrices instead of two: Wg,Wv,WoutW_g, W_v, W_{out}).

3.6 The FFN block itself

The FFN (a.k.a. MLP) is applied independently to every token position — no mixing across positions (attention already did that). A standard FFN:

ƒ
FFN(x)=σ(xW1+b1)W2+b2,W1Rd×4d,  W2R4d×d\text{FFN}(x) = \sigma(xW_1 + b_1)W_2 + b_2, \quad W_1 \in \mathbb{R}^{d \times 4d}, \; W_2 \in \mathbb{R}^{4d \times d}

It expands d4dd \to 4d, applies the nonlinearity, contracts back. This is where roughly two-thirds of a transformer's parameters live (the 4×4\times expansion dominates attention's projections). Interpretively, the FFN acts as a per-token key-value memory: W1W_1 rows detect features, W2W_2 columns write associated outputs into the residual stream. The gated version replaces σ(xW1)\sigma(xW_1) with the SwiGLU expression above.

4. Minimal implementation

A complete, runnable pre-norm transformer block with RMSNorm and a SwiGLU FFN — production-shaped, the way LLaMA-family models are built.

import torch
import torch.nn as nn
import torch.nn.functional as F
 
class RMSNorm(nn.Module):
    def __init__(self, dim: int, eps: float = 1e-6):
        super().__init__()
        self.eps = eps
        self.gamma = nn.Parameter(torch.ones(dim))  # learned scale; no beta, no mean
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # rms over the feature dim; upcast to fp32 for numerical stability
        norm = x.float().pow(2).mean(dim=-1, keepdim=True).add(self.eps).rsqrt()
        return (x.float() * norm).type_as(x) * self.gamma
 
class SwiGLU(nn.Module):
    """Gated FFN. Hidden width shrunk to ~8/3*d to match a standard 4*d FFN's params."""
    def __init__(self, dim: int, hidden: int | None = None):
        super().__init__()
        hidden = hidden or int(8 * dim / 3)
        hidden = 256 * ((hidden + 255) // 256)  # round to a hardware-friendly multiple
        self.w_gate = nn.Linear(dim, hidden, bias=False)
        self.w_value = nn.Linear(dim, hidden, bias=False)
        self.w_out = nn.Linear(hidden, dim, bias=False)
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.w_out(F.silu(self.w_gate(x)) * self.w_value(x))
 
class Block(nn.Module):
    def __init__(self, dim: int, n_heads: int):
        super().__init__()
        self.attn_norm = RMSNorm(dim)
        self.ffn_norm = RMSNorm(dim)
        self.attn = nn.MultiheadAttention(dim, n_heads, batch_first=True)
        self.ffn = SwiGLU(dim)
 
    def forward(self, x: torch.Tensor, attn_mask=None) -> torch.Tensor:
        # PRE-NORM: norm is inside the residual branch; the skip path is a clean identity.
        h = self.attn_norm(x)
        x = x + self.attn(h, h, h, attn_mask=attn_mask, need_weights=False)[0]
        x = x + self.ffn(self.ffn_norm(x))
        return x
 
if __name__ == "__main__":
    blk = Block(dim=512, n_heads=8)
    tok = torch.randn(2, 16, 512)           # (batch, seq, dim)
    out = blk(tok)
    print(out.shape)                        # torch.Size([2, 16, 512])
    # sanity: a fresh block is near-identity, so output ≈ input (residual highway)
    print((out - tok).abs().mean().item())  # small number

Three things to notice. First, the upcast to fp32 inside RMSNorm — the squaring can overflow or lose precision in bf16, so production code computes the norm in fp32 then casts back; getting this wrong is a real source of training instability. Second, the clean residual (x = x + sublayer(norm(x))) — the norm never touches the x on the left of the +. Third, rounding the hidden dim to a multiple of 256 so the matmuls hit fast tensor-core paths; the "8/3·d" is theoretical, hardware wants alignment.

5. Production tradeoffs

Choice Cost / latency Quality Failure mode What changes at scale
LayerNorm Baseline; 2 passes over activations + mean/var Baseline Generally safe Bandwidth-bound; mean term is wasted ops
RMSNorm 7–64% faster norm; fewer memory passes Parity with LN fp32 upcast needed or bf16 instability Free win; default everywhere
Post-norm Same FLOPs as pre-norm Slightly better in narrow regimes Diverges past ~24 layers without warmup Unusable at 80+ layers unmodified
Pre-norm Same FLOPs; +1 final norm Slightly softer final activations Residual stream magnitude grows with depth Standard; pair with final norm
ReLU/GELU FFN 2 matmuls Baseline None Simple, but leaves quality on the table
SwiGLU/GeGLU 3 matmuls (+~50% FFN compute) Reliably lower perplexity Param accounting if you forget the 2/3 shrink Standard in modern LLMs

Cost. The FFN is ~two-thirds of the model's parameters and FLOPs, so the SwiGLU "third matmul" is not free — it's the dominant compute in the block. You recover the parameter count by shrinking hidden width to ≈8/3·d, but you still pay one extra matmul of activation read/writes. At inference this shows up as FFN being the throughput bottleneck, which is why MoE (sparse FFNs) is the next lever: route each token to a few experts so you scale parameters without scaling per-token FFN FLOPs.

Latency. Norm kernels are memory-bandwidth bound, not compute bound — they read and write the whole activation tensor and do little arithmetic. That's why RMSNorm's win is about fewer passes over memory, and why production stacks fuse norm + the following matmul into one kernel to avoid a round-trip to HBM.

Quality and failure modes. The catastrophic failures are at training time: post-norm + no warmup at depth -> divergence; RMSNorm in bf16 without fp32 upcast -> slow instability; forgetting the final norm in a pre-norm model -> a poorly-calibrated output head. None of these show up in a 12-layer toy model — they emerge at scale, which is exactly why interviewers ask.

What changes at scale. Deep pre-norm models grow the residual stream's magnitude with depth; large training runs counter this with branch-scaling (e.g. dividing each sublayer output by 2N\sqrt{2N} for NN layers) or with careful initialization so the stream stays in range for the final norm.

6. How it's asked

[IC4] Write RMSNorm and tell me what it saves over LayerNorm. RMSNorm divides each token vector by its root-mean-square (x / sqrt(mean(x^2) + eps) * gamma) and skips two things LayerNorm does: subtracting the mean and adding the learned bias beta. The saving is fewer passes over the activation tensor on a bandwidth-bound kernel, giving 7–64% faster norm with no quality loss — because models learn representations roughly orthogonal to the uniform vector, so subtracting the mean removes a near-zero component. You must compute the squared mean in fp32 to avoid bf16 instability.
[IC4] Why does a residual connection stop gradients from vanishing? Because x_{l+1} = x_l + f(x_l) makes the backward pass ∂L/∂x_l = ∂L/∂x_{l+1} · (1 + ∂f/∂x_l). The +1 is an identity term that survives even when the sublayer's Jacobian goes to zero, so the gradient flows back through every layer via a clean highway instead of being a long product of sub-1 factors that decays exponentially with depth. That's what lets you train 80 layers instead of 6.
[IC4] What is the FFN doing and why is it 4× wide? It's a per-token two-layer MLP applied independently at each position — attention mixes across positions, the FFN does per-token feature computation. The d -> 4d -> d expansion gives it capacity to act as a key-value memory: the first matrix detects features, the second writes associated outputs into the residual stream. It holds roughly two-thirds of the model's parameters, so it dominates compute.
[IC5] Pre-norm is the default but post-norm sometimes scores higher. Explain and tell me what you'd do at 100 layers. Post-norm normalizes on the residual highway (LN(x + sublayer(x))), so it renormalizes the stream every layer — which can help final quality (a documented BLEU gain in some MT setups) but repeatedly distorts the gradient highway, causing divergence at depth without heavy warmup. Pre-norm normalizes inside the branch (x + sublayer(LN(x))), keeping the skip a pure identity and gradients well-behaved, at the cost of a residual stream whose magnitude grows with depth. At 100 layers I'd use pre-norm with a final norm before the head, plus residual branch-scaling (e.g. 1/√(2N)) or careful init to keep the growing stream in range — getting the stability for free and recovering most of post-norm's calibration benefit.
[IC5] Why SwiGLU over plain GELU, and how do you keep the parameter count fair? SwiGLU multiplies a value projection by a SiLU-gated projection (SiLU(xW_g) ⊗ xW_v), giving an input-dependent, per-dimension gate instead of a fixed pointwise nonlinearity — empirically a reliable perplexity/downstream improvement. It uses three weight matrices instead of two, so to match a standard 4d FFN's parameter count you shrink the hidden width to ≈8/3·d (then round to a hardware-friendly multiple like 256). The cost is one extra matmul's worth of activation traffic, paid in the block's dominant compute, which is why it's a deliberate quality-for-compute trade that the field decided is worth it.

7. Pitfalls & flashcards

  • bf16 RMSNorm without fp32 upcast — the x^2 sum loses precision; compute the norm in fp32, cast back. Silent training instability otherwise.
  • Forgetting the final norm in pre-norm models — the residual stream grows with depth; without a norm before the output head, logits are poorly scaled.
  • Putting the norm on the residual (post-norm) at depth — works in toy models, diverges at scale without aggressive warmup.
  • SwiGLU parameter blowup — three matrices not two; if you don't shrink hidden width to ≈8/3·d you've silently grown the model ~33%.
  • Confusing LayerNorm with BatchNorm — LayerNorm normalizes across features of one token; BatchNorm across the batch, which is fragile for variable-length sequences and is not what transformers use.
  • Assuming RMSNorm is a quality upgrade — it's a speed upgrade at quality parity, not a quality win. The win is cheaper, not better.

Flashcard. Pre-norm = x + Sublayer(Norm(x)) (clean skip, trains deep). Post-norm = Norm(x + Sublayer(x)) (renormalizes stream, diverges deep). RMSNorm = LayerNorm minus the mean and beta. Residual's gradient +1 is why depth works. FFN = ~2/3 of params; SwiGLU gates it for reliable perplexity gains at ~3 matmuls.

8. Further reading

Next: Positional encodings — RoPE, ALiBi, and length extrapolation.

Primary sources
← More in Transformer & DL Foundations