Concept units

Checkpoints: saving training to resume it

Intermediate

Read these first if they are new to you

The idea in one paragraph

Training a model can take hours or days, and a job can stop at any moment: a time limit, a crash, a machine that is needed elsewhere. A checkpoint is a file that saves everything needed to carry on: the weights, the optimizer's own memory, and the step count. The program writes one every so often. After a stop, it loads the latest checkpoint and continues as if nothing had happened. Only the steps since that save are lost.

A checkpoint holds more than the weights

The weights alone are enough to use a finished model. To continue training, you need everything that the next step depends on:

  • The model's weights.
  • The optimizer state: the numbers the optimizer keeps between steps. For example, AdamW keeps two running averages for every weight. Without them, the first steps after a resume behave differently.
  • The step count, so that the learning-rate schedule continues from the right place and the run knows when to stop.
  • Any extra copies, such as the averaged weights.
  • The state of the random-number generators, so that the batches and random choices that follow are the ones the run would have made (see random draws).
Worked example: how big is a checkpoint?

A model has 100 million weights, each stored as a 4-byte number.

  1. The weights take 100,000,000 × 4 bytes = 400 MB.
  2. AdamW keeps two numbers per weight: 2 × 400 MB = 800 MB.
  3. An averaged copy of the weights adds another 400 MB.
  4. The step count and generator states take a few kilobytes.

The checkpoint is about 1.6 GB, four times the size of the weights alone.

Your turn. How big is the checkpoint for a model with 25 million weights, with AdamW and no averaged copy? (Answer: 100 MB + 200 MB = 300 MB.)

What fills a 1.6 GB checkpoint
400 MB 800 MB 400 MB weights optimizer state averaged copy step count and generator states: a few kB
What to notice. The optimizer state is the largest part. A file with only the weights is enough to use the model, but too little to continue training it.

A full resume continues exactly where training stopped

The snippet runs the same training in four parts. Run A takes 20 steps without stopping. Run B takes 10 steps and saves a checkpoint. Run C loads that checkpoint into new objects and takes the other 10 steps. Run D loads only the weights and starts a fresh optimizer.

resume.py

import torch

def make():  # a linear layer (3 inputs, 1 output), with AdamW for its weights
    model = torch.nn.Linear(3, 1)
    return model, torch.optim.AdamW(model.parameters(), lr=0.01)

def train(model, opt, steps):
    for _ in range(steps):                        # _ is a name the loop never uses
        x = torch.randn(8, 3)                     # a random minibatch
        # The loss is the mean squared gap between the output and each row's sum.
        # keepdim=True keeps the sums in a column, the shape of model(x).
        loss = (model(x) - x.sum(1, keepdim=True)).pow(2).mean()
        # Clear the old gradients, compute new ones, and take one step.
        opt.zero_grad(); loss.backward(); opt.step()

torch.manual_seed(0)                              # run A: 20 steps without stopping
a, opt_a = make()
train(a, opt_a, 20)

torch.manual_seed(0)                              # run B: 10 steps, save, stop
b, opt_b = make()
train(b, opt_b, 10)
torch.save({"model": b.state_dict(), "optimizer": opt_b.state_dict(),
            "step": 10, "rng": torch.get_rng_state()}, "ckpt.pt")

c, opt_c = make()                                 # later: new objects, then resume
ck = torch.load("ckpt.pt")
c.load_state_dict(ck["model"]); opt_c.load_state_dict(ck["optimizer"])
torch.set_rng_state(ck["rng"])
train(c, opt_c, 20 - ck["step"])

d, opt_d = make()                                 # resume, but forget the optimizer
d.load_state_dict(ck["model"])
torch.set_rng_state(ck["rng"])
train(d, opt_d, 20 - ck["step"])

# torch.equal is True only when two tensors hold exactly the same numbers.
print("full resume matches run A:   ", torch.equal(a.weight, c.weight))
print("without optimizer state:     ", torch.equal(a.weight, d.weight))
full resume matches run A:    True
without optimizer state:      False

With everything saved, the resumed run ends with exactly the same weights as the run that never stopped. Without the optimizer state, it ends somewhere else. A state_dict() is PyTorch's name for all of an object's numbers, gathered into a dictionary that torch.save can write. torch.manual_seed(0) starts PyTorch's random numbers from the seed 0, so runs A and B start from the same weights and draw the same minibatches. torch.get_rng_state() reads where PyTorch's generator is in its sequence, and torch.set_rng_state puts it back there (see random draws).

Save often enough to lose little, rarely enough to stay fast

Saving takes time, during which training pauses. Saving rarely risks losing more work when the job stops. The interval between saves trades one against the other.

Worked example: a stop at step 47,350

A run takes 3 steps per second. It saves every 2,000 steps, which is every 2,000 ÷ 3 ≈ 667 seconds, about 11 minutes. Each save takes 20 seconds.

  1. The job stops at step 47,350. The latest checkpoint is from step 46,000.
  2. The restarted job repeats 47,350 − 46,000 = 1,350 steps: 1,350 ÷ 3 = 450 seconds, or 7.5 minutes of lost work.
  3. The worst case is a stop just before a save: 2,000 steps, about 11 minutes.
  4. Saving costs 20 seconds out of every 667 + 20, so about 3% of the run time.

Try it. The figure repeats the worked example with an interval you choose. Find an interval that keeps the worst-case loss under 15 minutes and the time spent saving under 3%.

Checkpoints along a run that stops at step 47,350

Lost work and saving time.

What to notice. The shaded stretch is the work that the restarted job repeats. Shorter intervals shrink it but add more pauses for saving.

Write each checkpoint safely

  • Write to a temporary file, then rename it to the real name. A rename happens all at once, so a stop during the save leaves the previous checkpoint whole.
  • Keep the last few checkpoints, not only the latest, in case one is damaged.
  • Keep the checkpoint with the lowest validation loss as well; that is often the model you want in the end.
This is a simplification

The snippet resumes exactly because its batches come from one random generator. A real run also needs to know where it was in the shuffled order of the current epoch. With several data-loader workers or several GPUs, there are several generators. Some GPU operations also give slightly different results from run to run. Many projects therefore accept a resume that is close rather than identical.

Check yourself

  1. A colleague saves only model.state_dict() every hour and trains with AdamW. What goes wrong when the job restarts?

    Answer

    The optimizer starts with empty running averages and the step count starts from zero, so the learning-rate schedule restarts too. Training continues, but not as the original run would have. The snippet shows the weights end up different.

  2. A run takes 5 steps per second and saves every 3,000 steps. What is the most work it can lose, in minutes?

    Answer

    3,000 ÷ 5 = 600 seconds, which is 10 minutes.

  3. Why write the checkpoint to a temporary file first and then rename it?

    Answer

    If the job stops in the middle of writing, the half-written file has the temporary name, and the last good checkpoint is untouched. Writing straight over the old file could leave no good checkpoint at all.

Go further

Optional background, for readers who want more depth.

Where this shows up

Units that build on this one