TAI BUI
Back to curriculum
BuildPython

Learning Rate Schedules and Warmup

The learning rate is the single most important hyperparameter you'll tune. Not the architecture, not the dataset size, not the activation function. If you tune nothing else, tune this.

TIME: ~90 minutesPREREQUISITES: Lesson 03.06 (Optimizers), Lesson 03.08 (Weight Initialization)

Learning Objectives

  • Implement constant, step decay, cosine annealing, warmup + cosine, and 1cycle learning rate schedules from scratch
  • Demonstrate the three failure modes of learning rate selection: divergence (too high), stalling (too low), and oscillation (no decay)
  • Explain why warmup is necessary for Adam-based optimizers and how it stabilizes early training
  • Compare convergence speed across all five schedules on the same task and select the appropriate one for a given training budget

The Problem

Set the learning rate to 0.1 and training diverges: loss jumps to infinity in three steps. Set it to 0.0001 and training crawls: after 100 epochs, the model has barely moved from random. Set it to 0.01 and training looks fine for 50 epochs, then the loss starts oscillating around a minimum it can never reach because the steps are too large.

The optimal learning rate isn't a constant. It changes as training progresses: early on you want large steps that cover ground fast, and late in training you want tiny steps that settle into a sharp minimum. The gap between a 90% accurate model and a 95% accurate model is often nothing but the schedule.

Every major model released in the last three years uses one. Llama 3 ran a peak lr of 3e-4 with 2000 warmup steps and cosine decay down to 3e-5. GPT-3 used lr=6e-4 with warmup spread over 375 million tokens. Neither number is arbitrary — both came out of hyperparameter sweeps that cost millions of dollars to run.

You need to understand schedules because the defaults won't fit your problem. Fine-tuning a pretrained model calls for a different schedule than training from scratch. Bump the batch size and the warmup period has to change with it. And when training breaks at step 10,000, you need to know whether the schedule is the culprit or something else entirely.

Warm-up0/3
1. You are parking a car. As you approach the final spot, what is the sensible way to adjust your speed?Warm-up
Top-down sketch of a car on the left with an arrow towards an empty parking spot next to a parked car, labelled distance still to cover.
Illustrative sketch.
2. From the optimizers lesson, what are Adam's running moment estimates like in the very first few steps of training, before it has seen many gradients?Warm-up
Adam's two running-average formulas with default betas 0.9 and 0.999 and its update formula, above rows of squares showing 1, 2, 3, 5 and 12 gradients seen after the matching training step, and a last row for 100.
The first few training steps, counted by gradients seen.
3. In gradient descent you subtract the gradient times a step size. What tends to happen near a minimum if that step size is far too large?Warm-up
Parabola f(w) = w^2 with its minimum marked at w = 0 and the current point at w = 0.6, beside a card showing the gradient 2w = 1.2 and the update w <- w - lr * 1.2.
Illustrative numbers.
Check: The Problem0/4
1. What is the learning rate in gradient-based training?Define
Three number lines for a weight w = 0.80 and gradient 0.50, each updated with a different lr: lr 0.01 gives new w 0.795, lr 0.1 gives 0.75, lr 1 gives 0.3.
Illustrative numbers, computed as w - lr * gradient.
2. You train the same model at three learning rates. At lr=0.1 the loss explodes to infinity within 3 steps. At lr=0.0001 the loss has barely moved after 100 epochs. At lr=0.01 training looks fine for 50 epochs, then the loss oscillates around a minimum it never reaches. Which run shows the stalling failure mode?Explain
Three small loss-against-epoch panels for lr 0.1, lr 0.0001 and lr 0.01: the first leaves the top of the chart within three epochs, the second is almost flat near its starting value for 100 epochs, the third falls and from about epoch 50 jitters around a level above zero.
Schematic sketches of the stem's description, not measured runs; no loss values are implied.
3. Llama 3 used a peak learning rate of 3e-4 and cosine decay down to 3e-5. By what factor does the rate fall from peak to the end of the schedule?Apply
Cosine decay curve for Llama 3 from the peak rate 3e-4 at the start down to 3e-5 at the end of the schedule, with horizontal gridlines at 0, 3e-5 and 3e-4.
Lesson's cosine_schedule with lr 3e-4 and lr_min 3e-5; the x axis is progress, not a step count.
4. A constant lr=0.01 run drops the loss quickly, then hovers and jitters around a value for the next 50 epochs without improving. Which change addresses this?Analyze
Loss against epoch for a toy noisy quadratic with constant lr 0.01 on a log axis: it falls from 10 to about 0.03 within roughly 10 epochs and then jitters between about 0.001 and 0.1 through epoch 70.
Toy problem computed with numpy (one step per epoch, gradient noise 3), not the lesson's network; illustrative.

