Thomas Piacentini
← Workshop

Cheat sheet

Optimizers and Learning Rate Schedules

Original text · English

Pick SGD, Adam or AdamW, shape the learning rate, then clip, scale and accumulate gradients safely.

Optimizers side by side

OptimizerDefaultsBehaviour
SGDlr=0.01One rate for all weights. Oscillates across a valley
SGDMomentumlr=0.01, beta=0.9Keeps about the last 10 gradients. 3,000-5,000 steps, not 10,000
RMSPropepsilon=1e-8Divides each gradient by the running RMS
Adamlr=0.001, beta1=0.9, beta2=0.999Momentum plus RMSProp plus bias correction
AdamWweight_decay=0.01Same as Adam. Decay is not scaled by the adaptive factor
Sources

Update rules

SGD
w = w - \text{lr} \cdot g
Momentum
m_t = \beta m_{t-1} + g, then w = w - \text{lr} \cdot m_t
RMSProp
s_t = \beta s_{t-1} + (1 - \beta) g^2, step \text{lr} \cdot g / (\sqrt{s_t} + \epsilon)
Adam moments
m_t = \beta_1 m_{t-1} + (1 - \beta_1) g and v_t = \beta_2 v_{t-1} + (1 - \beta_2) g^2
Bias correction
\hat{m} = m_t / (1 - \beta_1^t). It matters for the first ~10 steps.
Adam step
w = w - \text{lr} \cdot \hat{m} / (\sqrt{\hat{v}} + \epsilon)
AdamW step
the Adam step, then - \text{lr} \cdot \lambda w on the weights.
Sources

Use each optimizer when

  • Use AdamW for a transformer or LLM: lr=1e-4, wd=0.01-0.1.
  • Use SGD + Momentum for a CNN or ResNet: lr=0.1, momentum=0.9, weight_decay=1e-4.
  • Use Adam for a GAN: lr=2e-4, beta1=0.5.
  • Use AdamW for fine-tuning: lr=2e-5, wd=0.01.
  • Use AdamW with lr=3e-4, wd=0.01 when you do not know yet.
  • When the defaults fail, change lr first. Then \beta_2. Almost never change \beta_1 or \epsilon.
Sources

Schedules side by side

CommandWhat it does
constant_schedule(step, lr=0.01)Same rate every step. Use it for small models and debugging
step_decay_schedule(step, lr=0.1, step_size=100, gamma=0.1)Cuts lr by gamma at fixed steps. Abrupt drops can spike the loss
cosine_schedule(step, lr=0.01, total_steps=1000, lr_min=1e-5)Cosine from lr to lr_min. The safe default for a moderate budget
warmup_cosine_schedule(step, lr=0.01, total_steps=1000, warmup_steps=100, lr_min=1e-5)Linear ramp, then cosine. Use it for a large budget
one_cycle_schedule(step, lr=0.01, total_steps=1000)Up from lr/25, then down to lr/10000. Use it for a small budget
CosineWithWarmup(warmup_steps, total_steps, lr_max, lr_min)Warmup plus cosine with checked boundaries. Pins at lr_min
InverseSqrtWarmup(warmup_steps, lr_max)Linear warmup, then lr_max * sqrt(warmup_steps / step)
LinearWarmupConstant(warmup_steps, lr_max)Linear warmup, then a flat lr_max. An ablation baseline
Sources

Pick a schedule

  1. Total steps unknown use a constant LR with manual decay.
  2. Large budget, days or weeks use warmup plus cosine decay. Warmup = 1-5% of steps.
  3. Moderate budget use cosine annealing. Set lr_min = lr_max / 10.
  4. Small budget, hours use 1cycle. Find lr_max with the LR range test.
  5. Fine-tuning a Llama or GPT model use get_cosine_schedule_with_warmup with warmup = 3-5% of total steps.
Sources

Cosine with linear warmup

def lr(self, step: int) -> float:
if step < 0:
raise ValueError(f"step must be non-negative, got {step}")
if self.warmup_steps > 0 and step <= self.warmup_steps:
return self.lr_max * (step / self.warmup_steps)0 at step 0
if step >= self.total_steps:
return self.lr_minfloor, no extrapolation
decay_span = max(1, self.total_steps - self.warmup_steps)
progress = (step - self.warmup_steps) / decay_span
cosine = 0.5 * (1.0 + math.cos(math.pi * progress))
return self.lr_min + (self.lr_max - self.lr_min) * cosine
Sources

Clip and scale in one step

