What a real training loop adds to the update rule.
On this page
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.
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.
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:
| step | Adam and Adam + L2 | AdamW |
|---|---|---|
| 1 | 4.500 | 4.250 |
| 2 | 4.002 | 3.541 |
| 3 | 3.508 | 2.875 |
| 4 | 3.020 | 2.253 |
| 5 | 2.540 | 1.677 |
The difference is where λθ goes. This page uses PyTorch's form: the decay step is lr·λ·θ.
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.
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.
| knob | raise it | lower it | default |
|---|---|---|---|
weight_ (λ) | every weight shrinks faster toward 0 | weaker pull toward 0; 0 turns decay off | 0.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)
lr·weight_decay·θ.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.
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:
| axis | g | ε | gSAM |
|---|---|---|---|
| x | −35 | −0.0499 | −35.50 |
| y | 2 | +0.0029 | 2.001 |
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
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
perturb.Each SAM step costs two forward and backward passes.
| knob | raise it | lower it | default |
|---|---|---|---|
| ρ (rho) | looks farther, prefers flatter regions, can slow convergence | closer to the base optimizer, which is ρ 0 | 0.05, the paper's setting |
A large rate moves fast early, but near a minimum a constant rate keeps bouncing, so a schedule changes α as training goes.
div_factor.pct_start of the run, 30% by default.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).
| knob | raise it | lower it | default |
|---|---|---|---|
| W, warmup steps | gentler start | full rate sooner, riskier | none; LLaMA 2,000 |
| T (T_ | decay spread over more steps | reaches αmin sooner | must be set |
| αmin (eta_ | the floor of the rate | a smaller final rate | 0 |
γ (gamma) | gentler drops | sharper drops | StepLR 0.1 |
| s (step_ | longer flat stretches | more frequent drops | must be set |
| pct_ | a longer climb | a shorter climb | 0.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)
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
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):
| τ | value clipping turn | norm clipping turn |
|---|---|---|
| 20 | 2.4° | 0° |
| 10 | 8.0° | 0° |
| 5 | 18.5° | 0° |
| 2 | 41.7° | 0° |
Figure 7. Norm clips to the circle, value to the square.
| knob | raise it | lower it | default |
|---|---|---|---|
τ (max_norm) | clips less often; above the largest norm, never | shortens 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
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.
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))
lr 1e-3, betas (0.9, 0.999) and eps 1e-8 (AdamW documentation).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
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.
| symptom | likely cause | fix |
|---|---|---|
| loss rises or turns NaN in the first steps | α too high for the start | lower α, warm it up (section 3), clip the gradient |
| weight decay seems weak with Adam | Adam rescales L2 inside the gradient | AdamW (section 1) |
| loss jumps when the schedule moves | stepped per batch when built per epoch, or the reverse | step it as its docs say; One |
| rare, very large gradients | exploding gradients, common in RNNs | clip by norm (section 4) |
| the first rate of the schedule is skipped | sched. called before opt. | call opt.step() first, as PyTorch requires since 1.1 (docs) |
| a resumed run starts like a fresh one | only the model was saved, so Adam's moments restart at zero | save opt. too |