The Concept

Constant Learning Rate

The simplest approach: pick a number and use it for every step.

lr(t) = lr_0

It's rarely optimal. Either it's too high for the end of training, which causes oscillation around the minimum, or it's too low for the beginning, which wastes compute on tiny steps. Fine for small models and quick debugging. A terrible choice for anything that trains longer than an hour.

Step Decay

The old-school approach from the ResNet era: cut the learning rate by a factor (usually 10x) at fixed epochs.

lr(t) = lr_0 * gamma^(floor(epoch / step_size))

With gamma = 0.1 and step_size = 30, lr drops by 10x every 30 epochs. ResNet-50 used exactly this setup: lr=0.1, dropped by 10x at epochs 30, 60, and 90.

The problem is that the optimal decay points depend on the dataset and architecture, so moving to a different problem means re-tuning when to drop. The transitions are abrupt too, and loss can spike right when the rate changes.

Cosine Annealing

Smooth decay from the maximum learning rate down to a minimum, following a cosine curve:

lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * t / T))

Where t is the current step and T is the total number of steps.

At t=0, the cosine term is 1, so lr = lr_max. At t=T, the cosine term is -1, so lr = lr_min. The decay stays gentle at first, picks up speed through the middle, then eases off again near the end.

This is the default for most modern training runs, and it needs no hyperparameters beyond lr_max and lr_min. The cosine shape matches an empirical observation: most of the learning happens in the middle of training, so that's where you want the step sizes to stay reasonable.

Warmup: Why You Start Small

Warmup means starting with a tiny learning rate (often lr_max / warmup_steps, sometimes zero) and ramping it linearly up to lr_max over the first N steps.

lr(t) = lr_max * (t / warmup_steps)     for t < warmup_steps

The reason: Adam and other adaptive optimizers keep running estimates of gradient mean and variance. At step 0 those estimates are initialized to zero, so the first few gradient updates are based on garbage statistics. If the learning rate is large during this window, the model takes huge, poorly-directed steps. By the time training reaches the full learning rate, Adam's statistics have already stabilized.

Typical warmup runs 1-5% of total training steps. Llama 3 trained for roughly 1.8 trillion tokens and warmed up for 2000 steps. GPT-3 warmed up over 375 million tokens.

Linear Warmup + Cosine Decay

The modern default: ramp up linearly, then decay with cosine.

if t < warmup_steps:
    lr(t) = lr_max * (t / warmup_steps)
else:
    progress = (t - warmup_steps) / (total_steps - warmup_steps)
    lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * progress))

This is what Llama, GPT, PaLM, and most modern transformers run. Warmup kills the early instability, then cosine decay settles the model into a good minimum.

1cycle Policy

Leslie Smith's discovery (2018): ramp the learning rate up from a low value to a high value in the first half of training, then ramp it back down in the second half. It sounds backwards — why would increasing the learning rate midway through training help anything?

The theory is that a high learning rate acts like regularization, adding noise to the optimization trajectory. During the ramp-up phase the model explores more of the loss landscape and finds better basins. The ramp-down phase then refines within whichever basin it found.

Phase 1 (0 to T/2):    lr ramps from lr_max/25 to lr_max
Phase 2 (T/2 to T):    lr ramps from lr_max to lr_max/10000

