Saving a GPT Model and Optimizer Checkpoint
Loading a GPT model and optimizer from a checkpoint is a two-part act: the program constructs live objects first, then attaches saved entries to them by key.
Why Save a Training State
Training a language model produces state that is worth keeping. The model's learned values change as training runs, and the optimizer's bookkeeping changes with them. A practical workflow therefore saves both to a file and reads them back later, instead of retraining from nothing each time.
This article starts from that saved file. The task is to rebuild a GPT model and its optimizer from a checkpoint, and then to compute loss values on the training data and on the validation data. The approach is code first: you read the loading steps as a trace, predict what each step produces, and then check your prediction against the explanation.
What do you think happens?
Before reading further, predict: if you could only load one thing from the checkpoint, which would you need to restore a model that can keep training, and why?
Reveal answer
Both the model entries and the optimizer entries
The source saves the model's learned values and the optimizer's bookkeeping together, because both change as training runs. Restoring only one of them leaves the other part of the training state missing.
Reading the Checkpoint File
The loading procedure uses five statements, and each one either reads from the checkpoint, builds an object, or attaches saved values to an object. Knowing which of the three a line does is the key to finding where a run goes wrong. Here is the sequence in order.
- 1Read the checkpoint file named
model_and_optimizer.pth. This is a read step: the file on disk becomes a Python object. - 2Construct a GPT model from the configuration called GPT_CONFIG_124M. This is a build step: a live model object now exists.
- 3Load saved values into that model. This is an attach step: entries from the checkpoint are poured into the model object.
- 4Construct an AdamW optimizer over the model's parameters. This is a build step: a live optimizer object now exists.
- 5Load saved values into the optimizer. This is an attach step: entries from the checkpoint are poured into the optimizer object.
The source describes the loading code as a flow of values. A file on disk is read into a Python object. That object holds saved entries under named keys. Two of those entries are then used to fill in two live objects, the model and the optimizer. The checkpoint is not the model, and the model is not the optimizer. The file is a container of named entries; the live objects are what the program builds and then fills.
Mapping Keys to Live Objects
The saved object holds two entries that matter for this procedure. The entry under model_state_dict feeds the GPT model, and the entry under optimizer_state_dict feeds the AdamW optimizer. Each key selects one saved group of values, and each group goes into exactly one live object.
Read the diagram from top to bottom. The file is the single source. Two keys pull two different groups out of it. Each group lands in a different live object. Nothing in the diagram crosses over: the model entries never go to the optimizer, and the optimizer entries never go to the model.
Why Build Before Loading
Loading is a two-part act. First, the program constructs a live object. Second, a saved entry is poured into that object. The order matters. The source calls load_state_dict on an object that already exists, so the object has to be built before any saved state can be attached to it.
Notice that the optimizer is built over model.parameters(), so the model must already exist before the optimizer can be constructed from it. The optimizer's saved state is then loaded last, after the optimizer object is in place.
Computing Train and Validation Loss
Once the model is restored, the source calculates two loss values for the GPT model: one for the training set and one for the validation set. Both are computed with a custom function named calc_loss_loader. The function is called twice, with the same model and the same device. Only the data loader changes between the two calls.
Because the model and device are held fixed, any difference between train_loss and val_loss comes from the data each call was given, not from a different model or a different device. The two values are separate results of the same function applied to two different inputs.
Tracing the two loss calls
Given a restored model, a device, a training loader, and a validation loader, show how the two loss values are produced.
- 1Fix the shared inputs
The model and device are passed to both calls unchanged. Nothing about them differs between the two calls.
- 2Call with the training loader
The first call to
calc_loss_loaderreceives the training loader. Its result is the training loss. - 3Call with the validation loader
The second call receives the validation loader. Its result is the validation loss.
- 4Attach each result to its label
The first returned value is labeled
train_lossand the second is labeledval_loss. The labels depend only on which loader each call received.
Two loss values, train_loss and val_loss, that differ only because the loader argument differs.
Finding Where a Run Goes Wrong
A run can fail in the loading path or in the loss-calculation path. The source points to specific checks that narrow the search. Work through the lines in order and ask, for each one, whether it reads, builds, or attaches.
Common mistake Swapping the two loader arguments
What it looks likePassing the validation loader to the first calc_loss_loader call and the training loader to the second.
Why it failsThe code still runs without complaint, but the two results end up attached to the wrong label.
Do this insteadCheck the loader argument of each call first. Confirm that the training loss comes from the training loader and the validation loss from the validation loader.
Common mistake Loading saved state before the object exists
What it looks likeCalling the optimizer load step before the AdamW optimizer has been constructed over the model's parameters.
Why it failsload_state_dictis called on an existing object. Before construction there is no optimizer object to receive the saved entries.Do this insteadKeep the build-then-attach order: construct the model, load its state, construct the optimizer, then load the optimizer state.
Common mistake Treating the checkpoint as the model
What it looks likeAssuming that reading model_and_optimizer.pth has already produced a working GPT model.
Why it failsThe file is a container of named entries. The live model is a separate object built by the program.
Do this insteadRemember that a live object is constructed first and a saved entry is then poured into it.
Common mistake Changing the model or device between the two loss calls
What it looks likeRestoring a different model for the validation call than for the training call.
Why it failsThe source holds the model and device fixed, so a difference in results would no longer be attributable to the data alone.
Do this insteadUse the same model and device in both calls and vary only the loader.
Practice Tracing the Restore
A colleague's restore run completes the file read and the GPT model build, then fails on the attach step for the optimizer. Using the five-step order, name the step that most likely went wrong first, and explain what the correct order would have required before that step could succeed.
Hints
- Count the steps in order: read, build model, load model, build optimizer, load optimizer.
- Ask which object the failing step is trying to attach saved entries to, and whether that object was built yet.
Two loss values come back from a run, and they are the same number. List the inputs you would check first, and state which of them could explain the result without any change to the model or device.
Hints
- Recall that the model and device are fixed across both calls.
- Check which loader was passed to each call.
Key Takeaways
- A checkpoint is a file read into a Python object that holds saved entries under named keys, not the model or optimizer itself.
- The GPT model is built from GPT_CONFIG_124M and loaded from
model_state_dict, while the AdamW optimizer is built overmodel.parameters()and loaded fromoptimizer_state_dict. - Loading is a two-part act: construct the live object first, then attach saved entries to it, because
load_state_dictis called on an existing object. - Training and validation losses come from two calls to
calc_loss_loaderthat share the same model and device and differ only in the loader passed, so a swapped loader is the most common silent error.