Listen up. If you're building ML pipelines, understanding Saving and Loading in Python is non-negotiable. This is where models go from messy research scripts to production-grade engineering.
1Pytorch saving Part 1
A trained PyTorch model exists as a graph of Python objects sitting in RAM (or GPU memory) β the moment the process that holds it exits, that state is gone. Forty-eight hours of GPU training and a 99% accuracy score are worth nothing if you close the interpreter without explicitly writing the learned parameters to disk first.
This is why serialization is not an optional step tacked onto the end of a training script; it's the boundary between 'a model that exists' and 'a model you can actually use.' A production pipeline saves checkpoints during training (so a crash doesn't erase hours of progress) and saves a final artifact after training completes, so the model can be loaded again later β in a notebook, on a web server, or on a completely different machine.
PyTorch's answer to this problem is torch.save(), and the object it's almost always used to save is the model's state_dict β the learned weights and biases β rather than the model instance itself. The next sections unpack exactly what that dictionary contains and why it's the recommended unit of serialization.
# Neural Networks exist in RAM.
# You must save the weights to the hard drive.Metrics calculated successfully.
2Pytorch saving Part 2
Every nn.Module in PyTorch exposes its learned parameters through .state_dict(), which returns an OrderedDict mapping each layer's name (as defined in the model's __init__) to the tensor holding its current weights or biases β for example "fc1.weight" or "conv1.bias".
Crucially, the state_dict contains only numbers: the parameter tensors as they currently stand after training. It does not contain the Python class definition, the forward() method's logic, or any information about how those layers are connected β that architecture only exists in the code that defines the MyNetwork class itself.
Calling model.state_dict().keys() is a fast way to sanity-check what a model actually learned to store β every layer that has trainable parameters shows up as a key, in the exact order it was registered on the module.
# View the learned weights
print(model.state_dict().keys())Metrics calculated successfully.
3Pytorch saving Part 3
It's worth being precise about what model.state_dict() is not, because the wrong mental model here causes real bugs later. It is not a copy of your training data, not the Python source file, and not a snapshot of the optimizer's internal state (momentum buffers, learning-rate schedule) β that's a separate dictionary, optimizer.state_dict(), that you'd save alongside it only if you intend to resume training later.
What it is: a dictionary containing every learned weight and bias tensor for every layer that has trainable parameters, keyed by layer name. If your network has a self.fc1 = nn.Linear(784, 128), the returned dict will have entries like "fc1.weight" (shape [128, 784]) and "fc1.bias" (shape [128]).
This distinction matters the moment you go to save a model: saving just model.state_dict() is enough to run inference later, but if your pipeline needs to resume interrupted training, you need a checkpoint that bundles the model's state_dict together with the optimizer's state_dict and the current epoch number.
# The State DictionaryMetrics calculated successfully.
4Pytorch saving Part 4
torch.save() serializes a Python object to disk using a pickle-based binary format, with special handling so tensor storage is written efficiently rather than pickled naively. Calling torch.save(model.state_dict(), "my_awesome_model.pth") writes out just the weights-and-biases dictionary from the previous section, not the model instance itself.
The .pt and .pth extensions are pure convention β PyTorch doesn't inspect the filename to decide how to serialize anything, and either extension (or none at all) works identically. What actually matters is what you pass as the first argument: a state_dict produces a small, portable file containing only tensors; passing the model object itself produces a much more fragile artifact, which the next section explains.
Because the file is just a serialized dictionary of tensors, it can be inspected, copied, or version-controlled like any other binary artifact β there's no hidden 'live' Python process bundled inside it.
# Save the model weights to a file
torch.save(model.state_dict(), "my_awesome_model.pth")Metrics calculated successfully.
5Pytorch saving Part 5
PyTorch technically lets you call torch.save(model, "model.pth") to pickle the entire model object, architecture and all. It's tempting because loading it back looks simpler β torch.load("model.pth") alone hands you a ready-to-use model, no class instantiation required. That convenience is also exactly why it's discouraged for anything beyond a quick local experiment.
Pickling the full object ties the saved file to the *exact* class definition, module path, and even directory structure that existed at save time. Rename the class, move the file that defines it, or refactor your project layout, and loading the pickled model breaks with an import error β even though the underlying weights are perfectly fine.
The recommended pattern avoids all of this: save only model.state_dict(), and keep the class definition itself as ordinary version-controlled source code. Loading then becomes a two-step, code-independent process β instantiate the architecture from source, then inject the saved weights β which the next two sections walk through.
# Saving SafelyMetrics calculated successfully.
6Pytorch saving Part 6
Reloading a model saved via state_dict is a deliberate two-step process. First, you instantiate the model class exactly as it was defined during training β loaded_model = MyNetwork() β which creates a fresh module with randomly initialized weights and the correct layer structure, but none of the learned knowledge yet.
Second, you call loaded_model.load_state_dict(torch.load("my_awesome_model.pth")). torch.load() deserializes the saved file back into an OrderedDict of tensors, and load_state_dict() walks that dictionary, matching each key to the corresponding parameter on the freshly created model and copying the saved values in place.
Once that call succeeds, loaded_model is functionally identical to the model at the moment it was saved β same weights, same biases, same learned behavior β even though it was built from a brand-new instance of the class.
# 1. Create the empty architecture
loaded_model = MyNetwork()
# 2. Inject the saved weights into the architecture
loaded_model.load_state_dict(torch.load("my_awesome_model.pth"))Metrics calculated successfully.
7Pytorch saving Part 7
The saved .pth file contains raw numbers keyed by layer name β "fc1.weight", "fc1.bias", and so on β but nothing in that dictionary describes how those layers are wired together, what activation functions sit between them, or what forward() should do with an input tensor. That logic lives entirely in the MyNetwork class definition, which is ordinary Python code, not something torch.save() ever captured.
So instantiating MyNetwork() again isn't a formality β it's what recreates the actual computation graph and gives load_state_dict() a set of correctly-shaped parameter slots to copy values into. Without that empty architecture to load into, the saved tensors would just be a dictionary of numbers with no model to attach them to.
This is also why load_state_dict() fails loudly (RuntimeError: Missing key(s) or Unexpected key(s)) if the class definition has changed since saving β the keys in the checkpoint no longer line up with the parameters the fresh instance actually has.
# Rebuilding the EngineMetrics calculated successfully.
8Pytorch saving Part 8
The two-step save/load pattern solves the code-portability problem, but there's a second, separate portability issue lurking in the saved tensors themselves: the hardware they were trained on. Every tensor in a state_dict remembers which device it lived on β CPU or a specific GPU index β at the moment it was saved.
That device information travels with the file. If you don't account for it explicitly when loading on different hardware, torch.load() will try to recreate tensors on the exact same device they came from, which fails outright if that device doesn't exist on the machine doing the loading.
The next two sections walk through exactly when this bites you and the one-argument fix that makes a checkpoint hardware-agnostic.
# SYSTEM WARNING:
# ADA Protocol initiating...Metrics calculated successfully.
9Pytorch saving Part 9
Picture the concrete failure: you train on an NVIDIA GPU, so every tensor in the saved state_dict is tagged cuda:0. You email the .pth file to a friend running a laptop with no NVIDIA GPU at all. They run loaded_model.load_state_dict(torch.load("my_awesome_model.pth")) and instead of a working model, they get a RuntimeError β PyTorch tried to allocate those tensors on a CUDA device that simply doesn't exist on their machine.
This isn't a bug in their setup; it's torch.load() doing exactly what it's documented to do by default β restoring each tensor onto the device it was originally saved from. The file itself is perfectly valid; the mismatch is purely about where the bytes get materialized.
This exact scenario is extremely common in practice: models are frequently trained on GPU clusters and then deployed to CPU-only inference servers, or shared with collaborators who don't have matching hardware β which is precisely the problem the next section's fix addresses.
# ADA initializing device checks...Metrics calculated successfully.
10Pytorch saving Part 10
The fix is a single keyword argument on torch.load(): map_location. Passing torch.load("my_awesome_model.pth", map_location=torch.device('cpu')) tells PyTorch to remap every tensor in the checkpoint onto CPU memory during deserialization, regardless of which device they were originally saved from β no CUDA device required.
This is the standard, hardware-agnostic way to load a checkpoint of unknown origin. A common production pattern goes one step further and makes it dynamic: device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'), then torch.load(path, map_location=device) β the exact same loading code then works correctly whether it runs on a GPU server or a CPU-only laptop.
After loading with map_location, the tensors sit on the target device, but the model itself should still be moved there explicitly with loaded_model.to(device) before running inference, so the model and its inputs agree on where they live.
# DEFEND THE SYSTEMMetrics calculated successfully.
11Pytorch saving Part 11
Put together, the full deployment workflow covered in this lesson is: train the model, serialize only its state_dict with torch.save() rather than the whole object, and on the receiving end instantiate the same architecture from source before calling load_state_dict() to restore the weights β with map_location handling any mismatch between the training and deployment hardware.
This pattern is exactly what production ML deployment relies on: a checkpoint file that's small, portable across machines, and decoupled from any specific pickle-fragile object graph. Whether the model ends up behind a REST API, inside a batch inference job, or loaded into a Jupyter notebook for evaluation, the loading code looks the same.
From here, the next step is combining this with more advanced checkpointing β bundling the model's state_dict together with the optimizer's state and the current epoch, so training can be paused and resumed rather than just re-run from scratch.
print("System secured.\
Model exported safely.")Metrics calculated successfully.
12Step-by-Step Breakdown
You trained a model for 48 hours on a GPU. It has 99% accuracy. If you close Python right now, all that training is deleted instantly.
In PyTorch, the matrix of learned weights and biases is stored in a Python dictionary called the state_dict.
What exactly does model.state_dict() contain in PyTorch?
- βThe source code of the Python file.
- βIt is a Python dictionary containing all the learned weights and biases for every layer in the neural network.
- βA copy of the training data.
To save the model securely, you use torch.save(), passing it the state_dict and a filename (usually ending in .pt or .pth).
What is the PyTorch best practice for saving a trained model to the hard drive?
- βUse Pandas to save it as a CSV.
- βSave ONLY the
state_dict(the weights) usingtorch.save(), rather than trying to save the entire Python class. - βTake a screenshot of the terminal.
To load the model on a web server tomorrow, you must first instantiate the EMPTY class architecture, and then load the weights into it.
Why must you instantiate the MyNetwork() class AGAIN before you can load your saved weights?
- βBecause Python automatically deletes old variables.
- βBecause the saved
.pthfile only contains the raw numbers (weights), not the architecture logic. PyTorch needs the class framework to map the numbers to the right layers. - βTo bypass NVIDIA licensing.
Now, prepare yourself. We are about to enter the ADA Defense Protocol. Ensure you understand device mapping during loading.
You trained the model on an NVIDIA GPU. You send the .pth file to a friend on a MacBook without an NVIDIA GPU. If they run torch.load(), it crashes.
ADA DEFENSE: How do you safely load a PyTorch model that was trained on a GPU onto a machine that only has a CPU?
- βYou cannot. The friend must buy an NVIDIA GPU.
- βPass
map_location=torch.device('cpu')into thetorch.load()function to force the tensors into CPU RAM. - βRename the
.pthfile to.cpu.
Threat neutralized. Deployment protocols secured. Proceeding to Advanced Architectures.
Build a Real State Dictionary. Finish build_state_dict(): map each layer's name to its learned weights.
Level Up π
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Readable Serialization Code
Saving model.state_dict() instead of the whole model produces code a future maintainer can actually audit β the checkpoint is just tensors, and the architecture stays visible as ordinary version-controlled Python, not hidden inside a pickle blob.
# Prefer:
torch.save(model.state_dict(), "model.pth")
# Over:
torch.save(model, "model.pth")SEO Implications
- 1
High-Intent Deployment Queries
Searches like 'how to save a pytorch model', 'state_dict vs save model', and 'RuntimeError loading model on CPU' spike heavily once developers move from training notebooks to deployment, making accurate, example-driven coverage of this exact failure mode valuable for organic search.
Best Practices
Save state_dict, Not the Model Object
torch.save(model.state_dict(), path) decouples the checkpoint from the exact class path and file layout that existed at save time, unlike torch.save(model, path), which breaks the moment the code around it is refactored.
Always Pass map_location When Loading
Load checkpoints with torch.load(path, map_location=device) even when you expect the training and inference hardware to match β it costs nothing when devices agree, and prevents a hard crash when they don't.
Frequent Bugs
torch.load() raising a RuntimeError because a GPU-trained checkpoint is being restored on a machine with no CUDA device.
Pass map_location=torch.device('cpu') (or a dynamically chosen device) to torch.load() so every tensor in the checkpoint is remapped to available hardware instead of the device it was originally saved from.
Real-World Examples
Deploying a GPU-Trained Model to a CPU Inference Server
A model trained on an NVIDIA GPU cluster needs to serve predictions from a CPU-only web server, and torch.load() crashes with a CUDA device error on first load.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = MyNetwork()
model.load_state_dict(torch.load("my_awesome_model.pth", map_location=device))
model.to(device)
model.eval()