Saving and Loading
A trained model that dies with its process was never trained. This chapter covers Hasktorch's serialization story: checkpointing models from Haskell, and — since the weights are ordinary libtorch tensors — exchanging them with PyTorch in both directions.
import Torch
import Torch.Script (IValue (..))
import Torch.Serialize
Checkpointing a model
Torch.Serialize.saveParams writes any Parameterized model — the
same typeclass the optimizers flatten with — and loadParams reads
one back. Loading is functional: you give it a model value of the
right shape to serve as the skeleton, and it returns a new value
with the stored parameters in place:
model <- sample (LinearSpec 3 1)
saveParams model "/tmp/linear-checkpoint.pt"
fresh <- sample (LinearSpec 3 1)
restored <- loadParams fresh "/tmp/linear-checkpoint.pt"
The freshly sampled model disagrees with the original, the restored one is exact:
let probe = asTensor [[1, 2, 3 :: Float]]
[linear model probe, linear fresh probe, linear restored probe] =>
[ Tensor Float [1,1] [[-1.8353 ]]
, Tensor Float [1,1] [[-0.4390 ]]
, Tensor Float [1,1] [[-1.8353 ]]]
That is the whole checkpointing API. The file holds the flattened
parameter list, so it does not remember your record's field names —
which is fine for save-and-resume with the same code, and is exactly
the underlying save/load pair, which serialize a bare [Tensor]
when you have no model record at all.
Interop with PyTorch: pickle
For crossing the language border there is a second format. PyTorch
checkpoints are pickled dictionaries — torch.save(model.state_dict(),
path) — and Torch.Serialize speaks it directly through the
IValue type from the TorchScript bindings
(chapter 15): pickleSave and
pickleLoad map dictionaries to IVGenericDict, tensors to
IVTensor, and so on. A state dict is built by hand from the
parameters and their names:
let Linear w b = model
stateDict =
IVGenericDict
[ (IVString "weight", IVTensor (toDependent w)),
(IVString "bias", IVTensor (toDependent b))
]
pickleSave stateDict "/tmp/state_dict.pth"
reloaded <- pickleLoad "/tmp/state_dict.pth"
reloaded =>
IVGenericDict [(IVString "weight",IVTensor Tensor Float [1,3] [[-0.2462 , -0.4971 , -0.1107 ]]),(IVString "bias",IVTensor Tensor Float [1] [-0.2628 ])]
The file we just wrote is a regular PyTorch checkpoint; on the Python
side it loads with plain torch.load:
>>> torch.load("/tmp/state_dict.pth")
{'weight': tensor([[...]]), 'bias': tensor([...])}
The reverse direction works the same — save from Python with
torch.save(dict(model.state_dict()), path) (the dict(...) matters:
an OrderedDict subclass confuses the unpickler), then pickleLoad
it and match on the IVGenericDict to pull tensors out by name.
Names and shapes are yours to reconcile with your Haskell record;
for shipping a whole model rather than weights — architecture
included — TorchScript's torch.jit.trace plus
Torch.Script.loadScript is the better vehicle, as chapter 15 shows.
The typed API
Torch.Typed.Serialize mirrors the untyped pair: saveParameters
and loadParameters work on any typed model whose parameters
flatten to an HList of tensors, and loadParametersWithSpec
builds the model from its spec and the file in one step. The shapes
in the model's type must match what was saved — the file format
cannot check this for you, but the surrounding program is checked as
always, so a shape mismatch surfaces at the load site rather than
mid-training.
One honest caveat applies to every format here: loading a checkpoint executes no code, but pickle files from untrusted sources are still untrusted input to a C++ parser — treat model files with the same care as any other binary you download.