Pre-Norm vs Post-Norm › Training Stability: The Gradient Flow Perspective
Language AI HandbookAll chapters Pre-Norm vs Post-Norm › Training Stability: The Gradient Flow PerspectiveOne section · Sections in this chapter5 / 18 ↓
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
The practical motivation for pre-norm comes from training stability. While the forward pass differences we've seen are instructive, the real story unfolds during training, when gradients must flow backward through potentially hundreds of layers. Understanding this gradient flow explains why pre-norm enables stable training of very deep networks while post-norm struggles.
The Vanishing Gradient Problem in Deep Networks
Training neural networks requires computing how changes in each parameter affect the final loss. We do this through backpropagation: starting from the loss, we work backward through the network, computing gradients layer by layer using the chain rule.
Here's the challenge. The chain rule multiplies gradients together. If you have 100 layers and each layer slightly shrinks the gradient (say, by a factor of 0.99), the gradient reaching the first layer is of what it started as. If each layer shrinks it by 0.95, you get , and the earliest layers barely learn anything. This is the vanishing gradient problem.
The converse, exploding gradients, happens when each layer slightly amplifies the gradient. Even small amplification factors compound exponentially, leading to numerical overflow and unstable training.

The visualization makes the exponential decay visceral. Even at the modest depth of 12 layers (BERT's configuration), a 2% per-layer reduction leaves only about 78% of the gradient. At 100 layers, a 5% reduction per layer is catastrophic, leaving less than 1% of the original gradient signal.
Residual connections were designed to address this problem by providing a direct path for gradients. But as we'll see, where you place the layer normalization determines whether this path remains truly direct.
Deriving the Gradient Flow
To understand the stability difference, we need to trace how gradients flow through each block type. Let's denote:
- : the loss function we're minimizing
- : the input to a transformer block
- : the output of the block
- : the gradient arriving from subsequent layers (we receive this during backpropagation)
- : the gradient we need to compute and pass to earlier layers
The chain rule tells us that . The key question is: what does look like for each block type?
Gradient Flow in Post-Norm
Recall the post-norm formulation: . To compute the gradient, we apply the chain rule from outside to inside:
- First, the gradient passes through LayerNorm
- Then, it splits into two paths: through the residual () and through the sublayer
Mathematically, this gives:
where:
- : the gradient of the loss with respect to the block input, which we want to compute
- : the gradient of the loss with respect to the block output (provided by the next layer during backpropagation)
- : the Jacobian of the layer normalization operation, a matrix that describes how small changes in the input to LayerNorm affect its output
- : the combined contribution from the residual path (the ) and the sublayer path
Notice the structure: the LayerNorm Jacobian appears as a multiplicative factor in the main gradient path. This is the crux of the instability problem. Even though LayerNorm has reasonably well-behaved gradients for a single layer (typically with eigenvalues close to 1), stacking many post-norm blocks means multiplying many of these Jacobians together. Small deviations from 1 compound across layers, leading to vanishing or exploding gradients in deep networks.
Gradient Flow in Pre-Norm
Now consider pre-norm: . The structural difference determines how the gradient splits. Applying the chain rule:
- First, the gradient splits at the addition: one path goes through the residual (), another through the sublayer
- Only the sublayer path passes through LayerNorm
This yields:
where:
- : the gradient of the loss with respect to the block input
- : the gradient of the loss with respect to the block output
- : the gradient contribution from the direct residual path (since )
- : the Jacobian of the sublayer with respect to its (normalized) input
- : the Jacobian of layer normalization
The Gradient Highway
The key insight lies in that term inside the parentheses. In pre-norm, the gradient from the output has a direct, unmodified path back to the input. This path bypasses both the sublayer and the layer normalization entirely.
Think of it as a gradient highway: no matter what happens in the sublayer (vanishing activations, saturated nonlinearities, poorly conditioned weight matrices), the gradient can always flow back through the residual connection. The term ensures that at minimum, the full gradient reaches the input.
In post-norm, this highway doesn't exist in the same form. The LayerNorm Jacobian gates all gradient flow, including the residual path. If these Jacobians systematically shrink or grow gradients even slightly, the effects compound across many layers.
This mathematical property explains the empirical observations:
- Pre-norm networks train stably even at 100+ layers
- Post-norm networks require careful warmup schedules and smaller learning rates
- Pre-norm tolerates larger learning rates without diverging


The heatmaps reveal the structural difference in gradient flow. In post-norm, gradient strength decays as it travels backward through layers, with no escape from the cascading LayerNorm Jacobians. In pre-norm, the bright diagonal represents the gradient highway: full-strength gradients flowing directly from any layer's output to its input, bypassing all intermediate transformations.
Let's simulate this difference with a numerical experiment:
def simulate_gradient_flow(n_layers, norm_type="pre"):
"""
Simulate gradient magnitude through stacked transformer blocks.
Args:
n_layers: Number of transformer blocks to stack
norm_type: 'pre' or 'post'
Returns:
List of gradient magnitudes at each layer
"""
np.random.seed(42)
d_model = 64
# Initialize parameters for each layer
sublayer_weights = [
np.random.randn(d_model, d_model) * 0.02 for _ in range(n_layers)
]
gamma = np.ones(d_model)
beta = np.zeros(d_model)
# Forward pass: track activations
x = np.random.randn(1, d_model)
activations = [x.copy()]
for i in range(n_layers):
W = sublayer_weights[i]
if norm_type == "pre":
x_norm = layer_norm(x, gamma, beta)
sublayer_out = np.tanh(x_norm @ W)
x = x + sublayer_out
else: # post
sublayer_out = np.tanh(x @ W)
x = layer_norm(x + sublayer_out, gamma, beta)
activations.append(x.copy())
# Backward pass: track gradient magnitudes
# Start with unit gradient at output
grad = np.ones_like(x)
# Express magnitudes relative to the unit-gradient baseline. A raw vector
# norm starts at sqrt(d_model), which would hide the curves on this chart.
gradient_baseline = np.sqrt(d_model)
gradient_magnitudes = [np.linalg.norm(grad) / gradient_baseline]
for i in range(n_layers - 1, -1, -1):
W = sublayer_weights[i]
x_prev = activations[i]
if norm_type == "pre":
# Simplified gradient (actual would involve LN Jacobian)
# The key point is the +1 from residual
x_norm = layer_norm(x_prev, gamma, beta)
tanh_deriv = 1 - np.tanh(x_norm @ W) ** 2
sublayer_grad = (grad * tanh_deriv) @ W.T
# Gradient through residual (direct path)
residual_grad = grad.copy()
# Combine (simplified: ignoring LN Jacobian details)
grad = residual_grad + sublayer_grad * 0.5 # LN scaling effect
else:
# Post-norm: gradient must go through LN first
# Simplified: LN tends to have gradient magnitude ~1
tanh_deriv = 1 - np.tanh(x_prev @ W) ** 2
sublayer_grad = (grad * tanh_deriv) @ W.T
grad = grad + sublayer_grad
grad = (
grad
/ (np.linalg.norm(grad) + 1e-6)
* np.linalg.norm(grad)
* 0.95
)
gradient_magnitudes.append(np.linalg.norm(grad) / gradient_baseline)
return gradient_magnitudes[::-1]
# Compare gradient flow for different depths
depths = [6, 12, 24, 48]
pre_norm_grads = {d: simulate_gradient_flow(d, "pre") for d in depths}
post_norm_grads = {d: simulate_gradient_flow(d, "post") for d in depths}

The visualization illustrates the stability difference. Pre-norm maintains consistent gradient magnitudes across all depths because the residual connection provides a direct gradient path. Post-norm gradients tend to decay in deeper networks, making optimization more challenging.