Gradient Clipping › Implementing Gradient Clipping in PyTorch

Michael BrenndoerferUpdated April 24, 20254 min read
Language AI HandbookAll chapters
Part 10 of 67Neural Network Foundations
Chapter 73Gradient Clipping › Implementing Gradient Clipping in PyTorch
Reading section 5 of 14 · Implementing Gradient Clipping in PyTorch · 4 minSection 5 of 14 · 4 min

Book contents

Section 5 of 14 · 4 min read

Gradient Clipping › Implementing Gradient Clipping in PyTorchOne section · Sections in this chapter5 / 14 ↓
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

PyTorch provides built-in functions for both clipping strategies, and integrating them into a training loop requires only a single additional line of code. The standard workflow places clipping between the backward pass (which computes gradients) and the optimizer step (which applies them).

The position matters: you must clip after calling loss.backward() so that gradients exist, but before calling optimizer.step() so that the modified gradients are what update the parameters. Clipping after the optimizer step would be too late; the damage would already be done.

In[15]:
Code
import torch
import torch.nn as nn
import torch.optim as optim

# Create a model for demonstration
model = nn.Sequential(
    nn.Linear(10, 128),
    nn.ReLU(),
    nn.Linear(128, 128),
    nn.ReLU(),
    nn.Linear(128, 1),
)

optimizer = optim.Adam(model.parameters(), lr=0.001)


# Training step with gradient clipping
def train_step_with_clipping(model, x, y, optimizer, max_norm=1.0):
    """Perform one training step with gradient clipping."""
    optimizer.zero_grad()

    # Forward pass
    predictions = model(x)
    loss = nn.MSELoss()(predictions, y)

    # Backward pass
    loss.backward()

    # Compute gradient norm BEFORE clipping
    grad_norm_before = torch.nn.utils.clip_grad_norm_(
        model.parameters(), max_norm=float("inf")
    )

    # Actually clip gradients
    grad_norm_after = torch.nn.utils.clip_grad_norm_(
        model.parameters(), max_norm=max_norm
    )

    # Update weights
    optimizer.step()

    return loss.item(), grad_norm_before.item()


# Simulate training
x = torch.randn(64, 10)
y = torch.randn(64, 1)

loss, grad_norm = train_step_with_clipping(model, x, y, optimizer, max_norm=1.0)
Out[16]:
Console
Training Step with Gradient Clipping
--------------------------------------------------
Loss: 0.931612
Gradient norm (before clipping): 0.9401
Max norm threshold: 1.0
No clipping needed (norm within threshold)

The gradient norm determines whether clipping activates. In this case, the norm exceeds 1.0, so all gradients are scaled down proportionally. PyTorch's clip_grad_norm_ function handles this computation automatically, returning the original norm for logging purposes. The underscore suffix indicates it modifies gradients in-place, which is the standard convention in PyTorch for in-place operations.

Notice that we call clip_grad_norm_ twice in the example above: once with max_norm=float('inf') to retrieve the current norm without modifying gradients, and once with our actual threshold to apply clipping. In production code, you would typically combine these into a single call, using the returned value for logging. The PyTorch documentation recommends this pattern because clip_grad_norm_ always returns the pre-clipping norm.

Tracking Clipping Frequency

Monitoring how often gradients get clipped provides insight into training stability. If clipping happens on every batch, your threshold might be too low. If it never happens, you might not need clipping at all. A healthy signal is occasional clipping, triggered by gradient spikes but not routine gradient values.

Clipping frequency also reveals something about the loss landscape. If your model clips on nearly every batch, it suggests the loss landscape is consistently steep, possibly due to aggressive learning rates, poor initialization, or a fundamentally challenging optimization problem. If clipping frequency suddenly increases mid-training, that can indicate the model is entering a difficult region of parameter space, perhaps because it encountered a particularly challenging batch or because the data distribution shifted.

In[17]:
Code
def train_with_clipping_stats(
    model, dataloader, optimizer, max_norm=1.0, epochs=1
):
    """Train while tracking gradient clipping statistics."""
    clip_counts = []
    grad_norms = []

    for epoch in range(epochs):
        epoch_clips = 0
        epoch_norms = []

        for x, y in dataloader:
            optimizer.zero_grad()
            loss = nn.MSELoss()(model(x), y)
            loss.backward()

            # Get gradient norm before clipping
            total_norm = 0.0
            for p in model.parameters():
                if p.grad is not None:
                    total_norm += p.grad.data.norm(2).item() ** 2
            total_norm = total_norm**0.5
            epoch_norms.append(total_norm)

            # Check if clipping will occur
            if total_norm > max_norm:
                epoch_clips += 1

            # Apply clipping
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
            optimizer.step()

        clip_counts.append(epoch_clips)
        grad_norms.append(epoch_norms)

    return clip_counts, grad_norms


# Create synthetic dataloader
from torch.utils.data import DataLoader, TensorDataset

X = torch.randn(1000, 10)
Y = torch.randn(1000, 1)
dataset = TensorDataset(X, Y)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

# Fresh model
model = nn.Sequential(
    nn.Linear(10, 128),
    nn.ReLU(),
    nn.Linear(128, 128),
    nn.ReLU(),
    nn.Linear(128, 1),
)
optimizer = optim.Adam(model.parameters(), lr=0.001)

clip_counts, grad_norms = train_with_clipping_stats(
    model, dataloader, optimizer, max_norm=1.0, epochs=3
)
Out[18]:
Console
Gradient Clipping Statistics
--------------------------------------------------
Epoch 1:
  Batches clipped: 17/32 (53.1%)
  Gradient norm - min: 0.6294, avg: 1.1707, max: 2.8505
Epoch 2:
  Batches clipped: 14/32 (43.8%)
  Gradient norm - min: 0.4770, avg: 1.0351, max: 2.0537
Epoch 3:
  Batches clipped: 10/32 (31.2%)
  Gradient norm - min: 0.5146, avg: 0.9937, max: 1.9324

The clipping statistics reveal training dynamics. The high initial clipping rate indicates that our threshold of 1.0 is relatively aggressive for this model, catching most gradient updates. As training progresses, gradient norms typically decrease and stabilize, leading to fewer clipping events. A healthy training run typically sees clipping on 5-20% of batches, preventing occasional spikes without constantly dampening gradients. If clipping occurs on nearly every batch, consider raising the threshold.