The five-line ritual
The unit of training is one step. It always has the same five lines, in the same order:
opt.zero_grad()— clear gradients from the previous step.logits = model(xb)— forward pass.loss = loss_fn(logits, yb)— scalar loss.loss.backward()— compute gradients via autograd.opt.step()— apply gradients to parameters.
Optionally insert clip_grad_norm_ between backward and step, and scheduler.step() after step. That's the whole loop body. Everything else is plumbing around it.
opt.zero_grad() before you finish thinking the sentence.The two famous bugs of step 1
Forgotten zero_grad() — gradients accumulate across steps, so each step sees the sum of every previous gradient. Loss stays high, training collapses. The fix is one line.
Calling opt.step() before loss.backward() — you step on stale gradients (the previous batch's). Loss curve looks vaguely like training but the model isn't actually learning the current batch.
Gradient accumulation (the right way to skip zero_grad)
Sometimes you want a larger effective batch size than fits in VRAM. The trick: skip zero_grad() for several batches, divide the loss by the number of accumulation steps, then opt.step() + opt.zero_grad() at the end. This produces gradients equivalent to a single batch accum_steps times bigger.