Checkpoints: saving training to resume it
Read these first if they are new to you
- Minibatches: a few examples per step Intermediate
- Reading Python Basic
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).
A model has 100 million weights, each stored as a 4-byte number.
- The weights take 100,000,000 × 4 bytes = 400 MB.
- AdamW keeps two numbers per weight: 2 × 400 MB = 800 MB.
- An averaged copy of the weights adds another 400 MB.
- 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.)
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.
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.
- The job stops at step 47,350. The latest checkpoint is from step 46,000.
- The restarted job repeats 47,350 − 46,000 = 1,350 steps: 1,350 ÷ 3 = 450 seconds, or 7.5 minutes of lost work.
- The worst case is a stop just before a save: 2,000 steps, about 11 minutes.
- 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%.
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.
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
-
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.
-
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.
-
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.
-
The standard save-and-resume code, with the model and optimizer state in one dictionary.
Where this shows up
Units that build on this one
- The training loop Intermediate