1cycle often trains faster than cosine annealing for the same compute budget. The catch is you need to know the total number of steps ahead of time.

Schedule Shapes

Mermaid diagram

Decision Flowchart

Mermaid diagram

Real Numbers from Published Models

Mermaid diagram

Check: The Concept0/23
1. What is a learning rate schedule?Define
Five calls of the lesson's warmup_cosine_schedule with lr 0.01, total_steps 1000 and warmup_steps 100, listing the step and the returned rate (0.000000, 0.005000, 0.010000, 0.005872, 0.000010), next to a small plot of the curve with those five points marked.
Values from running the lesson's code.
2. What is learning rate warmup?Define
Learning rate over the first 4000 steps of a run with peak 3e-4 and 2000 warmup steps: a straight line up from 0 at step 0 to 3e-4 at step 2000, then a slow fall.
Lesson's warmup_cosine_schedule with the Llama 3 numbers; total_steps 100000 and lr_min 1e-5 assumed for the decay part.
3. What is cosine annealing?Define
Plot of the lesson's cosine_schedule over 1000 steps with lr 0.01 and lr_min 1e-5: a smooth S-shaped curve from the top left to the bottom right, with the cosine formula beneath.
Values from running the lesson's code.
4. What is step decay?Define
Staircase plot on a log scale of the ResNet-50 recipe over 100 epochs: lr 0.1 at the start, with a tenfold fall at each of epochs 30, 60 and 90.
Computed with the lesson's step_decay_schedule.
5. What is the 1cycle policy?Define
Printed rows of the lesson's one_cycle_schedule with lr 0.01 and total_steps 1000 at steps 0, 125, 250, 375, 500, 625, 750, 875 and 999, each with its rate and a bar of # characters scaled to the largest rate.
Values from running the lesson's code; rows chosen, same line format as visualize_schedule.
6. What is an LR range test?Define
Loss against learning rate on a log axis for a 100-epoch run of the lesson's circle network whose lr grows exponentially from 1e-5 to 10 over the run: the loss starts near 0.48, falls to about 0.04 and then climbs again towards 0.21 at the largest rates.
Real run of the lesson's train_with_schedule; each point is one epoch's mean loss at that epoch's mid-point lr.
7. What does cosine annealing with warm restarts (SGDR) do?Define
Learning rate over 900 steps shaped as three consecutive cosine curves of 300 steps each, every one starting at 0.01 and falling towards 1e-5.
Illustrative: cycle length 300 chosen for the picture, values from the lesson's cosine_schedule.
8. What is the peak learning rate of a schedule?Define
Learning rate over a 100000-step run with 2000 warmup steps and peak 3e-4: a very steep rise at the start followed by a long smooth fall to near zero at the end.
Lesson's warmup_cosine_schedule; lr 3e-4 and 2000 warmup steps from the lesson's Use It example, lr_min 1e-5 from its code default.
9. Why is a constant learning rate usually suboptimal for a long training run?Explain
Plot of a constant learning rate: a flat horizontal line labelled lr_0 across a training-progress axis divided into early, middle and late parts.
Illustrative sketch of the constant schedule from the lesson.
10. Why does moving a step decay recipe to a new problem usually require re-tuning?Explain
Left, the ResNet-50 step-decay recipe as a log-scale staircase with drops at epochs 30, 60 and 90; right, an empty dashed box labelled new dataset, new model, drop epochs unknown.
Left panel computed with the lesson's step_decay_schedule.
11. In the cosine annealing formula, what learning rate does the schedule produce at t=0 versus at t=T?Explain
The cosine annealing formula above a half circle on which the angle pi * t / T sweeps from 0 at the point labelled t = 0 on the right, through the top at t = T/2, to pi at the point labelled t = T on the left.
Where the cosine term's angle sits at three moments of training.
12. Why do Adam-style optimizers need learning rate warmup?Explain
Adam's update formula with m as a running average of gradients and v of squared gradients, above four bars for the warmup learning rate lr_max * t / warmup_steps: 0.0000 at step 0, 0.0025 at step 25, 0.0050 at step 50 and 0.0100 at step 100.
Illustrative: lr_max 0.01 and warmup_steps 100 chosen for the picture; values from lr_max * t / warmup_steps.
13. Which schedule is the modern default for transformers such as Llama and GPT, and what does each phase do?Explain
Table of four published runs from the lesson with peak lr and warmup: Llama 3 (405B) 3e-4 and 2000 steps, GPT-3 (175B) 6e-4 and 375M tokens, BERT (340M) 1e-4 and 10K steps, ResNet-50 0.1 and none.
Peak and warmup columns of the lesson's Real Numbers diagram; its decay column is left out.
14. Why can raising the learning rate in the first half of 1cycle help?Explain
Plot of the lesson's one_cycle_schedule over 1000 steps with lr 0.01, with the first half (steps 0 to 500) shaded and labelled first half.
Values from running the lesson's code.
15. You want to use 1cycle, but you train with early stopping and will not know the total number of steps until training ends. What is the problem?Explain
The signature of the lesson's one_cycle_schedule with its total_steps parameter, above a timeline that starts at step 0 and ends in a question mark where early stopping will end the run.
Signature copied from the lesson's code.
16. You follow the ResNet-50 recipe: lr=0.1, dropped by 10x at epochs 30, 60 and 90. What learning rate applies at epoch 45?Apply
Staircase plot on a log scale of the ResNet-50 recipe with lr 0.1 and tenfold drops at epochs 30, 60 and 90, with a dashed vertical line at epoch 45.
Computed with the lesson's step_decay_schedule.
17. You train with Adam at the full peak rate from step 0. Loss spikes and diverges within the first few hundred steps, yet a rerun with the rate ramped up over the first 5 percent of steps trains smoothly. What explains the difference?Analyze
Learning rate over the first 20 percent of training for two runs: run A sits at the peak rate from step 0, run B climbs linearly from 0 to the peak over the first 5 percent and then stays at the peak.
Sketch of the stem's two runs; the plot stops at 20 percent and the later decay is not shown.
18. A team reuses the ResNet recipe (drops at epochs 30, 60, 90) on a new dataset. Accuracy stalls near epoch 20 and only improves when the first drop arrives at epoch 30. What is the likely wrong assumption?Analyze
Two stacked panels over epochs 0 to 40: the ResNet recipe's learning rate at 0.1 until a tenfold drop at epoch 30, and a schematic accuracy curve that rises, goes flat from about epoch 20 and rises again after the drop at 30.
Learning rate computed with the lesson's step_decay_schedule; accuracy is a schematic of the stem, no values implied.
19. You configured 1cycle for 1000 steps but stop the run at step 400. Why is the model likely worse than if it had run to 1000?Analyze
The 1cycle schedule configured for 1000 steps, plotted as lr relative to the peak: a rise from near 0 to the peak at step 500 and a fall to near 0 at step 1000, with a dashed vertical line at step 400.
Computed with the lesson's one_cycle_schedule with lr = 1, so the y axis reads as a fraction of the peak.
20. A loss spike shows up at exactly the same step every time you rerun training with the same seed. What should you suspect first?Analyze
Three stacked loss curves labelled rerun 1, rerun 2 and rerun 3, each a falling curve with one sharp spike, the spike at the same horizontal position in all three, marked by a dashed vertical line.
Schematic: no step numbers or loss values are implied.
21. You copy a 2000-step warmup from a paper that trained at batch size 256, but you train at batch size 1024. What is the risk?Analyze
Table comparing the paper and your run: batch size 256 versus 1024, warmup 2000 optimizer steps in both, and 256 versus 1024 examples per optimizer step.
Numbers from the stem.
22. Your total step count is fixed in advance and your goal is the fastest convergence for the compute you have. Which schedule fits best?Design
Table of the lesson's five schedule functions with a small plot of each over 500 steps at lr 0.05 and the arguments each takes besides step and lr: constant none, step decay step_size and gamma, cosine total_steps and lr_min, warmup cosine total_steps, warmup_steps and lr_min, 1cycle total_steps.
Shapes use the same settings as the visualize_schedule calls in the lesson's main.py; arguments read from the function signatures.
23. You will run the same recipe at batch sizes 256, 512 and 1024. How should you set warmup so it stays comparable across runs?Design
Table of optimizer steps per epoch on an illustrative dataset of 1,024,000 examples: 4,000 at batch size 256, 2,000 at 512 and 1,000 at 1024.
Illustrative dataset size; steps = examples / batch size.

Build It

Step 1: Schedule Functions

Each function takes the current step and returns the learning rate at that step.

python
import math
 
 
def constant_schedule(step, lr=0.01, **kwargs):
    return lr
 
 
def step_decay_schedule(step, lr=0.1, step_size=100, gamma=0.1, **kwargs):
    return lr * (gamma ** (step // step_size))
 
 
def cosine_schedule(step, lr=0.01, total_steps=1000, lr_min=1e-5, **kwargs):
    if step >= total_steps:
        return lr_min
    return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * step / total_steps))
 
 
def warmup_cosine_schedule(step, lr=0.01, total_steps=1000, warmup_steps=100, lr_min=1e-5, **kwargs):
    if total_steps <= warmup_steps:
        return lr * (step / max(warmup_steps, 1))
    if step < warmup_steps:
        return lr * step / warmup_steps
    progress = (step - warmup_steps) / (total_steps - warmup_steps)
    return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * progress))
 
 
def one_cycle_schedule(step, lr=0.01, total_steps=1000, **kwargs):
    mid = max(total_steps // 2, 1)
    if step < mid:
        return (lr / 25) + (lr - lr / 25) * step / mid
    else:
        progress = (step - mid) / max(total_steps - mid, 1)
        return lr * (1 - progress) + (lr / 10000) * progress

Step 2: Visualize All Schedules

Print a text-based plot showing how each schedule evolves over training.

python
def visualize_schedule(name, schedule_fn, total_steps=500, **kwargs):
    steps = list(range(0, total_steps, total_steps // 20))
    if total_steps - 1 not in steps:
        steps.append(total_steps - 1)
 
    lrs = [schedule_fn(s, total_steps=total_steps, **kwargs) for s in steps]
    max_lr = max(lrs) if max(lrs) > 0 else 1.0
 
    print(f"\n{name}:")
    for s, lr_val in zip(steps, lrs):
        bar_len = int(lr_val / max_lr * 40)
        bar = "#" * bar_len
        print(f"  Step {s:4d}: lr={lr_val:.6f} {bar}")

Step 3: Training Network

Same two-layer network on the circle dataset as the previous lessons, but now the learning rate changes over training instead of staying fixed.

python
import random
 
 
def sigmoid(x):
    x = max(-500, min(500, x))
    return 1.0 / (1.0 + math.exp(-x))
 
 
def relu(x):
    return max(0.0, x)
 
 
def relu_deriv(x):
    return 1.0 if x > 0 else 0.0
 
 
def make_circle_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], label))
    return data
 
 
