← All tutorials

Optimizers in Practice: AdamW, SAM, Schedules and Clipping

What a real training loop adds to the update rule.

On this page

1 Decoupled weight decay (AdamW) 2 Sharpness-aware minimization (SAM) 3 Learning rate schedules 4 Gradient clipping 5 AdamW, a schedule and clipping in one PyTorch loop 6 Failures in the training loop 7 References
forwardloss on one batch backward()fills .grad cliplength ≤ τ opt.step()moves the weights next batch scheduler.step()per epoch or batch 2SAMsecond pass 4Clippingcaps length 1AdamWdecay in step 3Schedulesets α
Figure 1. One training step and the four controls, by section number.

The optimizer turns each gradient into a step. Around it, AdamW changes how weight decay enters Adam's step, SAM spends a second pass to prefer flat minima, a schedule changes α per step or per epoch (one full pass over the training data), and clipping caps the gradient's length.

1 Decoupled weight decay (AdamW)

Weight decay shrinks the weights toward zero each step. Here it meets Adam's step, whose worked first step is in Adaptive Optimizers, section 4.4.

Adam, recap
mt = β1·mt−1 + (1 − β1)·gt
g the gradient; m its running average; β1 how slowly that average forgets.
vt = β2·vt−1 + (1 − β2)·gt²
v the running average of the squared gradient; β2 how slowly it forgets.
θ ← θ − α·m̂t√v̂t + ε
θ the weights; α the learning rate; m̂ and v̂ the zero-start-corrected averages (Adaptive Optimizers, section 4.3); ε, a tiny constant that keeps the step from dividing by zero.

On the illustrative loss of the series, L = 5x² + 0.2y² from (−3.5, 5), at α 0.5 and weight decay λ 0.1, y after each step:

Under Adam the L2 term is divided away; AdamW's decay acts at full size.
stepAdam and Adam + L2AdamW
14.5004.250
24.0023.541
33.5082.875
43.0202.253
52.5401.677

The difference is where λθ goes. This page uses PyTorch's form: the decay step is lr·λ·θ.

L2 inside the gradient against decoupled decay
gt = ∇L(θ) + λθ
With Adam + L2, the slope of the L2 penalty, λθ, joins the gradient, so Adam later divides it by √v̂ like everything else.
θ ← θ − α·∇L − αλθ
Under SGD the same term is a plain shrink by αλ, so L2 and decay are one thing there.
θ ← θ − α·λ·θ
AdamW applies this decay outside the divide, then takes the Adam step, whose moments see only ∇L.

Loshchilov and Hutter 2019 also scale the decay by the schedule, as lr·λ·θ does here. Under L2, weights with large gradients are barely decayed; AdamW decays every weight at the same rate. Take weight A, used on every token, and weight B, a rare token's embedding row.

Decay part of one step, log scale10⁻⁶10⁻⁵10⁻⁴10⁻³ABABAdam + L2AdamW1
Figure 2. ① Adam + L2 decays A and B 500× apart; AdamW gives both 10⁻⁵.

Illustrative values, steady state with α 1e−3, λ 0.01 and θ 1, and √v̂ 10 for A and 0.02 for B. LLaMA, Meta's large language model, trains with AdamW weight decay 0.1 (Touvron et al. 2023). Decay also meets gradient balancing, in Gradient Balancing Inside the Training Loop, section 9.2.

Figure 3. Decay only, √v̂ fixed; illustrative.

AdamW's decay knob.
knobraise itlower itdefault
weight_decay (λ)every weight shrinks faster toward 0weaker pull toward 0; 0 turns decay off0.01 in PyTorch AdamW
import torch
from torch.optim import Adam
from torch.optim import AdamW
model = torch.nn.Linear(10, 1)
p = list(model.parameters())
lr, lam = 1e-3, 1e-2  # α, λ
decay = dict(weight_decay=lam)
# Adam: λθ joins the grad
adam = Adam(p, lr, **decay)
# AdamW: decay outside it
adamw = AdamW(p, lr, **decay)
  1. 1A small model, α and λ.
  2. 2Adam with λ inside the gradient, the L2 line.
  3. 3AdamW with the decay outside the rescaling: the step lr·weight_decay·θ.

2 Sharpness-aware minimization (SAM)

In this section ε is SAM's perturbation, a small move of the weights that SAM tests; it is unrelated to Adam's ε in section 1, the tiny constant that keeps the step from dividing by zero.

