Pre-LN and Post-LN Transformer Architectures: Key Differences and Implementation Guide

Pre-LN applies Layer Normalization before attention and feed-forward sub-layers to enable stable training of deep transformers, while Post-LN applies normalization after the residual connection following the original "Attention Is All You Need" design.

The architectural choice between Pre-LN and Post-LN transformer architectures determines the placement of Layer Normalization relative to residual connections, directly impacting gradient flow and training stability in deep neural networks. This article examines the implementation differences using the labmlai/annotated_deep_learning_paper_implementations repository, which provides annotated PyTorch implementations demonstrating how these design patterns affect model scalability.

Architectural Fundamentals: Pre-LN vs Post-LN

Transformer blocks consist of three core components: a multi-head self-attention sub-layer, a feed-forward sub-layer, and residual connections. The position of the Layer Normalization (LN) operation relative to these sub-layers defines the two major variants.

Pre-LN (Pre-Normalization) Architecture

In Pre-LN transformers, Layer Normalization is applied before each sub-layer. The computation follows the pattern: LN → Sub-layer → Add residual.

  • The residual connection is added after the normalized sub-layer output
  • Normalizes the input to each sub-layer, keeping the residual path clean
  • Enables stable training of very deep stacks (≥30 layers)
  • Often eliminates the need for large learning-rate warm-ups

Post-LN (Post-Normalization) Architecture

In Post-LN transformers, Layer Normalization is applied after each sub-layer. The computation follows the pattern: Sub-layer → Add residual → LN.

  • The residual connection is added before the normalization
  • Represents the original formulation from "Attention Is All You Need" (Vaswani et al., 2017)
  • Works well for shallow models (≤12 layers)
  • Can suffer from training instability for deeper networks due to gradient shrinking

Training Dynamics and Gradient Flow

The placement of Layer Normalization significantly affects how gradients propagate through the network during backpropagation.

Gradient Propagation in Deep Stacks

In Post-LN architectures, the residual path bypasses the LN operation. Consequently, gradients must pass through the Layer Normalization after the addition operation. With many layers, the LN can shrink gradient magnitudes, leading to vanishing-gradient problems that destabilize training.

In Pre-LN architectures, the normalization occurs before the sub-layer, preserving the clean residual path. This design maintains gradient magnitude throughout deep stacks, preventing the degradation that occurs in Post-LN variants.

Learning Rate Schedule Requirements

Post-LN transformers typically require careful learning-rate warm-up phases and complex scheduling to prevent early-training instabilities. The Pre-LN architecture often converges faster and tolerates simpler, more aggressive learning-rate schedules due to its improved gradient flow characteristics.

Model Depth Scalability

Empirical results demonstrate distinct scalability limits: Pre-LN versions (such as those in Transformer-XL and GPT-2) can be stacked to depths exceeding 30 layers without instability, whereas Post-LN models typically degrade in performance after approximately 12 layers.

LabML Implementation Details

The labmlai/annotated_deep_learning_paper_implementations repository adopts Pre-LN for all transformer variants, providing clean, annotated PyTorch implementations.

Core Transformer Block in models.py

In labml_nn/transformers/models.py, the TransformerLayer class implements the Pre-LN pattern at lines 66-92. Each attention and feed-forward sub-layer is wrapped by nn.LayerNorm before the sub-layer computation, with residuals added afterwards.

The implementation creates distinct normalization layers for each sub-path:

class TransformerLayer(nn.Module):
    def __init__(self, d_model, n_heads, d_ff):
        super().__init__()
        self.norm_self_attn = nn.LayerNorm([d_model])   # Pre-norm for attention

        self.attn = MultiHeadAttention(d_model, n_heads)

        self.norm_ff = nn.LayerNorm([d_model])          # Pre-norm for feed-forward

        self.ff = FeedForward(d_model, d_ff)

    def forward(self, x, mask=None):
        # Self-attention sub-layer: LN first, then attention, then residual

        y = self.norm_self_attn(x)
        y = self.attn(y, y, y, mask)
        x = x + y

        # Feed-forward sub-layer: LN first, then FFN, then residual

        y = self.norm_ff(x)
        y = self.ff(y)
        x = x + y
        return x

XL and Switch Transformer Variants

