Layer Normalization › Gradient Flow Through Layer Normalization

Michael BrenndoerferUpdated June 11, 20255 min read
Language AI HandbookAll chapters
Part 15 of 67Transformer Blocks
Chapter 104Layer Normalization › Gradient Flow Through Layer Normalization
Reading section 8 of 16 · Gradient Flow Through Layer Normalization · 5 minSection 8 of 16 · 5 min

Book contents

Section 8 of 16 · 5 min read

Layer Normalization › Gradient Flow Through Layer NormalizationOne section · Sections in this chapter8 / 16 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Understanding how gradients flow through layer normalization is essential for debugging training issues and understanding why normalization stabilizes training.

The backward pass through layer normalization is more complex than a simple element-wise operation because each output depends on all inputs through the mean and variance computation. When we change a single input xix_i, it affects its own normalized output, the mean μ\mu, and the variance σ2\sigma^2, which in turn affects every output element. This coupling makes the gradient computation more involved.

Given the loss LL and the upstream gradient ∂L∂y\frac{\partial L}{\partial y} (the gradient flowing back from later layers), we need to compute three gradients: ∂L∂x\frac{\partial L}{\partial x} for backpropagation, and ∂L∂γ\frac{\partial L}{\partial \gamma} and ∂L∂β\frac{\partial L}{\partial \beta} for updating the learnable parameters.

Gradients for Learnable Parameters

The output of layer normalization is yi=γi⋅x^i+βiy_i = \gamma_i \cdot \hat{x}_i + \beta_i, where x^i\hat{x}_i is the normalized input. Since this is a simple affine transformation, the gradients follow directly from the chain rule.

For the scale parameter γi\gamma_i:

∂L∂γi=∑n∂L∂yn,i⋅x^n,i\frac{\partial L}{\partial \gamma_i} = \sum_{n} \frac{\partial L}{\partial y_{n,i}} \cdot \hat{x}_{n,i}

where:

  • ∂L∂γi\frac{\partial L}{\partial \gamma_i}: the gradient of the loss with respect to the ii-th scale parameter
  • nn: an index over all tokens (across batch and sequence dimensions)
  • ∂L∂yn,i\frac{\partial L}{\partial y_{n,i}}: the upstream gradient for the ii-th feature of the nn-th token
  • x^n,i\hat{x}_{n,i}: the normalized value of the ii-th feature for the nn-th token

Intuitively, this sums up how much each token's normalized value contributed to the loss through this scale parameter.

For the shift parameter βi\beta_i:

∂L∂βi=∑n∂L∂yn,i\frac{\partial L}{\partial \beta_i} = \sum_{n} \frac{\partial L}{\partial y_{n,i}}

where:

  • ∂L∂βi\frac{\partial L}{\partial \beta_i}: the gradient of the loss with respect to the ii-th shift parameter

This is simply the sum of upstream gradients, since βi\beta_i adds directly to the output.

Gradient for Input

The gradient with respect to input is more involved because x^i\hat{x}_i depends on xix_i in three ways: directly, through the mean μ\mu, and through the variance σ2\sigma^2. Applying the chain rule carefully yields:

∂L∂xi=γiσ(∂L∂yi−1d∑j=1d∂L∂yj−x^id∑j=1d∂L∂yjx^j)\frac{\partial L}{\partial x_i} = \frac{\gamma_i}{\sigma} \left( \frac{\partial L}{\partial y_i} - \frac{1}{d}\sum_{j=1}^{d}\frac{\partial L}{\partial y_j} - \frac{\hat{x}_i}{d}\sum_{j=1}^{d}\frac{\partial L}{\partial y_j}\hat{x}_j \right)

where:

  • ∂L∂xi\frac{\partial L}{\partial x_i}: the gradient of the loss with respect to the ii-th input element
  • γi\gamma_i: the learned scale parameter for the ii-th feature
  • σ=σ2+ϵ\sigma = \sqrt{\sigma^2 + \epsilon}: the standard deviation (with epsilon for stability)
  • dd: the feature dimension (number of elements in the input vector)
  • x^i\hat{x}_i: the normalized input, equal to (xi−μ)/σ(x_i - \mu) / \sigma