def train_with_schedule(schedule_fn, schedule_name, data, epochs=300, base_lr=0.05, **kwargs):
    random.seed(0)
    hidden_size = 8
    total_steps = epochs * len(data)
 
    std = math.sqrt(2.0 / 2)
    w1 = [[random.gauss(0, std) for _ in range(2)] for _ in range(hidden_size)]
    b1 = [0.0] * hidden_size
    w2 = [random.gauss(0, std) for _ in range(hidden_size)]
    b2 = 0.0
 
    step = 0
    epoch_losses = []
 
    for epoch in range(epochs):
        total_loss = 0
        correct = 0
 
        for x, target in data:
            lr = schedule_fn(step, lr=base_lr, total_steps=total_steps, **kwargs)
 
            z1 = []
            h = []
            for i in range(hidden_size):
                z = w1[i][0] * x[0] + w1[i][1] * x[1] + b1[i]
                z1.append(z)
                h.append(relu(z))
 
            z2 = sum(w2[i] * h[i] for i in range(hidden_size)) + b2
            out = sigmoid(z2)
 
            error = out - target
            d_out = error * out * (1 - out)
 
            for i in range(hidden_size):
                d_h = d_out * w2[i] * relu_deriv(z1[i])
                w2[i] -= lr * d_out * h[i]
                for j in range(2):
                    w1[i][j] -= lr * d_h * x[j]
                b1[i] -= lr * d_h
            b2 -= lr * d_out
 
            total_loss += (out - target) ** 2
            if (out >= 0.5) == (target >= 0.5):
                correct += 1
            step += 1
 
        avg_loss = total_loss / len(data)
        accuracy = correct / len(data) * 100
        epoch_losses.append(avg_loss)
 
    return epoch_losses

