| name | pytorch |
| description | [Applies to: **/*.py] Definitive guidelines for writing clean, performant, and maintainable PyTorch code, emphasizing modern best practices, explicit device management, and efficient training patterns. |
| source | cursor_mdc |
PyTorch Best Practices
This guide outlines the definitive best practices for developing with PyTorch, ensuring your code is readable, performant, and production-ready. We prioritize usability, explicit control, and modern tooling.
1. Code Organization and Structure
Structure your PyTorch projects for clarity, testability, and scalability. Encapsulate logical blocks into distinct functions or classes.
1.1. Modularize Your Codebase
Separate data loading, model definition, training, and evaluation into dedicated modules or functions. This makes components reusable and testable.
❌ BAD: Monolithic script
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
class MyModel(nn.Module):
pass
def main():
train_data = TensorDataset(...)
train_loader = DataLoader(train_data, batch_size=32)
model = MyModel()
optimizer = torch.optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss()
for epoch in range(10):
for batch_idx, (data, target) in enumerate(train_loader):
pass
if __name__ == "__main__":
main()
✅ GOOD: Modularized structure
import torch
from torch.utils.data import DataLoader, TensorDataset
def get_dataloaders(batch_size: int) -> tuple[DataLoader, DataLoader]:
X = torch.randn(1000, 784)
y = torch.randint(0, 10, (1000,))
train_dataset = TensorDataset(X, y)
val_dataset = TensorDataset(X[:100], y[:100])
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4, pin_memory=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4, pin_memory=True)
return train_loader, val_loader
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self, num_classes: int = 10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(32, 64, kernel_size=, padding=),
nn.ReLU(),
nn.MaxPool2d(kernel_size=, stride=)
)
.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear( * * , ),
nn.ReLU(),
nn.Linear(, num_classes)
)
() -> torch.Tensor:
x = .features(x)
x = .classifier(x)
x
torch
torch.nn nn
torch.optim Adam
src.model SimpleCNN
src.data get_dataloaders
() -> :
model.train()
total_loss =
data, target loader:
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
total_loss += loss.item()
total_loss / (loader)
() -> :
model.()
correct =
total =
torch.no_grad():
data, target loader:
data, target = data.to(device), target.to(device)
output = model(data)
_, predicted = torch.(output.data, )
total += target.size()
correct += (predicted == target).().item()
* correct / total
torch
src.model SimpleCNN
src.data get_dataloaders
src.train train_epoch, evaluate_model
():
device = torch.device( torch.cuda.is_available() )
train_loader, val_loader = get_dataloaders(batch_size)
model = SimpleCNN().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = torch.nn.CrossEntropyLoss()
epoch (epochs):
train_loss = train_epoch(model, train_loader, optimizer, criterion, device)
val_accuracy = evaluate_model(model, val_loader, device)
()
torch.save(model.state_dict(), )
__name__ == :
run_experiment()
2. Common Patterns and Anti-patterns
Adopt patterns that enhance clarity and performance, and avoid common pitfalls.
2.1. Explicit Device Placement
Always explicitly move tensors and models to the correct device (cpu or cuda). Never rely on implicit device handling.
❌ BAD: Implicit device assumption
model = MyModel()
data = torch.randn(1, 3, 224, 224)
output = model(data)
✅ GOOD: Explicit device placement
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyModel().to(device)
data = torch.randn(1, 3, 224, 224).to(device)
output = model(data)
2.2. Disable Gradient Calculation for Inference
Use torch.no_grad() for validation and inference to save memory and speed up computation.
❌ BAD: Calculating unnecessary gradients
model.eval()
for data, target in val_loader:
output = model(data)
loss = criterion(output, target)
✅ GOOD: Disabling gradients
model.eval()
with torch.no_grad():
for data, target in val_loader:
output = model(data)
loss = criterion(output, target)
2.3. Avoid In-Place Operations (Unless Profiled)
In-place operations (.add_(), .mul_(), etc.) can break PyTorch's autograd engine and make debugging difficult. Prefer out-of-place operations unless profiling explicitly shows an in-place operation is a critical bottleneck for memory or speed.
❌ BAD: In-place operation
x = torch.randn(5, requires_grad=True)
y = x * 2
x.add_(1)
✅ GOOD: Out-of-place operation
x = torch.randn(5, requires_grad=True)
y = x * 2
x = x + 1
3. Performance Considerations
Optimize your PyTorch code for speed and memory efficiency.
3.1. Leverage torch.compile
Use torch.compile to significantly accelerate your models by tracing and optimizing the computation graph. Apply it to your nn.Module instances.
❌ BAD: Running model in eager mode only
model = MyModel().to(device)
for data, target in loader:
output = model(data)
✅ GOOD: Compiling the model
model = MyModel().to(device)
compiled_model = torch.compile(model)
for data, target in loader:
output = compiled_model(data)
3.2. Mixed Precision Training with torch.cuda.amp
For CUDA devices, use automatic mixed precision (AMP) to halve memory bandwidth and speed up computation without significant accuracy loss.
❌ BAD: Full precision training
optimizer = torch.optim.Adam(model.parameters())
for data, target in loader:
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
✅ GOOD: Mixed precision training
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
optimizer = torch.optim.Adam(model.parameters())
for data, target in loader:
with autocast():
output = model(data)
loss = criterion(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
3.3. Optimize DataLoader
Configure DataLoader for efficient asynchronous data loading.
❌ BAD: Default DataLoader settings
DataLoader(dataset, batch_size=32)
✅ GOOD: Optimized DataLoader
DataLoader(
dataset,
batch_size=32,
shuffle=True,
num_workers=min(os.cpu_count(), 8),
pin_memory=True,
persistent_workers=True
)
4. Common Pitfalls and Gotchas
Be aware of these common issues to avoid frustrating debugging sessions.
4.1. Device Mismatches
Ensure all tensors involved in an operation are on the same device. This is the most frequent error.
❌ BAD: Device mismatch
model = MyModel().cuda()
input_tensor = torch.randn(1, 3, 224, 224).cpu()
output = model(input_tensor)
✅ GOOD: Consistent device placement
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyModel().to(device)
input_tensor = torch.randn(1, 3, 224, 224).to(device)
output = model(input_tensor)
4.2. Multiprocessing Start Method
When using multiprocessing with CUDA, always set the start method to "spawn" or "forkserver" to avoid "poison fork" issues.
❌ BAD: Default fork start method with CUDA
import torch.multiprocessing as mp
mp.set_start_method('fork')
✅ GOOD: Safe multiprocessing start method
import torch.multiprocessing as mp
try:
mp.set_start_method('spawn', force=True)
except RuntimeError:
pass
5. Type Hints
Use type hints extensively for improved readability, maintainability, and static analysis.
❌ BAD: Untyped functions
def train_step(model, data, target, optimizer, criterion):
pass
✅ GOOD: Type-hinted functions
import torch
import torch.nn as nn
from torch.optim import Optimizer
from torch.utils.data import DataLoader
def train_step(
model: nn.Module,
data: torch.Tensor,
target: torch.Tensor,
optimizer: Optimizer,
criterion: nn.Module
) -> float:
return loss.item()
6. Virtual Environments
Always use virtual environments (venv or conda) to manage dependencies and ensure reproducible setups.
❌ BAD: Global package installation
pip install torch torchvision
✅ GOOD: Virtual environment setup
python -m venv .venv
source .venv/bin/activate
pip install torch torchvision
conda create -n my_env python=3.10
conda activate my_env
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
7. Packaging
For reusable components or libraries, package your code properly using setuptools or poetry.
❌ BAD: Just a collection of scripts
my_project/
├── train.py
├── model.py
└── data.py
✅ GOOD: Packaged structure
my_project/
├── pyproject.toml # Or setup.py
├── src/
│ └── my_project_lib/
│ ├── __init__.py
│ ├── model.py
│ ├── data.py
│ └── train_utils.py
├── scripts/
│ └── run_training.py # Entry point that imports from src/
└── tests/
├── test_model.py
└── test_data.py
8. Testing Approaches
Implement unit tests for individual modules and integration tests for training loops.
8.1. Unit Tests for Modules
Test nn.Module definitions, data transformations, and utility functions independently.
❌ BAD: No tests, or only end-to-end testing
✅ GOOD: Unit test for a model
import unittest
import torch
from src.model import SimpleCNN
class TestSimpleCNN(unittest.TestCase):
def test_forward_pass(self):
model = SimpleCNN(num_classes=10)
input_tensor = torch.randn(1, 1, 28, 28)
output = model(input_tensor)
self.assertEqual(output.shape, (1, 10))
def test_output_range(self):
model = SimpleCNN(num_classes=2)
input_tensor = torch.randn(1, 1, 28, 28)
output = model(input_tensor)
self.assertTrue(output.dtype == torch.float32)
if __name__ == '__main__':
unittest.main()
8.2. Integration Tests for Training Loops
Use small, mock datasets to quickly verify the training loop's functionality without long training times.
❌ BAD: Only testing with full datasets
✅ GOOD: Integration test with mock data
import unittest
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from src.model import SimpleCNN
from src.train import train_epoch, evaluate_model
class TestTrainingLoop(unittest.TestCase):
def test_training_run(self):
device = torch.device("cpu")
model = SimpleCNN(num_classes=2).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
mock_X = torch.randn(10, 1, 28, 28)
mock_y = torch.randint(0, 2, (10,))
mock_dataset = TensorDataset(mock_X, mock_y)
mock_loader = DataLoader(mock_dataset, batch_size=2)
initial_loss = train_epoch(model, mock_loader, optimizer, criterion, device)
second_epoch_loss = train_epoch(model, mock_loader, optimizer, criterion, device)
self.assertLess(second_epoch_loss, initial_loss * 1.1)
accuracy = evaluate_model(model, mock_loader, device)
self.assertGreaterEqual(accuracy, 0.0)
if __name__ == '__main__':
unittest.main()