SAM wraps another optimizer and prefers flat minima, where the loss stays low when the weights move a little. Test data shifts the loss surface slightly, so a flat minimum keeps its low loss there; Foret et al. 2021 report SAM improving image classifiers on CIFAR and ImageNet. SAM tests every point within a radius ρ of the weights.

Two minima of equal loss, ρ 0.0510.062520.0025sharp, curvature 50flat, curvature 2loss Lworst loss within ρ
Figure 4. Illustrative. Worst loss within ρ 0.05 is ① 0.0625 and ② 0.0025.
The SAM objective
minθ max‖ε‖ ≤ ρ L(θ + ε)
Score each θ by the worst loss within distance ρ, then minimize that score. It ranks the flat minimum 25 times lower.

SAM estimates the worst nearby point with one step uphill, written ε; the hat marks this one-step estimate of ε. At the ravine start with ρ 0.05:

The test point sits 0.05 uphill, and the gradient read there is slightly steeper on x.
axisgεgSAM
x−35−0.0499−35.50
y2+0.00292.001
One SAM gradient
ε = ρ·∇L(θ)‖∇L(θ)‖
A step of length ρ straight uphill, a one-step guess at the worst nearby point. ‖∇L‖ is the length of the gradient vector (Vectors and Matrices, section 2).
gSAM = ∇L(θ + ε)
The gradient read at that point. The base optimizer applies it at θ itself; descending it lowers the SAM objective.

Figure 5. ① θ, ② θ + ε, ③ the step; drag θ.

In code, perturb is the first pass; closure returns the minibatch loss. SAM's official code is in JAX, Google's array library (google-research/sam).

import torch
from torch import stack
def perturb(ps, closure, rho):
    closure().backward()
    gs = [p.grad for p in ps]
    ns = [g.norm() for g in gs]
    k = rho / stack(ns).norm()
    ds = [k * g for g in gs]
    with torch.no_grad():
        for p, d in zip(ps, ds):
            p.add_(d)  # θ + ε̂
    return ds
  1. 1Reads the gradient at θ.
  2. 2Scales it to length ρ, which is ε.
  3. 3Moves every weight to θ + ε and returns ε.
ds = perturb(ps, closure, rho)
opt.zero_grad()
closure().backward()  # θ + ε̂
with torch.no_grad():
    for p, d in zip(ps, ds):
        p.sub_(d)  # back to θ
opt.step()  # steps with g_SAM
  1. 1Moves to θ + ε with perturb.
  2. 2Reads the gradient there, gSAM, the second SAM line.
  3. 3Moves the weights back to θ.
  4. 4Lets the base optimizer step with gSAM.

Each SAM step costs two forward and backward passes.

SAM's one knob.
knobraise itlower itdefault
ρ (rho)looks farther, prefers flatter regions, can slow convergencecloser to the base optimizer, which is ρ 00.05, the paper's setting

3 Learning rate schedules

A large rate moves fast early, but near a minimum a constant rate keeps bouncing, so a schedule changes α as training goes.

Step10× drop every 30Exponential×0.95 each stepCosineCosineAnnealingLRWarmup, cosineLLaMA1Warm restartsSGDR, every T₀2One-cycleOneCycleLR3
Figure 6. Each panel is one run's rate. ① Warmup, ② a restart, ③ peak at 30%.
Warmup
αt = αmax·tW for t < W
① t is the step and W the warmup length, so the rate climbs in a straight line. Goyal et al. 2017 warm up for 5 epochs in large-batch ImageNet training, because early in training the network is changing rapidly.
Cosine
st = 1 + cos(π·tT)
st runs from 2 at t 0 to 0 at t T, the run length.
αt = αmin + 12·(αmax − αmin)·st
So the rate falls from αmax to αmin along half a cosine; with αmax 0.1, αmin 0 and T 100 it is 0.0854 at t 25 and 0.05 at t 50.
Step and exponential decay
αt = α0·γ⌊t/s⌋
In step decay γ is the factor per drop and s the steps between drops; the exponent, t divided by s and rounded down, counts the drops so far. With α0 0.1, γ 0.1 and s 30, as in the step panel of Figure 6, the rate is 0.1 up to step 29 and 0.01 from step 30.
αt = α0·γt
Exponential decay applies γ every step; at γ 0.95 the rate halves in about 14 steps.
Warm restarts
st = 1 + cos(π·TcurTi)
② The cosine above with t replaced by Tcur, the steps since the last restart; Ti is the cycle length, T0 at first.
αt = αmin + 12·(αmax − αmin)·st
The same rate line as the cosine, so when a cycle ends st is back at 2 and the rate jumps back to αmax.
One-cycle
αstart = αmax25
③ The rate starts 25 times below αmax; 25 is the default of div_factor.
αpeak = αmax
It climbs to αmax after pct_start of the run, 30% by default.
αend = αmax25·104
Then it falls to 25 × 104 times below αmax; 104 is the default of final_div_factor.

LLaMA used 2,000 warmup steps and a cosine to 10% of the peak. Transformers that normalize each sublayer's input (pre-LN) need less warmup than ones that normalize its output (post-LN) (The Transformer Block, section 3).

Schedule knobs, with PyTorch defaults.
knobraise itlower itdefault
W, warmup stepsgentler startfull rate sooner, riskiernone; LLaMA 2,000
T (T_max)decay spread over more stepsreaches αmin soonermust be set
αmin (eta_min)the floor of the ratea smaller final rate0
γ (gamma)gentler dropssharper dropsStepLR 0.1
s (step_size)longer flat stretchesmore frequent dropsmust be set
pct_starta longer climba shorter climb0.3

A scheduler keeps one counter and sets α from it. Step, exponential and cosine usually step once per epoch; one-cycle steps once per batch.

import torch
import torch.optim as optim
from torch.optim import lr_scheduler
net = torch.nn.Linear(4, 1)
ps = net.parameters()
opt = optim.SGD(ps, lr=0.1)
# pick one, stepped per epoch
sched = lr_scheduler.StepLR(opt, 30, 0.1)
sched = lr_scheduler.ExponentialLR(opt, 0.95)
sched = lr_scheduler.CosineAnnealingLR(
    opt, T_max=100)
  1. 1Builds a model and an SGD optimizer to schedule.
  2. 2Step decay, the rate times 0.1 every 30 epochs.
  3. 3Exponential decay, the rate times 0.95 every epoch.
  4. 4Cosine annealing over 100 epochs, the cosine schedule equation.
sched = lr_scheduler.CosineAnnealingWarmRestarts(
    opt, T_0=10)
sched = lr_scheduler.OneCycleLR(
    opt, max_lr=0.1, total_steps=1000)
for step in range(1000):
    opt.step()
    sched.step()  # per batch
  1. 1Warm restarts every 10 epochs.
  2. 2One-cycle up to 0.1 and back over 1000 steps.
  3. 3Steps the optimizer, then the one-cycle schedule, once per batch.

4 Gradient clipping

Clipping guards against exploding gradients, gradients that suddenly become very large. It caps either the length ‖g‖ of the gradient vector (Vectors and Matrices, section 2) or each coordinate. For g = (−35, 2):

Norm clipping never turns the gradient; value clipping turns it more as τ shrinks.
τvalue clipping turnnorm clipping turn
202.4°0°
108.0°0°
518.5°0°
241.7°0°
Two ways to clip
g ← g·min(1, τ‖g‖)
In norm clipping, under τ the min picks 1 and nothing changes; above it the whole gradient is rescaled to length τ, same direction.
gi ← clip(gi, −τ, τ)
Value clipping caps each coordinate on its own, which bends the direction toward the diagonal and can leave it longer than τ.

Figure 7. Norm clips to the circle, value to the square.

clip_grad_norm_ has no default for max_norm.
knobraise itlower itdefault
τ (max_norm)clips less often; above the largest norm, nevershortens more steps, like a smaller αnone, required; LLaMA used 1.0

Clipping sits between backward() and opt.step().

import torch
import torch.nn.functional as F
from torch.nn.utils import clip_grad_norm_
from torchvision import datasets, models, transforms
data = datasets.CIFAR10('.', download=True,
    transform=transforms.ToTensor())
loader = torch.utils.data.DataLoader(data, batch_size=128)
net = models.resnet18(num_classes=10)
opt = torch.optim.SGD(net.parameters(), lr=0.1)
x, y = next(iter(loader))
loss = F.cross_entropy(net(x), y)
loss.backward()
clip_grad_norm_(net.parameters(), 1.0)
opt.step()  # after the clip
  1. 1A ResNet-18 for the 10 CIFAR-10 image classes, its optimizer, and batches of 128 images.
  2. 2One batch, its cross-entropy loss (the standard classification loss) and its gradients.
  3. 3Gradient length capped at τ 1.0, the norm clipping line.
  4. 4Steps with the clipped gradient.

The through-time case is in RNN Basics: Recurrence and Backpropagation Through Time, section 2.2; its interaction with balancing is in Gradient Balancing Inside the Training Loop, section 10.

5 AdamW, a schedule and clipping in one PyTorch loop

The optimizer is built once, before the loop, and keeps its state for the parameters from then on; each batch runs forward, backward, the optional clip and opt.step(), and the scheduler changes α per batch or per epoch.

The setup trains a ResNet-18 on the 50,000 CIFAR-10 training images with AdamW and a cosine schedule over every batch.

import torch
import torch.nn.functional as F
from torch.nn.utils import clip_grad_norm_
from torch.optim import AdamW, lr_scheduler
from torchvision import datasets, models, transforms
data = datasets.CIFAR10('.', download=True,
    transform=transforms.ToTensor())
loader = torch.utils.data.DataLoader(
    data, batch_size=128, shuffle=True)
net = models.resnet18(num_classes=10)
opt = AdamW(net.parameters())  # once
epochs = 30
sched = lr_scheduler.CosineAnnealingLR(
    opt, T_max=epochs * len(loader))
  1. 1CIFAR-10 in shuffled batches of 128 images.
  2. 2The ResNet-18 and AdamW, built once, before the loop, with PyTorch's defaults lr 1e-3, betas (0.9, 0.999) and eps 1e-8 (AdamW documentation).
  3. 3A cosine schedule over every batch of 30 epochs.

One update per batch, 391 batches per epoch.

for epoch in range(epochs):
    for x, y in loader:
        loss = F.cross_entropy(net(x), y)
        opt.zero_grad()
        loss.backward()
        clip_grad_norm_(net.parameters(), 1.0)
        opt.step()  # every batch
        sched.step()  # per batch
  1. 1The cross-entropy loss on one batch of images.
  2. 2Clears old gradients and fills new ones.
  3. 3Caps the gradient's total length at 1.0 before the step (section 4).
  4. 4One optimizer update per batch.
  5. 5One schedule tick per batch, since T_max counts batches; with T_max in epochs it would step once per epoch, after the inner loop.

SAM replaces the single opt.step() with its two-pass step from section 2.

6 Failures in the training loop

Most of these trace back to the rate, the schedule or the saved state.
symptomlikely causefix
loss rises or turns NaN in the first stepsα too high for the startlower α, warm it up (section 3), clip the gradient
weight decay seems weak with AdamAdam rescales L2 inside the gradientAdamW (section 1)
loss jumps when the schedule movesstepped per batch when built per epoch, or the reversestep it as its docs say; OneCycleLR steps per batch
rare, very large gradientsexploding gradients, common in RNNsclip by norm (section 4)
the first rate of the schedule is skippedsched.step() called before opt.step()call opt.step() first, as PyTorch requires since 1.1 (docs)
a resumed run starts like a fresh oneonly the model was saved, so Adam's moments restart at zerosave opt.state_dict() too

7 References

  1. Kingma, D. P., and Ba, J. (2015). Adam: a method for stochastic optimization. ICLR.
  2. Goyal, P., et al. (2017). Accurate, large minibatch SGD: training ImageNet in 1 hour. arXiv:1706.02677.
  3. Loshchilov, I., and Hutter, F. (2017). SGDR: stochastic gradient descent with warm restarts. ICLR.
  4. Smith, L. N. (2018). A disciplined approach to neural network hyper-parameters: part 1, learning rate, batch size, momentum, and weight decay. arXiv:1803.09820.
  5. Loshchilov, I., and Hutter, F. (2019). Decoupled weight decay regularization. ICLR.
  6. Foret, P., et al. (2021). Sharpness-aware minimization for efficiently improving generalization. ICLR.
  7. Touvron, H., et al. (2023). LLaMA: open and efficient foundation language models. arXiv:2302.13971.
  8. PyTorch documentation, torch.optim.lr_scheduler (when schedulers step).
  9. PyTorch documentation, torch.optim.AdamW (default settings), and torch.nn.utils.clip_grad_norm_.