RMSNorm › Gradient Flow Through RMSNorm
Language AI HandbookAll chapters RMSNorm › Gradient Flow Through RMSNormOne section · Sections in this chapter8 / 15 ↓
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 the backward pass helps us see why RMSNorm is computationally cheaper and how gradients propagate through the network. The gradient computation for RMSNorm is simpler than LayerNorm because it doesn't need to backpropagate through the mean computation. Deriving this backward pass from scratch is also a useful exercise in applying the chain rule to shared computations.
The core challenge in computing gradients for normalization layers is that every output element depends on every input element through the shared normalization constant. When you change input element , you change output element directly and also change the RMS value, which affects all output elements. This coupling through the shared denominator means the gradient of the loss with respect to has two terms: a direct term and a distributed term.
Consider the forward pass where each output element is computed as:
where:
- : the -th element of the output
- : the -th learnable scale parameter
- : the -th element of the input
- : the root mean square computed over all input elements, shared by all output elements
To compute gradients, we need to determine how each input element affects each output element . This requires the partial derivative:
For notational convenience, let . We first need the derivative of with respect to . Using the chain rule on the square root and sum:
where:
- : how much the RMS changes when changes
- : the -th input element (the one we're differentiating with respect to)
- : the dimension of the input vector
- : the RMS value (appears in the denominator because of the square root derivative)
Now we apply the quotient rule to . The quotient rule states that :
where:
- : the Kronecker delta, equal to 1 if and 0 otherwise
- The first term : the direct effect when (changing directly changes the numerator )
- The second term : the indirect effect through the RMS in the denominator (changing any affects the RMS, which affects all outputs)
To compute the gradient of a loss with respect to input , we apply the chain rule, summing over all output elements:
The Kronecker delta selects only the term from the sum for the first part, giving us:
where:
- : the upstream gradient for output element
- The first term: the direct gradient path from through
- The second term: the indirect gradient path where affects all outputs via the shared RMS denominator
- The sum : aggregates the indirect effects across all output dimensions, and can be computed once and reused for all
The key efficiency advantage is that the sum can be computed once and reused for all . This makes the backward pass in complexity, the same as the forward pass. For LayerNorm, the backward pass is more complex because it must also propagate through the mean subtraction, leading to additional terms in the gradient expression.
def rmsnorm_backward(dout, x, gamma, eps=1e-6):
"""
Backward pass for RMSNorm.
Args:
dout: Upstream gradient, shape (..., d)
x: Original input, shape (..., d)
gamma: Scale parameter, shape (d,)
eps: Numerical stability constant
Returns:
dx: Gradient with respect to input
dgamma: Gradient with respect to scale
"""
# Compute RMS from forward pass
mean_sq = np.mean(x**2, axis=-1, keepdims=True)
rms = np.sqrt(mean_sq + eps)
# Normalized input
x_norm = x / rms
# Gradient for gamma
dgamma = np.sum(dout * x_norm, axis=tuple(range(dout.ndim - 1)))
# Gradient for x
d = x.shape[-1]
# Term 1: direct gradient through division
dx_norm = dout * gamma / rms
# Term 2: gradient through RMS (chain rule)
# d(1/rms)/dx_j = -x_j / (d * rms^3)
sum_term = np.sum(dout * gamma * x, axis=-1, keepdims=True)
dx_rms = -sum_term * x / (d * rms**3)
dx = dx_norm + dx_rms
return dx, dgamma# Verify gradients numerically
def numerical_gradient(f, x, h=1e-5):
"""Compute numerical gradient using central difference."""
grad = np.zeros_like(x)
it = np.nditer(x, flags=["multi_index"], op_flags=["readwrite"])
while not it.finished:
idx = it.multi_index
old_val = x[idx]
x[idx] = old_val + h
fxph = f(x.copy())
x[idx] = old_val - h
fxmh = f(x.copy())
grad[idx] = (fxph - fxmh) / (2 * h)
x[idx] = old_val
it.iternext()
return grad
# Test setup
d_test = 8
x_test = np.random.randn(2, d_test)
gamma_test = np.random.randn(d_test)
dout_test = np.random.randn(2, d_test)
def forward_scalar(x):
"""Forward pass returning scalar loss."""
rms = np.sqrt(np.mean(x**2, axis=-1, keepdims=True) + 1e-6)
y = gamma_test * (x / rms)
return np.sum(y * dout_test)
# Analytical gradient
dx_analytical, _ = rmsnorm_backward(dout_test, x_test.copy(), gamma_test)
# Numerical gradient
dx_numerical = numerical_gradient(forward_scalar, x_test.copy())Gradient verification: Max relative error: 1.33e-10 Gradient check passed
The gradient check confirms our analytical backward pass implementation is correct. A relative error below indicates that the analytical and numerical gradients agree to high precision, validating both the mathematical derivation and the code implementation.

The Jacobian heatmap makes the gradient structure concrete. The strong diagonal shows that each input element has a direct gradient path to its corresponding output element. The subtle off-diagonal values reveal the coupling effect: every input element has a small indirect influence on every output element via the shared RMS denominator. This coupling ensures that even elements whose direct contribution to the loss is small still receive gradients when the overall RMS scale needs to change.