Step 4: Compare All Schedules

Train the same network with each schedule and compare final loss and convergence behavior.

python
def compare_schedules(data):
    configs = [
        ("Constant", constant_schedule, {}),
        ("Step Decay", step_decay_schedule, {"step_size": 15000, "gamma": 0.1}),
        ("Cosine", cosine_schedule, {"lr_min": 1e-5}),
        ("Warmup+Cosine", warmup_cosine_schedule, {"warmup_steps": 3000, "lr_min": 1e-5}),
        ("1cycle", one_cycle_schedule, {}),
    ]
 
    print(f"\n{'Schedule':<20} {'Start Loss':>12} {'Mid Loss':>12} {'End Loss':>12} {'Best Loss':>12}")
    print("-" * 70)
 
    for name, schedule_fn, extra_kwargs in configs:
        losses = train_with_schedule(schedule_fn, name, data, epochs=300, base_lr=0.05, **extra_kwargs)
        mid_idx = len(losses) // 2
        best = min(losses)
        print(f"{name:<20} {losses[0]:>12.6f} {losses[mid_idx]:>12.6f} {losses[-1]:>12.6f} {best:>12.6f}")

Step 5: LR Too High vs Too Low

Demonstrate the three failure modes: too high (divergence), too low (crawling), and just right.

python
def lr_sensitivity(data):
    learning_rates = [1.0, 0.1, 0.01, 0.001, 0.0001]
 
    print("\nLR Sensitivity (constant schedule, 100 epochs):")
    print(f"  {'LR':>10} {'Start Loss':>12} {'End Loss':>12} {'Status':>15}")
    print("  " + "-" * 52)
 
    for lr in learning_rates:
        losses = train_with_schedule(constant_schedule, f"lr={lr}", data, epochs=100, base_lr=lr)
        start = losses[0]
        end = losses[-1]
 
        if end > start or math.isnan(end) or end > 1.0:
            status = "DIVERGED"
        elif end > start * 0.9:
            status = "BARELY MOVED"
        elif end < 0.15:
            status = "CONVERGED"
        else:
            status = "LEARNING"
 
        end_str = f"{end:.6f}" if not math.isnan(end) else "NaN"
        print(f"  {lr:>10.4f} {start:>12.6f} {end_str:>12} {status:>15}")