Let's break down the three terms inside the parentheses:

  1. Direct contribution ∂L∂yi\frac{\partial L}{\partial y_i}: The gradient that would flow if normalization were a simple scaling operation.

  2. Mean correction −1d∑j=1d∂L∂yj-\frac{1}{d}\sum_{j=1}^{d}\frac{\partial L}{\partial y_j}: Accounts for how changing xix_i affects μ\mu, which affects all outputs. This term subtracts the average gradient, centering the gradient distribution.

  3. Variance correction −x^id∑j=1d∂L∂yjx^j-\frac{\hat{x}_i}{d}\sum_{j=1}^{d}\frac{\partial L}{\partial y_j}\hat{x}_j: Accounts for how changing xix_i affects σ2\sigma^2, which scales all outputs. This term is proportional to the normalized value x^i\hat{x}_i, meaning inputs far from the mean get larger corrections.

This formula reveals something important: the gradient for each input element depends on the gradients of all other elements through the mean and variance terms. This coupling helps distribute gradient information across features, which can improve training stability.

The mean correction term subtracts a constant from every gradient. The effect is similar to centering the gradients, preventing them from all shifting in the same direction. If the loss pushes all features to increase, the mean correction tempers this. This ensures the relative ordering of features matters more than their absolute magnitude. This is analogous to how the forward pass removes the mean from the input; the backward pass removes the mean from the gradient.

The variance correction term is more subtle. It is proportional to x^i\hat{x}_i, the normalized value at position ii. Features that are far from the normalized mean (large ∣x^i∣|\hat{x}_i|) receive a larger variance correction. This makes sense: those features have a larger influence on the variance computation, so their gradient needs a larger adjustment to account for that influence. In practice, this term prevents individual features from receiving gradients that are systematically larger simply because they happen to have larger normalized values.

In[20]:
Code
def layer_norm_backward(dout, cache):
    """
    Layer normalization backward pass.

    Args:
        dout: Upstream gradient of shape (batch, seq_len, hidden_dim)
        cache: Values from forward pass

    Returns:
        dx: Gradient with respect to input
        dgamma: Gradient with respect to scale
        dbeta: Gradient with respect to shift
    """
    x, x_norm, mu, var, gamma, eps = cache
    d = x.shape[-1]

    # Gradients for learnable parameters (sum over batch and sequence)
    dgamma = (dout * x_norm).sum(dim=(0, 1))
    dbeta = dout.sum(dim=(0, 1))

    # Gradient for normalized input
    dx_norm = dout * gamma

    # Gradient for input (the complex part)
    std = torch.sqrt(var + eps)

    # Three terms in the gradient
    term1 = dx_norm / std
    term2 = dx_norm.mean(dim=-1, keepdim=True) / std
    term3 = (dx_norm * x_norm).mean(dim=-1, keepdim=True) * x_norm / std

    dx = term1 - term2 - term3

    return dx, dgamma, dbeta
In[21]:
Code
# Verify against PyTorch autograd
x_test = torch.randn(2, 4, 8, requires_grad=True)
gamma_test = torch.ones(8, requires_grad=True)
beta_test = torch.zeros(8, requires_grad=True)

# Forward pass with our implementation
output, cache = layer_norm_forward(x_test, gamma_test, beta_test)

# Create fake upstream gradient
dout = torch.randn_like(output)

# Our backward pass
dx_ours, dgamma_ours, dbeta_ours = layer_norm_backward(dout, cache)

# PyTorch autograd backward
output.backward(dout)
Out[22]:
Console
Gradient comparison with PyTorch autograd:
  dx max difference: 7.15e-07
  dgamma max difference: 0.00e+00
  dbeta max difference: 0.00e+00

The differences are on the order of 10−710^{-7} or smaller, well within floating-point precision. This confirms our manual backward pass implementation correctly computes the gradients that PyTorch's autograd produces automatically.