Checkpoint Formats and State Dicts
A checkpoint is a dictionary from parameter names to tensors plus a pile of implicit assumptions about dtype, sharding, and key naming, and every one of those assumptions is somewhere a load goes wrong.
torch.save(model.state_dict(), path) looks like serialising a model. It serialises a dict from string keys to tensors, and everything that makes those tensors meaningful, the module class, the config, the parallelism layout they were sharded under, lives outside the file. Loading is therefore a reconstruction, not a restore, and reconstruction is where the failures are.
What is actually in the dict
Keys are dotted module paths: model.layers.0.self_attn.q_proj.weight. Values are tensors carrying their own dtype and shape. Buffers registered with register_buffer are included; anything held as a plain Python attribute is not, which is why a manually-cached RoPE table can vanish across a save/load cycle without any warning.
load_state_dict returns a named tuple of missing_keys and unexpected_keys and, with strict=False, does not raise. Ignoring that return value is the single most common way to end up serving a model with a randomly initialised layer. The behaviour is not a bug; the silence is the point of strict=False, and it is the caller's job to inspect what came back.
Pickle, and why the ecosystem moved
The default PyTorch format is Python pickle, which is an instruction stream for reconstructing objects. Its GLOBAL opcode imports a symbol and REDUCE calls it, so loading a checkpoint executes code from the file. A model downloaded from an untrusted source is executable content, not data.
safetensors removes both the execution and a copy. The layout is a JSON header giving each tensor's name, dtype, shape, and byte offsets, followed by raw tensor bytes. Nothing is interpreted, and because tensors are contiguous byte ranges, the file can be memory-mapped and read zero-copy rather than deserialised into new allocations. It is now the default distribution format on the Hugging Face Hub.
Large models ship sharded: model-00001-of-00004.safetensors and friends plus a model.safetensors.index.json mapping every parameter name to its shard. The index is the load plan; a shard-count mismatch against the index is a corrupt download, and it surfaces as a missing-key error rather than as an I/O error.
Training state is the larger half
An inference checkpoint holds weights. A resumable training checkpoint holds weights, optimiser state, learning-rate scheduler state, the data-loader position, and RNG state. Adam alone stores two fp32 moments per parameter, so with fp32 master weights the optimiser state is roughly twice the size of the model, and a checkpoint that omits it produces a resume that looks fine and behaves like a fresh restart with a warm init.
Sharded training adds a second problem: a checkpoint written under one parallelism layout is not directly loadable under another. Resharding, whether by gathering to a full state dict or by using a distributed checkpoint format that stores per-tensor sharding metadata, is what makes "resume on a different node count" possible, and it is not something you can bolt on after the run has started.
When it breaks
Dtype is decided at load, not by the file. Weights saved in bf16 and loaded into an fp32 model are upcast silently, doubling memory and changing arithmetic; the reverse downcasts and loses the low bits of any parameter that needed them. Nothing in the format prevents either.
Key naming drifts with code. torch.compile wraps modules and prefixes keys with _orig_mod.; DataParallel and DDP add module.; a refactor that renames an attribute renames every key beneath it. The remapping code that accumulates around a long-lived training script is technical debt with a real failure rate, since a regex that strips one prefix too many turns a load error into a silent partial load.
Atomicity is not handled for you. A process killed mid-write leaves a truncated file that fails to parse hours later when someone tries to resume. Write to a temporary path and rename, keep more than one generation, and verify the newest checkpoint loads before deleting the one before it. Checkpoint corruption discovered at resume time is how a fault-tolerant run loses a day.
12 flashcards for this concept
Click a card to reveal the answer.