Check: Build It0/8
1. What does step_decay_schedule(250, lr=0.1, step_size=100, gamma=0.1) return?Apply
Staircase plot on a log scale of step_decay_schedule with lr 0.1, step_size 100 and gamma 0.1 over 400 steps, with a dashed vertical line at step 250.
Computed with the lesson's step_decay_schedule.
2. What does cosine_schedule(500, lr=0.01, total_steps=1000, lr_min=1e-5) return, approximately?Apply
Plot of cosine_schedule with lr 0.01, total_steps 1000 and lr_min 1e-5, with a dashed vertical line at step 500.
Values from running the lesson's code.
3. What does warmup_cosine_schedule(25, lr=0.01, total_steps=1000, warmup_steps=100) return?Apply
Plot of the first 300 steps of warmup_cosine_schedule with lr 0.01, total_steps 1000 and warmup_steps 100, with a dashed vertical line at step 25.
Values from running the lesson's code.
4. What does cosine_schedule(1200, lr=0.01, total_steps=1000, lr_min=1e-5) return?Apply
Plot of cosine_schedule with lr 0.01, total_steps 1000 and lr_min 1e-5 drawn from step 0 to step 1000, on an axis that runs to step 1400, with a vertical line at total_steps and a dashed one at step 1200.
Values from running the lesson's code; the curve is drawn only up to step 1000.
5. What does one_cycle_schedule(0, lr=0.01, total_steps=1000) return?Apply
Plot of the lesson's one_cycle_schedule with lr 0.01 and total_steps 1000 on a linear axis from 0 to 0.01, with an arrow pointing at step 0 on the left edge.
Values from running the lesson's code.
6. In compare_schedules, training runs 300 epochs over 200 samples and the schedule advances once per sample. The step decay config uses step_size=15000. How often does the rate drop, in epochs?Apply
Staircase plot on a log scale of the step decay config used in compare_schedules, lr 0.05, step_size 15000 and gamma 0.1, over 60000 steps with ticks every 15000 steps.
Computed with the lesson's step_decay_schedule; x axis in scheduler steps, not epochs.
7. In compare_schedules, total_steps is 300 * 200 = 60000 and warmup_steps is 3000. What share of training is warmup?Apply
Plot of the Warmup+Cosine config from compare_schedules over 60000 steps, lr 0.05, with a dashed vertical line at step 3000 labelled warmup_steps = 3000.
Computed with the lesson's warmup_cosine_schedule.
8. In lr_sensitivity, a 100-epoch run at lr=0.0001 starts at loss 0.330 and ends at 0.310. Which status does the code print?Apply
The lesson's status rule from lr_sensitivity (DIVERGED if end is NaN or above 1.0, BARELY MOVED if end exceeds 0.9 times start, CONVERGED if end is below 0.15, else LEARNING) above a one-row table: LR 0.0001, start loss 0.330, end loss 0.310, status unknown.
Rule copied from the lesson's main.py.

Use It

PyTorch ships these schedulers directly in torch.optim.lr_scheduler:

python
import torch
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR, OneCycleLR, StepLR
 
model = nn.Sequential(nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 1))
optimizer = optim.Adam(model.parameters(), lr=3e-4)
 
scheduler = CosineAnnealingLR(optimizer, T_max=1000, eta_min=1e-5)
 
for step in range(1000):
    loss = train_step(model, optimizer)
    scheduler.step()

For warmup + cosine, either write a lambda scheduler yourself or grab get_cosine_schedule_with_warmup from HuggingFace:

python
from transformers import get_cosine_schedule_with_warmup
 
scheduler = get_cosine_schedule_with_warmup(
    optimizer,
    num_warmup_steps=2000,
    num_training_steps=100000,
)

That HuggingFace function is what most Llama and GPT fine-tuning scripts actually call. When in doubt, use warmup + cosine with a 3-5% warmup fraction of total steps. It covers almost every case you'll run into.

Check: Use It0/4
1. In CosineAnnealingLR, what does the eta_min argument set?Define
The lesson's PyTorch code (Adam at lr 3e-4, CosineAnnealingLR with T_max 1000 and eta_min 1e-5) above a table of the real learning rate after 0, 250, 500 and 750 scheduler steps: 3.000e-04, 2.575e-04, 1.550e-04 and 5.247e-05.
Values from running torch.optim.lr_scheduler.CosineAnnealingLR; the rows stop at step 750.
2. After 1000 calls to scheduler.step() with CosineAnnealingLR(optimizer, T_max=1000, eta_min=1e-5), what is the optimizer's learning rate?Apply
The lesson's PyTorch code with Adam at lr 3e-4 and CosineAnnealingLR(T_max=1000, eta_min=1e-5) stepped 1000 times, above the real learning-rate curve from 3e-4 on a linear axis from 0 to 3.3e-4.
Curve from running torch.optim.lr_scheduler.CosineAnnealingLR.
3. You use PyTorch and need warmup plus cosine decay. Which route does the lesson give?Apply
A card with the import line from torch.optim.lr_scheduler (CosineAnnealingLR, OneCycleLR, StepLR) above the target curve: a 2000-step linear rise to 3e-4 followed by a cosine fall over 100000 steps.
Target curve from the lesson's warmup_cosine_schedule with lr_min 0.
4. You are fine-tuning a pretrained model for a short run and are unsure which schedule to use. What is the safe default?Design
A flow of three boxes: Pretrained model, then Short fine-tuning run, then LR schedule with a question mark.
Illustrative sketch.

