Debugging Neural Network Training
Find silent training bugs from the symptom. Checks, thresholds, init rules, clipping and AMP order, profilers.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Symptom, cause, first fix#
| Symptom | Cause | Fix |
|---|---|---|
| Loss stuck at -log(1/num_classes) | Model predicts a uniform distribution | Check the data pipeline and the labels |
| Loss NaN after a few steps | Learning rate too high | Reduce LR by 10x |
| Loss NaN immediately | log(0) or division by zero | Add epsilon to log and division |
| Loss oscillates wildly | LR too high or batch too small | Reduce LR, increase batch size |
| Loss decreases, then plateaus | LR too high for the fine-tuning phase | Add a cosine or step decay schedule |
| Train acc high, test acc low | Overfitting | Add dropout, weight decay, more data |
| Train acc = test acc = chance | Model learns nothing | Run the overfit-one-batch test |
| Gradients all zero | Dead ReLUs or a detached graph | Switch to LeakyReLU, check .requires_grad |
From Lesson 03.13
Common PyTorch bugs#
| Symptom | Cause | Fix |
|---|---|---|
| Loss oscillates, gradients accumulate | No optimizer.zero_grad() | Call it before loss.backward() |
| Test accuracy varies between runs | No model.eval() at test time | Add model.eval() and torch.no_grad() |
| Wrong results, no error | Silent broadcasting of shapes | Print shapes after every operation |
RuntimeError: expected CUDA tensor | CPU and GPU mismatch | Use .to(device) on model AND data |
| Graph grows forever, OOM | Tensors not detached | Use .detach() or with torch.no_grad() |
RuntimeError: modified by in-place operation | In-place op breaks autograd | Replace x += 1 with x = x + 1 |
| Loss stuck at random-chance level | Inputs not normalized | Normalize inputs to mean=0, std=1 |
Cross-entropy expects Long, got Float | Labels have the wrong dtype | Cast labels: labels.long() |
From Lesson 03.13, Lesson 00.12
Thresholds the checks use#
| Setting | Default | Note |
|---|---|---|
fraction_zero | > 0.5 | Dead neurons. Use LeakyReLU or a lower LR |
Activation std | < 1e-6 | Collapsed activations |
Gradient abs_mean | < 1e-7 | Vanishing gradient in that layer |
Gradient abs_mean | > 100 | Exploding gradient in that layer |
| First / last grad ratio | > 100 | Vanishing gradients through depth |
rel_diff | < 1e-5 | Gradient check passes. > 1e-3 is a bug |
Overfit final_loss | > 0.1 | FAIL: model or loop is broken |
start_lr | 1e-7 | LR finder sweeps up to end_lr=10 |
From 03.13 code/debug_neural_nets.py, Lesson 03.13, 00.12 code/debug_tools.py
One AMP training step#
self.optimizer.zero_grad(set_to_none=True)with torch.amp.autocast(device_type=self.device_type, dtype=self.amp_dtype): loss = self._loss_fn(self.model(inputs), targets)if not torch.isfinite(loss).all().item():skip, no scaler.update() ...self.scaler.scale(loss).backward()self.scaler.unscale_(self.optimizer)before the clipif has_non_finite_grad(self.model.parameters()): self.scaler.update()halves the scalepre_clip, post_clip = clip_global_l2_norm(list(self.model.parameters()), self.max_norm)self.scaler.step(self.optimizer)self.scaler.update()every stepscale = self.scaler.get_scale()log next to lr and normLoss not decreasing: check in this order#
From Lesson 03.13
Diagnostic functions#
| Task | Code |
|---|---|
| Shape, dtype, device, NaN of a tensor | debug_print(name, |
| Map every layer shape | check_shapes(model, |
| Find NaN or Inf gradients | detect_nan(model, |
| Find tensors on another device | check_devices(model, |
| Total norm, large or zero grads | check_gradient_health(model) |
| Prove the loop can learn | overfit_one_batch(model, |
| Sweep the learning rate | find_learning_rate(model, |
| Compare backprop to finite differences | gradient_check(model, |
From 00.12 code/debug_tools.py, 03.13 code/debug_neural_nets.py
Hook a debugger into a model#
debugger1 = NetworkDebugger(model1)hooks Linear, Conv2d, ReLU, LeakyReLUdebugger1.record_loss(loss.item())call once per stepdebugger1.print_report()loss, activation, gradient checksdebugger1.remove_hooks()with torch.autograd.detect_anomaly():built-in NaN and Inf detection loss.backward()Initialization by activation#
- Zero init
- all neurons stay identical. One effective neuron for any width.
- Variance rule
- \mathrm{Var}(z) = \text{fan}_{\text{in}} \cdot \mathrm{Var}(w) \cdot \mathrm{Var}(x)
- Xavier
- \mathrm{Var}(w) = 2/(\text{fan}_{\text{in}} + \text{fan}_{\text{out}}) for sigmoid and tanh.
- Kaiming
- \mathrm{Var}(w) = 2/\text{fan}_{\text{in}} for ReLU, Leaky ReLU, GELU and Swish.
- Residual scaling
- scale residual layers by 1/\sqrt{2N} for N layers.
- Random N(0,1)
- explodes by layer 5. N(0,0.01): vanishes by layer 10.
- Check
- activation magnitudes stay between 0.5 and 2.0 through all layers.
- PyTorch
nn.Linear(512, 256)defaults to Kaiming uniform.
Clip and scale step order#
From Lesson 19.45
Profile and inspect#
| Command | What it does |
|---|---|
python -m cProfile -s cumtime train.py | Every function call, sorted by cumulative time |
kernprof -l -v train.py | Line by line time for @profile functions |
python -m memory_profiler your_script.py | Line by line memory use |
tracemalloc.take_snapshot() | CPU memory by line after tracemalloc.start() |
torch.cuda.memory_summary() | GPU memory allocated and cached |
tensorboard --logdir= | Loss curves, weight and gradient histograms |
breakpoint() | Stop in pdb. p outputs.shape, c continue, q quit |
python3 code/ | Clip and AMP demo. At least one row is a SKIP |
From Lesson 00.12, Lesson 19.45
Numbers to watch#
- What does a healthy GradScaler scale do?
- It climbs in powers of two to near
2^17or2^18. - When is the skip rate an alert?
- Above 5 percent over a 1,000-step rolling window.
- The scale never changes?
- The skip path forgets
scaler.update(). - Data loading takes 60% of the time?
- Set
num_workers > 0in the DataLoader. - Out of memory?
- Reduce the batch size first. Then use
torch.cuda.empty_cache(). - Test accuracy is 99%?
- Check for data leakage. Split first, preprocess second.
- Train loss goes down and val loss goes up?
- The model overfits.
From Lesson 19.45, Lesson 00.12, Lesson 03.13
Checklist before full training#
- Run the overfit-one-batch test. If it fails, stop.
- Print the model summary. Verify the parameter count.
- Run one forward pass with random data. Check the output shape.
- Train for 5 epochs. Verify that the loss decreases.
- Check activation statistics and gradient flow.
- Print 5 random samples with labels.
- Log loss, learning rate and gradient norms during training.
From Lesson 03.13, Lesson 00.12