self.optimizer.zero_grad(set_to_none=True)
with torch.amp.autocast(device_type=self.device_type, dtype=self.amp_dtype):
predictions = self.model(inputs)
loss = self._loss_fn(predictions, targets)
if not torch.isfinite(loss).all().item():skip, do not call update()
return self._record_skip(...)
self.scaler.scale(loss).backward()
self.scaler.unscale_(self.optimizer)clip after unscale
if has_non_finite_grad(self.model.parameters()):
self.scaler.update()skip, the scaler halves
pre_clip, post_clip = clip_global_l2_norm(list(self.model.parameters()), self.max_norm)
self.scaler.step(self.optimizer)
self.scaler.update()doubles or halves the factor
Sources

Accumulate gradients

accumulation_steps = effective_batch // (micro_batch * world_size)
for x, y in chunks(x_full, y_full, n):n micro-batches
scaled = criterion(model(x), y) / ndivide before backward
scaled.backward()grads sum in param.grad
opt.step()once per effective batch
with no_sync_context(model):all micro-batches but the last
loss.backward()
Sources

Numbers to know

SettingDefaultNote
Adam/AdamW lr1e-4 to 3e-4Fine-tuning uses 1e-5 to 5e-5
Warmup1-5%Lesson 44 says 1-3 percent of total steps
lr_min10 percentOf lr_max. Zero leaves training unfinished
Step decay\gamma = 0.1step_size = 30 drops lr 10x every 30 epochs
max_norm1.0The default for language-model training
Scaling factor2^17A healthy run saturates near 2^17 or 2^18
Skip rate alert5 percentOver a 1,000-step rolling window
Accumulation checkmax_abs_diff < 1e-4Full batch against micro-batches
Sources

Troubleshooting

SymptomCauseFix
Loss explodes, NaN weightslr too high (lr > 0.01)Lower lr. Add warmup
Loss decreases slowlylr too low (lr < 0.00001)Raise lr. Sweep lr_max first
Loss oscillates near a minimumConstant lr is too large lateDecay with cosine or steps
Loss goes vertical on one batchGradient norm spikeClip the global L2 norm to 1.0
Scaling factor never changedNo update() on the skip pathCall scaler.update() every step
Optimizer step 16 times too bigLoss summed over micro-batchesDivide each loss by accum_steps
Subtle schedule bugsscheduler.step() firstCall optimizer.step() first
Long warmup does not scaleWarmup set as an absolute countSet warmup as a fraction of total steps
Sources

Questions

Why warmup for Adam?
Its moment estimates start at zero. Early steps use bad statistics.
Why AdamW over Adam plus L2?
Every parameter gets the same proportional shrinkage.
Why SGD for CNNs?
SGD finds flatter minima. They often generalize better.
What does 1cycle need?
The total number of steps in advance.
Does accumulation save time?
No. Doubling accum_steps doubles the wall time per optimizer step.
Does CPU autocast need loss scaling?
No. CPU autocast uses BF16 by default.
Sources
Original source
---
id: optimizers-schedules
title: Optimizers and Learning Rate Schedules
code: OL
subtitle: Pick SGD, Adam or AdamW, shape the learning rate, then clip, scale and accumulate gradients safely.
hue: 28
sources: phases/03-deep-learning-core/06-optimizers, phases/03-deep-learning-core/09-learning-rate-schedules, phases/19-capstone-projects/44-cosine-lr-warmup, phases/19-capstone-projects/45-gradient-clipping-amp, phases/19-capstone-projects/46-gradient-accumulation
source_hash: 6af6a467fef2cb6e
---

## Optimizers side by side
type: comparison
span: 2
from: phases/03-deep-learning-core/06-optimizers/docs/en.md, phases/03-deep-learning-core/06-optimizers/code/main.py

| Optimizer | Defaults | Behaviour |
|---|---|---|
| `SGD` | `lr=0.01` | One rate for all weights. Oscillates across a valley |
| `SGDMomentum` | `lr=0.01, beta=0.9` | Keeps about the last 10 gradients. 3,000-5,000 steps, not 10,000 |
| RMSProp | `epsilon=1e-8` | Divides each gradient by the running RMS |
| `Adam` | `lr=0.001, beta1=0.9, beta2=0.999` | Momentum plus RMSProp plus bias correction |
| `AdamW` | `weight_decay=0.01` | Same as Adam. Decay is not scaled by the adaptive factor |

## Update rules
type: concepts
from: phases/03-deep-learning-core/06-optimizers/docs/en.md

