Top 5 PyTorch Tips to Debug Your Models Without Losing Your Mind
When you start using PyTorch, it quickly feels like you’re fighting the framework:
- Shapes don’t match
- The loss doesn’t move
- The GPU complains
- Error messages look cryptic
The good news: 80% of the problems come from the same few causes.
If you get a few simple habits right, you can debug most models in minutes, not hours.
In this article, we’ll go through 5 practical PyTorch tips (with copy-paste code) to help you find what’s wrong faster.
1. Check tensor shapes at every key step
Most PyTorch issues are just shape mismatches.
If you’ve ever seen:
RuntimeError: mat1 and mat2 shapes cannot be multiplied
…you know exactly what I mean.
Habit: print .shape at critical points
Whenever you do view, reshape, permute, cat, etc., add one debug line:
- at the beginning of
forward - after flattening
- after concatenations
- before the final layer
Example with a simple network:
import torch
import torch.nn as nn
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(28 * 28, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
print("Input shape :", x.shape) # (batch_size, 1, 28, 28) ?
x = x.view(x.size(0), -1) # flatten
print("After view :", x.shape) # (batch_size, 784)
x = self.fc1(x)
print("After fc1 :", x.shape) # (batch_size, 128)
x = self.fc2(x)
print("After fc2 :", x.shape) # (batch_size, 10)
return x
You can enforce expectations with assertions:
def forward(self, x):
# We expect (batch_size, 1, 28, 28)
assert x.ndim == 4 and x.shape[1:] == (1, 28, 28), f"Unexpected shape: {x.shape}"
x = x.view(x.size(0), -1)
assert x.shape[1] == 28 * 28, f"Incorrect flatten shape: {x.shape}"
...
💡 Mental rule: if you’re lost, print shapes from raw data → final logits, like tracing a pipeline.
2. Try to overfit a tiny batch on purpose
This single trick can save you a lot of time.
If your model can’t overfit 32 examples, it won’t work on 32,000.
The goal: check that model + loss + labels are coherent.
Procedure
- Take one batch from your
DataLoader(e.g., 32 samples). - Train your model only on that batch for many epochs.
- Check if the loss goes close to zero.
If the loss stays high → something is fundamentally wrong.
“Overfit a batch” example
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
# Get ONE batch
data_iter = iter(train_loader)
inputs, targets = next(data_iter)
for epoch in range(100):
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}, loss = {loss.item():.4f}")
How to interpret this
-
If the loss drops a lot and becomes very small → your model can learn.
The problem might be elsewhere (LR, regularization, noisy data…). -
If the loss barely moves:
- there might be a bug in the
forward - labels may be wrong (wrong type/shape, misaligned)
- you may be using the wrong loss (e.g.
MSELosswith class indices) - your learning rate is completely off
- there might be a bug in the
💡 Rule of thumb: as long as you cannot overfit a small batch, don’t bother launching a 10-hour training run.
3. Look at the loss… but also at some predictions
We often stare only at the loss curve, but it’s very abstract.
Sometimes the model is actually learning, but you don’t realize it from the loss alone.
Simple habit: print a few predictions vs labels every epoch (or every N epochs).
Minimal example
for epoch in range(num_epochs):
model.train()
for inputs, targets in train_loader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
# Quick inspection at the end of the epoch
model.eval()
with torch.no_grad():
sample_inputs, sample_targets = next(iter(val_loader))
sample_outputs = model(sample_inputs)
preds = sample_outputs.argmax(dim=1)
print(f"\nEpoch {epoch} - train loss: {loss.item():.4f}")
print("Targets :", sample_targets[:10].tolist())
print("Preds :", preds[:10].tolist())
What to watch for
- Do predictions change over epochs?
- Does the model always output the same class (e.g.,
[3, 3, 3, 3, 3, 3, ...])? - Do predictions gradually become more consistent with labels?
If predictions are random or frozen:
- your learning rate might be too low → everything moves too slowly
- or too high → training diverges
- or you have a target bug (wrong label mapping, shifted indices, etc.)
💡 Even without fancy plots, just printing 10 predictions and labels each epoch already gives you a huge amount of signal.
4. Hunt down device and dtype issues
Two ultra-common categories of errors:
-
Mixing CPU and GPU:
RuntimeError: Expected all tensors to be on the same device, but found at least two devices -
Wrong type for the loss:
RuntimeError: expected scalar type Long but found Float
Base pattern you should always use
Always start by defining a device:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleNet().to(device)
criterion = nn.CrossEntropyLoss()
for inputs, targets in train_loader:
inputs = inputs.to(device)
targets = targets.to(device) # important: same device as inputs & model
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
If you’re unsure, log the devices:
print(inputs.device, targets.device, next(model.parameters()).device)
Watch dtype when using losses
Classic example:
-
For
nn.CrossEntropyLoss:outputs(logits) →torch.float32targets(class indices) →torch.int64(LongTensor)
You can check:
print(outputs.dtype) # torch.float32
print(targets.dtype) # torch.int64 expected for CrossEntropyLoss
If your targets are floats (for example after some accidental normalization or cast), things will break or behave weirdly.
💡 Quick debug order when you see a weird error:
- Check devices (CPU vs GPU).
- Check dtypes (float vs long, etc.).
- Only then dig deeper.
5. Isolate the problem with a simple forward and dummy data
Before any big training run, you should check that your model can do a forward pass without crashing.
You can do this with dummy data (torch.randn) that has the same shape as real data.
Test 1: forward with random inputs
batch_size = 16
dummy_inputs = torch.randn(batch_size, 1, 28, 28) # same shape as your real images
model = SimpleNet()
dummy_outputs = model(dummy_inputs)
print("Dummy output shape :", dummy_outputs.shape) # expected: (16, 10)
If it crashes here, you know:
- the bug is in the model architecture (dimensions, reshapes, etc.)
- not in the
DataLoader, optimizer, or loss.
Test 2: forward in eval mode, no gradients
model.eval()
with torch.no_grad():
dummy_outputs = model(dummy_inputs)
print(dummy_outputs[:5])
Then, do the same with a real batch from your DataLoader:
model.eval()
with torch.no_grad():
real_inputs, real_targets = next(iter(train_loader))
real_outputs = model(real_inputs)
print("Real output shape :", real_outputs.shape)
If dummy inputs work but real ones don’t:
- the problem is probably in data preparation (resizing, normalization, channels order, etc.).
💡 Ideal routine before any long training:
- Forward with
torch.randn→ OK? - Forward with one real batch → OK?
- Overfit one small batch → OK?
- Then launch full training.
Wrap-up: 5 habits for every PyTorch project
To summarize, most PyTorch pain can be reduced with a few simple habits:
- Check tensor shapes at key points (input, flatten, concat, output).
- Try to overfit a tiny batch to validate model + loss + labels.
- Inspect a few predictions each epoch, not just the loss value.
- Verify
device(CPU/GPU) anddtypebefore you overthink the bug. - Test the forward pass with dummy data, then with a real batch, before long runs.
If you turn these five points into a mental checklist, you’ll spend much less time on “stupid bugs” and much more time on building actually useful models.