Gradient Calculation in PyTorch
Calling loss.backward() computes gradients and stores them in the .grad attribute of each model weight tensor.
When Training Becomes Unstable
A neural network learns by adjusting its weights. Each weight receives a gradient, and that gradient tells the training process how much the weight should change and in which direction. In most steps these gradients are moderate, and training proceeds smoothly. Sometimes, however, a gradient becomes very large. When that happens, a single update can move the weights far away from where they were, and training can become unstable. This problem is known as exploding gradients.
Gradient clipping is the technique used to prevent exploding gradients by limiting how large the gradients can become. In this article you will follow a short sequence of PyTorch calls, watch a single value at several points, and work out what each step contributed. The goal is not only to remember the function name, but to be able to tell exactly where in the sequence a result stops matching your expectations.
Tracing the Order of a Training Step
A training iteration has a fixed order. The forward pass runs the model on a batch of inputs and produces predictions. A loss function compares those predictions with the targets. The loss.backward() call then computes gradients. Gradient clipping comes after .backward(), and the optimizer step that uses the gradients to update weights comes last. The order is not a formality: each stage depends on the output of the one before it, so the order of calls determines what each printed result actually means.
Clipping sits between backward and the optimizer step because it needs gradients to exist (they are created by backward) and because the update should use the already limited values. Placing it before backward would mean there is nothing to limit. Placing it after the optimizer step would mean the weights were already updated with the unlimited gradients.
Where Gradients Are Stored
Calling loss.backward() computes gradients and stores them in the .grad attribute of each model weight tensor.
The gradients are not returned by backward() and they are not stored in a separate gradient table. Instead, PyTorch attaches each one to the parameter it belongs to. After the backward call, each weight tensor that took part in the computation has a .grad attribute holding its gradient. This is why any code that reads gradients must run after .backward(). Before the backward call, the .grad attributes do not yet contain values from the current batch.
The diagram shows the idea in simplified form. The backward pass starts from the scalar loss and reaches each weight tensor that the loss depends on. Each weight then holds its own gradient in its .grad attribute. A learner who wants to inspect the result can read the .grad field of a specific parameter, which is exactly what the utility in the next section does across all parameters at once.
Tracing the Highest Gradient Value
The source uses a utility named find_highest_gradient(model) to report one number: the largest gradient value across the model. It scans every parameter that has a .grad value, flattens each gradient tensor, and keeps the largest value it finds. Because it reads .grad, it gives a meaningful answer only after .backward() has run. The code below is an illustrative version written for this article, not the source's exact implementation. It uses the magnitude of each gradient so that the largest value is compared regardless of sign.
The exact numbers vary with the random inputs. The first line prints 0.0 because no gradients exist yet. The second line prints a positive value. The third line prints a value that is at most the second one when clipping reduces the gradients.find_highest_gradientstarts at 0.0 and only updates when a parameter actually has a .grad value- The first print runs before backward, so the utility has no gradient values to scan
loss.backward()fills the .grad attribute of each weight and bias- clip_grad_norm_ is called on
model.parameters()withmax_norm=1.0 after backward and before the optimizer step
Reading the Three Printed Values
A learner runs the sequence above and wants to know which stage produced each printed number, and what a change between the second and third print would mean.
- 1Read the first print
This value is taken before
loss.backward()runs. The utility finds no gradients to scan, so it reports the starting value of 0.0. It does not reflect the current batch. - 2Read the second print
This value is taken after backward. Each parameter now has a .grad tensor, and the utility reports the largest magnitude found among them. This is the baseline for judging clipping.
- 3Read the third print
This value is taken after clip_grad_norm_ with
max_norm=1.0. If the combined gradient norm was above 1.0, the gradients were scaled down, so the largest value is smaller or equal. If the norm was already at or below 1.0, clipping leaves the gradients unchanged. - 4Locate a departure
If the third print is larger than the second, the expectation has broken. Clipping only limits gradients, so the most likely cause is that the clipping call ran on different gradients than the ones you are reading, or that the values were read from a different backward run.
Each printed number belongs to a stage: before backward, after backward, and after clipping. Tracking the stage is what lets you find the point where an observed result departs from expectation.
Clipping With clip_grad_norm_
Gradient clipping limits the size of gradients to help prevent exploding gradients. In PyTorch it is applied with torch.nn.utils.clip_grad_norm_(). In this course the call uses max_norm=1.0. The call takes the model's parameters, and it works on their gradients. Its documented behavior is to measure the norm of the gradients together and, when that combined norm exceeds max_norm, to scale the gradients down so the norm equals max_norm. When the combined norm is already at or below max_norm, the gradients are left as they are.
The before and after panels describe the same parameters at two moments. When clipping triggers, every gradient is scaled by the same factor, so the largest value shrinks along with the rest. The utility is the way to observe this effect, since it reports the highest value at each point in the sequence.
A common question is whether the threshold is applied to each parameter separately or to one combined value. The clipping call measures the gradients together across the parameters it receives, so the limit is a single shared threshold rather than a separate cap on every weight. A single parameter with an unusually large gradient can therefore lower the scale applied to all the gradients.
Mistakes in Call Order
Common mistake Reading the highest gradient before calling backward
What it looks likePrinting find_highest_gradient(model) right after the loss is computed, then assuming the value describes the current batch.
Why it failsThe .grad attributes are filled in by the backward call, so before it runs there are no current gradient values to scan.
Do this insteadCall loss.backward() first, then read the utility.
Common mistake Clipping before gradients exist
What it looks likeCalling clip_grad_norm_ before loss.backward() in the training step.
Why it failsClipping limits gradients, and there are none to limit until backward has created them.
Do this insteadPlace the clipping call after backward and before the optimizer step.
Common mistake Expecting the value to rise after clipping
What it looks likeSeeing a larger highest value in the third print and concluding that clipping works in reverse.
Why it failsClipping only scales gradients down when their combined norm is above
max_norm, so it cannot raise a gradient's magnitude.Do this insteadCompare the third print with the second one and check whether the value was measured from the same backward run.
Common mistake Assuming a single batch's gradients persist across unrelated calls without checking the sequence
Practice: Finding the Departure Point
What do you think happens?
After you call the clipping function, will the highest gradient value printed by the utility be larger, smaller, or unchanged compared with the value printed before clipping?
Reorder the three calls in the sample sequence so that clip_grad_norm_ runs before loss.backward(). Predict which printed value will fail to show the effect you expect, and explain at which step the observed result departs from what clipping should do.
Hints
- Think about what the .grad attributes contain at the moment clipping runs.
- Ask which stage produces the gradients that clipping is supposed to limit.
- Compare your predicted third print with what you would expect if the gradients were already limited.
Key Takeaways
loss.backward()computes gradients and stores each one in the .grad attribute of the weight tensor it belongs to, so gradient reads must happen after backward.- In a training iteration the order is forward pass, loss, backward, gradient clipping, then the optimizer step, and each stage depends on the one before it.
torch.nn.utils.clip_grad_norm_()withmax_norm=1.0 limits exploding gradients by scaling them down when their combined norm exceeds the limit.- Comparing the highest gradient value before and after clipping, using a utility such as
find_highest_gradient(model), shows the effect of clipping and helps locate where an observed result departs from expectation.