- **SGD**: $w = w - \text{lr} \cdot g$
- **Momentum**: $m_t = \beta m_{t-1} + g$, then $w = w - \text{lr} \cdot m_t$
- **RMSProp**: $s_t = \beta s_{t-1} + (1 - \beta) g^2$, step $\text{lr} \cdot g / (\sqrt{s_t} + \epsilon)$
- **Adam moments**: $m_t = \beta_1 m_{t-1} + (1 - \beta_1) g$ and $v_t = \beta_2 v_{t-1} + (1 - \beta_2) g^2$
- **Bias correction**: $\hat{m} = m_t / (1 - \beta_1^t)$. It matters for the first ~10 steps.
- **Adam step**: $w = w - \text{lr} \cdot \hat{m} / (\sqrt{\hat{v}} + \epsilon)$
- **AdamW step**: the Adam step, then $- \text{lr} \cdot \lambda w$ on the weights.

## Use each optimizer when
type: tips
from: phases/03-deep-learning-core/06-optimizers/docs/en.md

- Use AdamW for a transformer or LLM: lr=1e-4, wd=0.01-0.1.
- Use SGD + Momentum for a CNN or ResNet: lr=0.1, momentum=0.9, weight_decay=1e-4.
- Use Adam for a GAN: lr=2e-4, beta1=0.5.
- Use AdamW for fine-tuning: lr=2e-5, wd=0.01.
- Use AdamW with lr=3e-4, wd=0.01 when you do not know yet.
- When the defaults fail, change lr first. Then $\beta_2$. Almost never change $\beta_1$ or $\epsilon$.

## Schedules side by side
type: command-table
span: 2
from: phases/03-deep-learning-core/09-learning-rate-schedules/docs/en.md, phases/03-deep-learning-core/09-learning-rate-schedules/code/main.py, phases/19-capstone-projects/44-cosine-lr-warmup/code/main.py

| Command | What it does |
|---|---|
| `constant_schedule(step, lr=0.01)` | Same rate every step. Use it for small models and debugging |
| `step_decay_schedule(step, lr=0.1, step_size=100, gamma=0.1)` | Cuts lr by gamma at fixed steps. Abrupt drops can spike the loss |
| `cosine_schedule(step, lr=0.01, total_steps=1000, lr_min=1e-5)` | Cosine from lr to lr_min. The safe default for a moderate budget |
| `warmup_cosine_schedule(step, lr=0.01, total_steps=1000, warmup_steps=100, lr_min=1e-5)` | Linear ramp, then cosine. Use it for a large budget |
| `one_cycle_schedule(step, lr=0.01, total_steps=1000)` | Up from lr/25, then down to lr/10000. Use it for a small budget |
| `CosineWithWarmup(warmup_steps, total_steps, lr_max, lr_min)` | Warmup plus cosine with checked boundaries. Pins at `lr_min` |
| `InverseSqrtWarmup(warmup_steps, lr_max)` | Linear warmup, then `lr_max * sqrt(warmup_steps / step)` |
| `LinearWarmupConstant(warmup_steps, lr_max)` | Linear warmup, then a flat `lr_max`. An ablation baseline |

## Pick a schedule
type: flow
from: phases/03-deep-learning-core/09-learning-rate-schedules/docs/en.md

1. **Total steps unknown**: use a constant LR with manual decay.
2. **Large budget, days or weeks**: use warmup plus cosine decay. Warmup = 1-5% of steps.
3. **Moderate budget**: use cosine annealing. Set lr_min = lr_max / 10.
4. **Small budget, hours**: use 1cycle. Find lr_max with the LR range test.
5. **Fine-tuning a Llama or GPT model**: use `get_cosine_schedule_with_warmup` with warmup = 3-5% of total steps.

## Cosine with linear warmup
type: annotated-code
lang: python
from: phases/19-capstone-projects/44-cosine-lr-warmup/code/main.py

```python
def lr(self, step: int) -> float:
    if step < 0:
        raise ValueError(f"step must be non-negative, got {step}")
    if self.warmup_steps > 0 and step <= self.warmup_steps:
        return self.lr_max * (step / self.warmup_steps)   # 0 at step 0
    if step >= self.total_steps:
        return self.lr_min                                 # floor, no extrapolation
    decay_span = max(1, self.total_steps - self.warmup_steps)
    progress = (step - self.warmup_steps) / decay_span
    cosine = 0.5 * (1.0 + math.cos(math.pi * progress))
    return self.lr_min + (self.lr_max - self.lr_min) * cosine
```

## Clip and scale in one step
type: annotated-code
lang: python
from: phases/19-capstone-projects/45-gradient-clipping-amp/code/main.py, phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md