The same Pre-LN pattern appears in labml_nn/transformers/xl/__init__.py (lines 67-119) for the Transformer-XL implementation, where both self-attention and feed-forward sub-layers follow the normalization-first pattern plus a final LN after the last layer. The Switch Transformer implementation in labml_nn/transformers/switch/__init__.py also maintains this Pre-LN consistency across the codebase.

Practical Code Examples

Pre-LN Block Implementation

The LabML repository implements the modern Pre-LN design that powers deep transformer models:

class TransformerLayer(nn.Module):
    def __init__(self, d_model, n_heads, d_ff):
        super().__init__()
        self.norm_self_attn = nn.LayerNorm([d_model])
        self.attn = MultiHeadAttention(d_model, n_heads)
        self.norm_ff = nn.LayerNorm([d_model])
        self.ff = FeedForward(d_model, d_ff)

    def forward(self, x, mask=None):
        # Self-attention with Pre-LN

        y = self.norm_self_attn(x)
        y = self.attn(y, y, y, mask)
        x = x + y  # Residual added after normalized sub-layer

        # Feed-forward with Pre-LN

        y = self.norm_ff(x)
        y = self.ff(y)
        x = x + y  # Residual added after normalized sub-layer

        return x

This implementation corresponds to the source at labml_nn/transformers/models.py#L66-L92.

Post-LN Block Implementation

While not used in the LabML codebase, the original Post-LN formulation from the 2017 Transformer paper follows this pattern:

class TransformerLayerPost(nn.Module):
    def __init__(self, d_model, n_heads, d_ff):
        super().__init__()
        self.attn = MultiHeadAttention(d_model, n_heads)
        self.ff = FeedForward(d_model, d_ff)
        self.norm = nn.LayerNorm([d_model])

    def forward(self, x, mask=None):
        # Sub-layer computation first

        y = self.attn(x, x, x, mask)
        x = x + y  # Residual added before normalization

        
        y = self.ff(x)
        x = x + y  # Residual added before normalization

        
        # Apply LN after the addition

        return self.norm(x)

This Post-LN style requires careful initialization and learning-rate management for models exceeding 12 layers.

Summary

  • Pre-LN places Layer Normalization before each sub-layer (attention and feed-forward), leading to better gradient flow, stable training of deep transformers (>30 layers), and simpler learning-rate schedules.
  • Post-LN places Layer Normalization after each sub-layer (the original Vaswani et al. 2017 design), which works well for shallow models but can become unstable for deeper stacks due to gradient shrinking through the normalization layers.
  • The labmlai/annotated_deep_learning_paper_implementations repository exclusively uses Pre-LN across its transformer implementations in labml_nn/transformers/models.py, labml_nn/transformers/xl/__init__.py, and labml_nn/transformers/switch/__init__.py.
  • Pre-LN architectures preserve gradient magnitude through clean residual paths, while Post-LN architectures can suffer from vanishing gradients in deep networks.

Frequently Asked Questions

Why does Post-LN require learning rate warm-up?

Post-LN architectures place Layer Normalization after the residual addition, which can cause gradients to shrink as they propagate backward through many layers. According to the source analysis, this requires small initial learning rates that gradually warm up to prevent early-training instabilities and vanishing gradients. Pre-LN avoids this by normalizing inputs before sub-layers, keeping residual paths clean and allowing higher initial learning rates.

Can I convert a Post-LN checkpoint to Pre-LN?

Converting between architectures is not straightforward because the normalization operations apply to different intermediate representations. Pre-LN normalizes inputs to sub-layers, while Post-LN normalizes outputs after residual addition. Retraining is typically required, though some research explores architecture-agnostic initialization techniques. The LabML implementations in labml_nn/transformers/models.py follow Pre-LN exclusively to avoid these compatibility issues.

Which architecture performs better for small models?

For shallow transformers (≤12 layers), both architectures can achieve comparable performance. Post-LN remains common in standard 6-12 layer encoder-decoder models following the original "Attention Is All You Need" specification. However, as shown in the LabML codebase, Pre-LN has become the default even for standard depths due to its superior training stability and reduced sensitivity to hyperparameter tuning.

Does Pre-LN change the model capacity or just training dynamics?

Pre-LN primarily affects training dynamics and optimization rather than model capacity. Both architectures maintain the same parameter counts and representational capacity. However, Pre-LN enables effective utilization of deeper architectures (30+ layers) that Post-LN cannot train stably, indirectly allowing greater model capacity in practice. The feed-forward and attention mechanisms remain identical between variants, as seen in labml_nn/transformers/models.py.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →