Skip to content
mlmentorship

Debug this training loop

Trace gradient buffers and module mode, separate demonstrated defects from missing contracts, then execute a corrected CPU training fixture.

Published · 7 min read ·Role-specific ·Advanced

ML implementation · active recall

Practice before you read

40 minutes. Clarify the contract, implement a correct baseline, test edge cases, then optimize.

ML implementation · closed-book attempt

Debug this training loop

Clarify the contract, implement a correct baseline, test edge cases, then optimize.

Problem

Context. The supplied exercise alternates optimization and evaluation inside an ordinary PyTorch training loop. Model architecture, target semantics, and device setup are not supplied. Task. Trace gradients and module mode across consecutive batches, identify supported defects, and propose a corrected execution sequence. State which additional facts are needed before changing the loss or moving tensors. Constraints. Treat the code below as deliberately broken exercise input, not a runnable reference. Use ordinary optimizer and module semantics; do not infer a classification task merely from variable names. Inspect the supplied loop. Assume ordinary PyTorch module and optimizer behavior. Identify persistent-state defects, then separate them from questions requiring missing model, data or device information. The following is deliberately broken exercise code, not a runnable reference. import torch import torch.nn as nn import torch.optim as optim model = MyModel() optimizer = optim.Adam(model.parameters(), lr=1e-3) criterion = nn.MSELoss() for epoch in range(10): for x, y in dataloader: # forward pred = model(x) loss = criterion(pred, y) # backward loss.backward() optimizer.step() # eval inside training loop if epoch % 2 == 0: model.eval() val_pred = model(x_val) val_loss = criterion(val_pred, y_val) print(f"epoch {epoch} val loss: {val_loss}")

40:00recommended time

Closing or reloading clears the scratchpad. Only score, weak rubric dimensions, attempt count, and retry date can be stored locally.

Reading depth

Inspect a faulty training loop and distinguish demonstrable state-management defects from questions that require missing model or data information.

Problem

Context. The supplied exercise alternates optimization and evaluation inside an ordinary PyTorch training loop. Model architecture, target semantics, and device setup are not supplied.

Task. Trace gradients and module mode across consecutive batches, identify supported defects, and propose a corrected execution sequence. State which additional facts are needed before changing the loss or moving tensors.

Constraints. Treat the code below as deliberately broken exercise input, not a runnable reference. Use ordinary optimizer and module semantics; do not infer a classification task merely from variable names.

Inspect the supplied loop. Assume ordinary PyTorch module and optimizer behavior. Identify persistent-state defects, then separate them from questions requiring missing model, data or device information. The following is deliberately broken exercise code, not a runnable reference.

import torch
import torch.nn as nn
import torch.optim as optim

model = MyModel()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()

for epoch in range(10):
    for x, y in dataloader:
        # forward
        pred = model(x)
        loss = criterion(pred, y)

        # backward
        loss.backward()
        optimizer.step()

        # eval inside training loop
        if epoch % 2 == 0:
            model.eval()
            val_pred = model(x_val)
            val_loss = criterion(val_pred, y_val)
            print(f"epoch {epoch} val loss: {val_loss}")

Picture

optimizer.step() does not clear .grad buffers. model.eval() persists until the mode is changed again. Gradient recording is a third, separate setting: no_grad() does not restore training mode.

Clearing gradients and restoring training mode are separate operations

Clearing gradients does not restore training modeA training iteration needs both the intended model mode and a clean gradient buffer. Each action changes a separate persistent state.Each action changes only its ownpersistent stateinitialtrainoldeval()evaloldzero()evalzerotrain()trainzeroBoth rails must be ready: trainingmode and clean gradients.
Derivation
Gradients and train/eval mode persist independentlyAligned uncorrected and corrected checkpoints distinguish gradient storage from module mode. Without clearing, batch two adds g2 to stored g1. Explicit zero_grad clears that state first. Separately, eval mode persists until train is called. Clearing gradients does not restore training mode.Two independent kinds of persistent stateUNCORRECTEDEXPLICIT RESETBefore batch 2 backward.grad = g1zero_grad() ->NoneAfter batch 2 backward.grad = g1 + g2.grad = g2After validation calls eval()still eval modetrain() -> trainmode
zero_grad clears gradient storage; train restores module mode. They repair different persistent states. The corrected batch-two gradient excludes stale g1.
Check: Does eval clear gradients? Which separate calls repair the two retained-state mistakes?
Read it this way: A training iteration needs both the intended model mode and a clean gradient buffer. Each action changes a separate persistent state.
After zeroing gradients in evaluation mode, which state still needs repair?

Calculate the stale-gradient effect

Use a scalar SGD calculation, not Adam: , rate , loss . The first target gives gradient and . For target , the new gradient is .

Buffer before the second updateUsed gradientNext weight
Cleared before backward
Old retained

Stale accumulation changes the update. It is not generally an N-times learning-rate multiplier, especially with Adam’s moments. Intentional accumulation needs its own reduction and reset boundary.

Code with an explicit task

This complete CPU fixture uses two-class logits and integer class targets. Its tiny split tests execution, not convergence or evaluation validity.

import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset

torch.manual_seed(0)
device = torch.device("cpu")
train_data = TensorDataset(
	torch.tensor([[0., 0.], [1., 0.], [0., 1.], [1., 1.]]),
	torch.tensor([0, 1, 1, 0]),
)
val_data = TensorDataset(
	torch.tensor([[0.2, 0.1], [0.8, 0.1]]),
	torch.tensor([0, 1]),
)
dataloader = DataLoader(train_data, batch_size=2, shuffle=False)
val_loader = DataLoader(val_data, batch_size=2, shuffle=False)
model = nn.Sequential(
	nn.Linear(2, 4), nn.ReLU(), nn.Dropout(0.2), nn.Linear(4, 2)
).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
for epoch in range(2):
	model.train()
	for batch_inputs, batch_targets in dataloader:
		batch_inputs = batch_inputs.to(device)
		batch_targets = batch_targets.to(device)
		optimizer.zero_grad(set_to_none=True)
		loss = criterion(model(batch_inputs), batch_targets)
		loss.backward()
		optimizer.step()
	model.eval()
	val_sum, val_count = 0.0, 0
	with torch.no_grad():
		for batch_inputs, batch_targets in val_loader:
			batch_inputs = batch_inputs.to(device)
			batch_targets = batch_targets.to(device)
			val_sum += criterion(model(batch_inputs), batch_targets).item() * batch_targets.numel()
			val_count += batch_targets.numel()
	print(epoch, val_sum / val_count)
	model.train()

Change the loop requirement

1. Validate between training updates

New task: run one diagnostic validation batch immediately after an update.

Picture and change: replace the epoch validation block with this excerpt after optimizer.step(). It uses the fixture’s existing model, device, loss and validation loader.

validation_inputs, validation_targets = next(iter(val_loader))
validation_inputs = validation_inputs.to(device)
validation_targets = validation_targets.to(device)
previous_mode = model.training
model.eval()
try:
	with torch.no_grad():
		val_loss = criterion(model(validation_inputs), validation_targets)
finally:
	model.train(previous_mode)

Check: entered from training mode, it exits in training mode and val_loss.requires_grad is false.

Use when: more training forwards follow the diagnostic in the same epoch.

2. Bound measured gradient growth

New task: require a maximum gradient norm of one after observing finite gradient growth.

Picture and change: insert this between backward and the optimizer step, retaining gradient clearing.

torch.nn.utils.clip_grad_norm_(
	model.parameters(), max_norm=1.0, error_if_nonfinite=True
)

Check: gradient has norm five and becomes approximately . Nonfinite norms raise an error.

Use when: evidence motivates clipping. Its absence alone is not a demonstrated defect.

Do not invent the task from the loss name

MSE can be correct for regression with matching floating targets and outputs. The original excerpt does not establish classification, validation overlap or a GPU requirement. Check those contracts before changing them.

Try separating recording from mode

Validation uses no_grad(), but the next training batch behaves differently. Which state must be inspected?

Check your answer

Inspect model.training and mode-sensitive layers such as dropout and BatchNorm. no_grad() controls recording, not persistent module mode.

Choose the next discriminating check

Suspected defectCheap checkPossible consequence
Broken gradient flowInspect requires_grad, parameter gradients and parameter changesNo learning or an incorrect update
Wrong loss contractCheck output shape, target dtype, reduction and API inputsCrash or silently wrong objective
Mode or recording leakInspect module mode and validation graph stateChanged dropout/BatchNorm or unnecessary graph memory
Device mismatchCompare parameter, input and target devicesRuntime error or an unintended CPU run
Unstable numericsLocate the first nonfinite value and inspect gradient scaleCorrupted update; clipping may not repair the cause

Missing validation variables can crash; a fixed batch may instead mislead evaluation. Neither consequence follows without checking what those variables contain. Repeated validation inside every batch also changes cost and selection exposure.

Common follow-up

“You’ve fixed all the bugs. The model still isn’t training. What now?”

If the repaired loop still fails, try overfitting one batch and inspect predictions and gradients. Then test a controlled rate change; see model debugging.

API references: zero_grad, module modes, no_grad, and gradient clipping.

Related: attention implementation and learning-rate selection.