Skip to content

Model Serialization

Overview

Saving and loading models is critical for:

  • Checkpointing training progress
  • Deployment to production
  • Sharing models (HuggingFace Hub)
  • Reproducibility across systems

Different serialization formats have trade-offs in safety, compatibility, and size.

-

PyTorch Native: torch.save / torch.load

Saving State Dictionary

import torch
import torch.nn as nn

# Create and train model
model = nn.Linear(10, 5)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

#... training...

# Save state dictionary
torch.save(model.state_dict(), 'model.pth')

# state_dict contains:
# {
# 'weight'
# 'bias'
# }

Loading State Dictionary

# Create fresh model
model = nn.Linear(10, 5)

# Load weights
model.load_state_dict(torch.load('model.pth'))
model.eval() # Set to evaluation mode

# Use for inference
with torch.no_grad():
 output = model(input_data)

Saving Complete Checkpoint

# Save everything needed to resume training
checkpoint = {
 'epoch': epoch,
 'model_state_dict': model.state_dict(),
 'optimizer_state_dict': optimizer.state_dict(),
 'loss': loss,
 'config': model_config,
}

torch.save(checkpoint, f'checkpoint_epoch_{epoch}.pth')

# Later
checkpoint = torch.load('checkpoint_epoch_10.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
epoch = checkpoint['epoch']
loss = checkpoint['loss']

# Continue training from same point

Entire Model (Discouraged)

# Save entire model
torch.save(model, 'model.pth') # Pickles whole model

# Load
model = torch.load('model.pth')

# Problems:
# 1. Large file (includes code)
# 2. Compatibility issues (code changes)
# 3. Security risk (arbitrary code execution)
# 4. Python version dependencies

# Always use state_dict instead

State Dictionary Structure

Named Parameters

import torch.nn as nn

model = nn.Sequential(
 nn.Linear(10, 20),
 nn.ReLU(),
 nn.Linear(20, 5)
)

# Get state dict
state = model.state_dict()

print(state.keys())
# odict_keys([
# '0.weight', '0.bias', # First Linear layer
# '2.weight', '2.bias' # Third Linear layer (ReLU has no params)
#])

for name, param in state.items():
 print(f"{name}: {param.shape}")
 # 0.weight: torch.Size([20, 10])
 # 0.bias: torch.Size([20])
 # 2.weight: torch.Size([5, 20])
 # 2.bias: torch.Size([5])

Custom Module State

class CustomModel(nn.Module):
 def __init__(self):
 super().__init__()
 self.fc = nn.Linear(10, 5)
 self.scale = nn.Parameter(torch.ones(5)) # Trainable
 self.constant = torch.tensor(0.5) # Not in state_dict

 def forward(self, x):
 return self.fc(x) * self.scale

model = CustomModel()
state = model.state_dict()

print(state.keys())
# odict_keys(['fc.weight', 'fc.bias', 'scale'])
# Note

State Dict on Different Device

# Save on GPU
model = model.cuda()
torch.save(model.state_dict(), 'model.pth')

# Load on CPU
device = 'cpu'
state_dict = torch.load('model.pth', map_location=device)
model.load_state_dict(state_dict)

# Or with explicit mapping
state_dict = torch.load('model.pth', map_location={
 'cuda:0': 'cpu', # GPU 0 → CPU
 'cuda:1': 'cuda:2', # GPU 1 → GPU 2
})

SafeTensors Format

Why SafeTensors?

# Problems with torch.save:
# 1. Uses pickle (security risk)
# 2. Slow loading
# 3. Large file size
# 4. Requires full model code to load

# SafeTensors:
# 1. Safe format (just data, no code)
# 2. Fast loading (mmap support)
# 3. Smaller files (no metadata overhead)
# 4. Language-agnostic (used by JAX, TF too)

Saving and Loading

from safetensors.torch import save_file, load_file

# Save
state_dict = model.state_dict()
save_file(state_dict, 'model.safetensors')

# Load
state_dict = load_file('model.safetensors')
model.load_state_dict(state_dict)

# Advantages:
# - File is ~2x smaller
# - Loading 10x faster
# - Safe (no arbitrary code execution)
# - Compatible with HuggingFace Hub

Lazy Loading (Memory Efficient)

from safetensors.torch import load_file

# Load only specific keys (without loading full model)
state_dict = load_file('model.safetensors', 
 keys=['fc.weight', 'fc.bias'])

# Or iterate without loading all to memory
with safe_open('model.safetensors', framework='pt') as f:
 for key in f.keys():
 tensor = f.get_tensor(key)
 print(f"Loaded {key}")

-

HuggingFace Model Hub

Publishing Models

from transformers import AutoModel, AutoTokenizer

# Load model
model = AutoModel.from_pretrained('bert-base-uncased')

# Push to Hub
model.push_to_hub("username/my-bert-model")

# Push with config
model.push_to_hub(
 repo_id="username/my-bert-model",
 commit_message="Add my custom BERT variant",
 private=False
)

Downloading Models

from transformers import AutoModel

# Download and load
model = AutoModel.from_pretrained('username/my-bert-model')

# Download to cache directory
model = AutoModel.from_pretrained(
 'username/my-bert-model',
 cache_dir='/path/to/cache'
)

# Load specific revision
model = AutoModel.from_pretrained(
 'username/my-bert-model',
 revision='main' # or specific branch/tag
)

Custom Config and Tokenizer

from transformers import BertConfig, BertModel, BertTokenizer

# Create model with custom config
config = BertConfig(
 vocab_size=50265,
 hidden_size=512,
 num_hidden_layers=6,
 num_attention_heads=8,
)

model = BertModel(config)

# Save with config
model.save_pretrained('my-model')
config.save_pretrained('my-model')

# Load entire directory
model = BertModel.from_pretrained('my-model')
config = BertConfig.from_pretrained('my-model')
tokenizer = BertTokenizer.from_pretrained('my-model')

-

Checkpoint Management

Saving Best Model

import torch
import torch.nn as nn

class CheckpointManager:
 def __init__(self, model, optimizer, save_dir='./checkpoints'):
 self.model = model
 self.optimizer = optimizer
 self.save_dir = Path(save_dir)
 self.save_dir.mkdir(exist_ok=True)
 self.best_loss = float('inf')

 def save_checkpoint(self, epoch, loss, is_best=False):
 """Save checkpoint."""
 checkpoint = {
 'epoch': epoch,
 'model_state_dict': self.model.state_dict(),
 'optimizer_state_dict': self.optimizer.state_dict(),
 'loss': loss,
 }

 # Save latest
 latest_path = self.save_dir / 'latest.pth'
 torch.save(checkpoint, latest_path)

 # Save if best
 if is_best:
 best_path = self.save_dir / 'best.pth'
 torch.save(checkpoint, best_path)
 self.best_loss = loss

 # Save periodic
 if epoch % 10 == 0:
 period_path = self.save_dir / f'checkpoint_epoch_{epoch}.pth'
 torch.save(checkpoint, period_path)

 def load_checkpoint(self, path):
 """Load checkpoint."""
 checkpoint = torch.load(path)
 self.model.load_state_dict(checkpoint['model_state_dict'])
 self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
 return checkpoint['epoch'], checkpoint['loss']

# Usage
manager = CheckpointManager(model, optimizer)

for epoch in range(100):
 train_loss = train_epoch(model, optimizer, train_loader)
 val_loss = validate(model, val_loader)

 is_best = val_loss < manager.best_loss
 manager.save_checkpoint(epoch, val_loss, is_best=is_best)

Loading Last Checkpoint

import os
from pathlib import Path

def find_latest_checkpoint(checkpoint_dir):
 """Find most recent checkpoint."""
 checkpoint_dir = Path(checkpoint_dir)

 # First try 'latest.pth'
 latest = checkpoint_dir / 'latest.pth'
 if latest.exists():
 return latest

 # Otherwise find newest by modification time
 checkpoints = list(checkpoint_dir.glob('checkpoint_*.pth'))
 if not checkpoints:
 return None

 return max(checkpoints, key=lambda p: p.stat().st_mtime)

# Usage
latest_ckpt = find_latest_checkpoint('./checkpoints')
if latest_ckpt:
 checkpoint = torch.load(latest_ckpt)
 model.load_state_dict(checkpoint['model_state_dict'])
 print(f"Resumed from {latest_ckpt}")
else:
 print("No checkpoint found, training from scratch")

Cross-Framework Serialization

ONNX Format (Inference)

import torch
import torch.onnx

# Create model
model = nn.Linear(10, 5)
model.eval()

# Export to ONNX
dummy_input = torch.randn(1, 10)
torch.onnx.export(
 model,
 dummy_input,
 'model.onnx',
 input_names=['input'],
 output_names=['output'],
 dynamic_axes={'input': {0: 'batch_size'}}
)

# Load and use with ONNX Runtime
import onnxruntime

sess = onnxruntime.InferenceSession('model.onnx')
output = sess.run(None, {'input': input_data.numpy()})

TorchScript for Deployment

# Create model
model = nn.Linear(10, 5)

# Script (trace-based)
dummy_input = torch.randn(1, 10)
traced_model = torch.jit.trace(model, dummy_input)
traced_model.save('model.pt')

# Script (compile-based)
scripted_model = torch.jit.script(model)
scripted_model.save('model.pt')

# Load and use
loaded_model = torch.jit.load('model.pt')
output = loaded_model(input_data)

Storage and Bandwidth Optimization

Quantization Before Saving

import torch
import torch.quantization as quantization

# Create model
model = nn.Sequential(
 nn.Linear(10, 20),
 nn.ReLU(),
 nn.Linear(20, 5)
)

# Quantize to int8 (4x smaller)
model.qconfig = quantization.get_default_qconfig('fbgemm')
quantization.prepare(model, inplace=True)
quantization.convert(model, inplace=True)

# Save quantized model
torch.save(model.state_dict(), 'model_quantized.pth')

# File size comparison:
# Original
# Quantized

Compression Techniques

# 1. Weight pruning (remove small weights)
def prune_weights(state_dict, threshold=1e-6):
 pruned = {}
 for key, tensor in state_dict.items():
 mask = torch.abs(tensor) > threshold
 pruned[key] = tensor * mask
 return pruned

pruned_state = prune_weights(model.state_dict())
torch.save(pruned_state, 'model_pruned.pth')

# 2. Shared weights (same tensor multiple places)
# 3. Compression libraries (gzip, brotli)
import gzip

state_dict = torch.load('model.pth')
with gzip.open('model.pth.gz', 'wb') as f:
 torch.save(state_dict, f)

# 4. Different precision (float32 → float16)
state_half = {}
for key, tensor in model.state_dict().items():
 state_half[key] = tensor.half()

torch.save(state_half, 'model_fp16.pth')

-

Practical: Complete Training with Checkpoints

import torch
import torch.nn as nn
import torch.optim as optim
from pathlib import Path

class Trainer:
 def __init__(self, model, optimizer, criterion, checkpoint_dir='./checkpoints'):
 self.model = model
 self.optimizer = optimizer
 self.criterion = criterion
 self.checkpoint_dir = Path(checkpoint_dir)
 self.checkpoint_dir.mkdir(exist_ok=True)

 def train_epoch(self, train_loader):
 self.model.train()
 total_loss = 0

 for x, y in train_loader:
 pred = self.model(x)
 loss = self.criterion(pred, y)

 self.optimizer.zero_grad()
 loss.backward()
 self.optimizer.step()

 total_loss += loss.item()

 return total_loss / len(train_loader)

 def evaluate(self, val_loader):
 self.model.eval()
 total_loss = 0

 with torch.no_grad():
 for x, y in val_loader:
 pred = self.model(x)
 loss = self.criterion(pred, y)
 total_loss += loss.item()

 return total_loss / len(val_loader)

 def save_checkpoint(self, epoch, val_loss, is_best=False):
 """Save checkpoint."""
 checkpoint = {
 'epoch': epoch,
 'model_state_dict': self.model.state_dict(),
 'optimizer_state_dict': self.optimizer.state_dict(),
 'val_loss': val_loss,
 }

 # Save latest
 torch.save(checkpoint, self.checkpoint_dir / 'latest.pth')

 # Save best
 if is_best:
 torch.save(checkpoint, self.checkpoint_dir / 'best.pth')

 def load_checkpoint(self, checkpoint_name='latest.pth'):
 """Load checkpoint."""
 path = self.checkpoint_dir / checkpoint_name
 if not path.exists():
 return 0

 checkpoint = torch.load(path)
 self.model.load_state_dict(checkpoint['model_state_dict'])
 self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
 return checkpoint['epoch']

 def train(self, train_loader, val_loader, num_epochs=100):
 """Complete training loop with checkpointing."""
 start_epoch = self.load_checkpoint()
 best_val_loss = float('inf')

 for epoch in range(start_epoch, num_epochs):
 train_loss = self.train_epoch(train_loader)
 val_loss = self.evaluate(val_loader)

 is_best = val_loss < best_val_loss
 if is_best:
 best_val_loss = val_loss

 self.save_checkpoint(epoch, val_loss, is_best=is_best)

 print(f"Epoch {epoch}: train_loss={train_loss:.4f}, "
 f"val_loss={val_loss:.4f}, best={best_val_loss:.4f}")

# Usage
model = nn.Linear(10, 5)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.MSELoss()

trainer = Trainer(model, optimizer, criterion)
trainer.train(train_loader, val_loader, num_epochs=100)

# Later
trainer.load_checkpoint()
trainer.train(train_loader, val_loader, num_epochs=200)

Summary: Serialization Formats

Format Use Case Size Speed Safety
pickle Small models Large Slow Unsafe
state_dict Training checkpoints Medium Medium Safe
safetensors Sharing/deployment Small Fast Safe
ONNX Cross-framework inference Medium Fast Safe
TorchScript Deployment without code Medium Fast Safe
HuggingFace Community models Variable Medium Safe

-