An optimizer is three decisions: a direction, a step size over time, and a way of living with noise. This section is the second decision — the schedule t \mapsto \eta_t.
Two facts force it to exist:
Constant \eta: SGD parks on a noise floor\propto \eta. The rate must come down.
The target rate is often lethal at initialization. The rate must first come up.
Theory ranks few shapes (the proofs that exist live in the math appendix), so we proceed empirically: one CNN, one dataset, every schedule.
A testbed
LeNet-style CNN on Fashion-MNIST, modernized: ReLU, max-pooling, BatchNorm after every hidden layer, Xavier init pinned in both frameworks (without the norm layers, survivable \eta is a seed-dependent coin flip).
A scheduler is any callable epoch -> learning rate.
The training loop consults it at the start of every epoch and writes the rate into the optimizer; nothing else changes.
Stateless by design: a pure function of t can be plotted, resumed, and branched — that pays off at the end.
Baseline: constant \eta = 0.3
train loss 0.064, train acc 0.977, test acc 0.892
Both failure modes on one plot: loss noisy to the end (riding the noise floor), and test accuracy stalls while train accuracy climbs — overfitting.
Square-root decay
\eta_t = \eta_0 (t+1)^{-1/2} — the convex-optimal rate from 9.3. A scheduler is just a callable:
class SquareRootScheduler:def__init__(self, lr=0.1):self.lr = lrdef__call__(self, epoch):returnself.lr *pow(epoch +1.0, -0.5)scheduler = SquareRootScheduler(lr=0.3)d2l.plot(d2l.arange(num_epochs), [scheduler(t) for t inrange(num_epochs)], xlabel='epoch', ylabel='learning rate')
Square-root decay: training
train loss 0.095, train acc 0.968, test acc 0.901
Smoother and quieter — but test accuracy lands below the constant baseline. Timid at both ends: gives up the high early rate within ~3 epochs, yet ends with the largest tail \eta of any decay here. Shape matters.
One parameter, no milestones, no kinks (Loshchilov & Hutter, 2016).
train loss 0.094, train acc 0.968, test acc 0.908
The stay-high, decay-hard shapes (multiplicative, piecewise, cosine) beat the baseline and are too close to call from single runs. Cosine won on convenience, not measured superiority.
Warmup: the other end of the schedule
At initialization the loss surface is sharp; the target rate can kill the run in step one.
Standard fix since Goyal et al. (2017): linear ramp from \approx 0 over the first epochs.
Mechanism (Kalra & Barkeshli, 2024): early training at a growing rate reduces sharpness, raising the stability ceiling before the full rate arrives.
Adam has a second reason: an estimated preconditioner should not be trusted cold.
Warmup: dead vs. alive
Even a BatchNorm net has a ceiling. Cold start at \eta = 7.5 (25× the baseline) — collapsed to chance within one epoch:
train loss 2.314, train acc 0.099, test acc 0.100
Same rate through a 5-epoch ramp — trains to 80–90%:
train loss 0.215, train acc 0.918, test acc 0.895
Warmup + cosine
The default recipe of the late 2010s, and still strong:
train loss 0.048, train acc 0.987, test acc 0.906
Cosine’s hidden defect
The horizon T is baked in from step one.
Mid-run checkpoints: rate never came down → not finished models.
Want 2× the budget after the fact? Retrain.
Scaling-law study at 5 budgets? 5 full runs.
Warmup–stable–decay (MiniCPM; Hu et al., 2024): warm up, hold the peak constant for most of the run, decay in the last 10–20%. Every plateau checkpoint is horizon-free; the decay is a harvest step.
WSD: the loss cliff
train loss 0.032, train acc 0.993, test acc 0.908
Plateau loss sits above cosine’s, then drops in a cliff when the decay begins. Same final range as cosine at the same budget.
The river valley
Why the cliff (Wen et al., 2024): the loss surface is a winding valley — steep walls, gently sloping floor.
High constant rate: the iterate bounces between walls while drifting fast along the floor. Measured loss is inflated; progress is real.
Decay quenches the bouncing → the iterate settles to the floor it already reached → cliff.
The plateau travels, the decay lands. The noise-ball story of 9.3, upgraded from a bowl to a valley.
Branching off the plateau
Train warmup + stable only (no horizon committed), keep going as long as you like, then clone and decay whenever you want a finished model:
The branch matches the full cosine run’s range — with the horizon chosen after the fact. One long run + cheap branched decays = models at many budgets (Hägele et al., 2024).
Plain SGD carries no state, so cloning parameters sufficed. With momentum or Adam, branch the optimizer state too.
State of play
Linear decay to zero matched or beat cosine and WSD in careful LLM sweeps (Bergsma et al., 2025) — the final rate matters most.
Schedule-free (Defazio et al., 2024): constant rate; gradients at an interpolation of iterate and average, evaluate the average — decay by averaging, horizon always “now”.
Not settled: GLM-4.5 ablated WSD vs. cosine and shipped cosine (Zeng et al., 2025). Differences at matched tuning are small.
What is settled: schedules are cheap, they matter, and plateau checkpoints you can decay on demand beat a horizon baked in at step one.