Ship It

This lesson produces:

  • outputs/prompt-lr-schedule-advisor.md: a prompt that recommends the right learning rate schedule and hyperparameters for your training setup
Author's Note

The failure I see most often: someone copies a schedule wholesale out of a paper, changes the batch size, and can't figure out why training got unstable. Warmup is measured in optimizer steps, not epochs, and the number of optimizer steps scales inversely with batch size. Double the batch size and you're taking half as many steps to cover the same warmup window, so a 2000-step warmup that was fine at batch size 256 can be too short at batch size 1024. Tie warmup to a percentage of total steps — 3-5% is a safe default — and recompute it whenever the batch size changes instead of hardcoding a step count.

One more debugging heuristic worth keeping: if a loss spike shows up at the exact same step across repeated runs with the same seed, suspect the schedule first, usually the warmup-to-decay handoff or a step-decay boundary. If the spike moves around between runs, that's data or numerical instability, not the schedule. Checking the step number before touching anything else has saved me hours more than once.

Exercises

  1. Implement exponential decay: lr(t) = lr_0 * gamma^t where gamma = 0.999. Compare to cosine annealing on the circle dataset.

  2. Implement the learning rate range test (Leslie Smith): train for a few hundred steps while exponentially increasing the LR from 1e-7 to 1. Plot loss vs LR. The optimal max LR is just before the loss starts increasing.

  3. Train with warmup + cosine but vary the warmup length: 0%, 1%, 5%, 10%, 20% of total steps. Find the sweet spot where training is most stable.

  4. Implement cosine annealing with warm restarts (SGDR): reset the learning rate to lr_max every T steps and decay again. Compare to standard cosine on a longer training run.

  5. Build a "schedule surgeon" that monitors training loss and automatically switches from warmup to cosine when the loss stabilizes, and reduces lr if the loss plateaus for too long.

Key Terms

TermWhat people sayWhat it actually means
Learning rate"How fast the model learns"The scalar that multiplies the gradient to determine the parameter update size
Schedule"Change the LR over time"A function that maps training step to learning rate, designed to optimize convergence
Warmup"Start with a small LR"Linearly ramping the LR from near-zero to the target value over the first N steps to stabilize optimizer statistics
Cosine annealing"Smooth LR decay"Decreasing the LR following a cosine curve from lr_max to lr_min over training
Step decay"Drop LR at milestones"Multiplying the LR by a factor (usually 0.1) at fixed epoch intervals
1cycle policy"Up then down"Leslie Smith's method of ramping LR up then down in a single cycle for faster convergence
LR range test"Find the best learning rate"Training briefly while increasing LR to find the value where loss starts diverging
Cosine with warm restarts"Reset and repeat"Periodically resetting the LR to lr_max and decaying again (SGDR)
Eta min"The floor for the LR"The minimum learning rate that the schedule decays to
Peak learning rate"The maximum LR"The highest LR reached during training, typically after warmup

Further Reading

  • Loshchilov & Hutter, "SGDR: Stochastic Gradient Descent with Warm Restarts" (2017): introduced cosine annealing and warm restarts
  • Smith, "Super-Convergence: Very Fast Training of Neural Networks Using Large Learning Rates" (2018): the 1cycle policy paper
  • Touvron et al., "Llama 2: Open Foundation and Fine-Tuned Chat Models" (2023): documents the warmup + cosine schedule used at scale
  • Goyal et al., "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour" (2017): linear scaling rule and warmup for large batch training

Code

Ships With