```python
self.optimizer.zero_grad(set_to_none=True)
with torch.amp.autocast(device_type=self.device_type, dtype=self.amp_dtype):
    predictions = self.model(inputs)
    loss = self._loss_fn(predictions, targets)
if not torch.isfinite(loss).all().item():   # skip, do not call update()
    return self._record_skip(...)
self.scaler.scale(loss).backward()
self.scaler.unscale_(self.optimizer)        # clip after unscale
if has_non_finite_grad(self.model.parameters()):
    self.scaler.update()                     # skip, the scaler halves
pre_clip, post_clip = clip_global_l2_norm(list(self.model.parameters()), self.max_norm)
self.scaler.step(self.optimizer)
self.scaler.update()                         # doubles or halves the factor
```

## Accumulate gradients
type: annotated-code
lang: python
from: phases/19-capstone-projects/46-gradient-accumulation/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/code/main.py

```python
accumulation_steps = effective_batch // (micro_batch * world_size)
for x, y in chunks(x_full, y_full, n):    # n micro-batches
    scaled = criterion(model(x), y) / n     # divide before backward
    scaled.backward()                       # grads sum in param.grad
opt.step()                                  # once per effective batch
with no_sync_context(model):                # all micro-batches but the last
    loss.backward()
```

## Numbers to know
type: defaults
from: phases/03-deep-learning-core/06-optimizers/docs/en.md, phases/03-deep-learning-core/09-learning-rate-schedules/docs/en.md, phases/19-capstone-projects/44-cosine-lr-warmup/docs/en.md, phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/docs/en.md

| Setting | Default | Note |
|---|---|---|
| Adam/AdamW lr | 1e-4 to 3e-4 | Fine-tuning uses 1e-5 to 5e-5 |
| Warmup | 1-5% | Lesson 44 says 1-3 percent of total steps |
| `lr_min` | 10 percent | Of `lr_max`. Zero leaves training unfinished |
| Step decay | $\gamma = 0.1$ | step_size = 30 drops lr 10x every 30 epochs |
| `max_norm` | 1.0 | The default for language-model training |
| Scaling factor | `2^17` | A healthy run saturates near `2^17` or `2^18` |
| Skip rate alert | 5 percent | Over a 1,000-step rolling window |
| Accumulation check | `max_abs_diff < 1e-4` | Full batch against micro-batches |

## Troubleshooting
type: troubleshooting
from: phases/03-deep-learning-core/06-optimizers/docs/en.md, phases/03-deep-learning-core/09-learning-rate-schedules/docs/en.md, phases/19-capstone-projects/44-cosine-lr-warmup/docs/en.md, phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/docs/en.md

| Symptom | Cause | Fix |
|---|---|---|
| Loss explodes, NaN weights | lr too high (lr > 0.01) | Lower lr. Add warmup |
| Loss decreases slowly | lr too low (lr < 0.00001) | Raise lr. Sweep `lr_max` first |
| Loss oscillates near a minimum | Constant lr is too large late | Decay with cosine or steps |
| Loss goes vertical on one batch | Gradient norm spike | Clip the global L2 norm to 1.0 |
| Scaling factor never changed | No `update()` on the skip path | Call `scaler.update()` every step |
| Optimizer step 16 times too big | Loss summed over micro-batches | Divide each loss by `accum_steps` |
| Subtle schedule bugs | `scheduler.step()` first | Call `optimizer.step()` first |
| Long warmup does not scale | Warmup set as an absolute count | Set warmup as a fraction of total steps |

## Questions
type: qa
from: phases/03-deep-learning-core/06-optimizers/docs/en.md, phases/03-deep-learning-core/09-learning-rate-schedules/docs/en.md, phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/docs/en.md

- **Why warmup for Adam?** Its moment estimates start at zero. Early steps use bad statistics.
- **Why AdamW over Adam plus L2?** Every parameter gets the same proportional shrinkage.
- **Why SGD for CNNs?** SGD finds flatter minima. They often generalize better.
- **What does 1cycle need?** The total number of steps in advance.
- **Does accumulation save time?** No. Doubling `accum_steps` doubles the wall time per optimizer step.
- **Does CPU autocast need loss scaling?** No. CPU autocast uses BF16 by default.
Code and artifacts 1
What you take away

Saved in your notebook on this device.

Sources and limits

Reading checkpoints are self-reported. Test results are recorded separately and apply to the executed cases.

AI Engineering from Scratch · 3f9715c · MIT

Download this file ↓