Computation Graphs and Backpropagation
Manual gradient computation uses grad(loss, tensor) from torch.autograd to ask how the loss changes with respect to a chosen parameter.
A Small Script With a Hidden Trap
Manual gradient code is usually short. A typical example builds a tiny model with one input, one weight, and one bias, passes the input through a sigmoid and a binary cross-entropy loss, and then asks PyTorch for the gradient of that loss with respect to one of the parameters. The whole script may be only a dozen lines, but a wrong result almost always traces back to one specific line. This article teaches you to follow the script line by line, watching how each variable gets its value, so that you can notice the moment the program's state stops matching what you expected.
What do you think happens?
After the script calls grad(loss, b) once, can it call grad(loss, w1) against the same loss without any extra setting?
Reveal answer
No, the graph is normally destroyed after the gradients are calculated
By default PyTorch destroys the computation graph after gradients are calculated, to free memory. A second request against the same loss can fail because the graph it needs has been released. Keeping the graph requires retain_graph=True on the call that needs it.
Tracing the Forward Pass
Before any gradient is requested, the script has already built a chain of operations. The input x1 is multiplied by the weight w1, the bias b is added to produce z, the sigmoid of z gives a, and the binary cross-entropy of a against the target y gives the loss. In symbols, the chain is $z = x_1 w_1 + b$ followed by $a = \sigma(z)$, with the loss computed from $a$ and $y$. Each of these steps is recorded by PyTorch as it runs, and that recorded chain is what people mean by the computation graph.
The diagram shows the direction of data during the forward pass, from the inputs toward the loss. The parameters w1 and b are marked as requiring a gradient because the script creates them with requires_grad set to True. Tensors without that flag, such as the input x1 and the target y, are not the variables whose gradients this script asks for. Notice that the loss depends on every node on the path from the parameters to the loss, which is why the gradient of the loss with respect to a parameter can be assembled by walking that path backward.
Asking grad for a Gradient
The gradient of a loss with respect to a parameter answers the question of how the loss changes when that parameter changes. In this script, PyTorch is asked to compute that answer with the grad function, which is imported from torch.autograd. The function takes the loss and the variable you want to differentiate with respect to. The call grad(loss, b, retain_graph=True) asks how the loss changes as the bias b changes, and the result is stored in a variable named grad_L_b. The same pattern works for any parameter created with requires_grad set to True, such as the weight w1.
- grad is imported from torch.autograd
- loss is the output of the forward pass
- b is the parameter being differentiated; it was created with
requires_grad=True retain_graph=True keeps the graph in memory after this call
Why the Graph Is Freed by Default
By default, PyTorch destroys the computation graph after calculating the gradients, in order to free memory. This is a sensible default. A graph can hold many intermediate values, and once the gradient has been computed, a standard training step usually does not need them again. The consequence is that the graph is no longer available for another gradient request unless you explicitly ask PyTorch to keep it.
The lower path of the diagram is the default. After the first request, the graph is freed, and a second request against the same loss may fail. The upper path is what happens when the first call keeps the graph. Because the call can keep the graph available, a later call can still reach the values it needs.
Keeping the Graph With retain_graph
The script changes the default with retain_graph=True. This keyword argument tells PyTorch to keep the graph in memory after the call finishes. The source gives the reason directly: the script will reuse the same computation graph shortly, so it must stay available. Both the gradient with respect to the weight w1 and the gradient with respect to the bias b come from the same loss, and that shared loss is why the graph must survive the first request.
Two Gradients From One Loss
A script needs the gradient of one loss with respect to both the weight w1 and the bias b. Decide where retain_graph=True must appear.
- 1Identify the shared loss
Both gradients are requested from the same loss, so both calls depend on the same computation graph.
- 2Consider the first call
The first grad call would normally free the graph after it finishes. That default is a problem if another call still needs the graph.
- 3Set retain_graph on the first call
Writing
grad(loss, b, retain_graph=True)keeps the graph in memory, so the graph is still available for the next request. - 4Make the second call
Requesting
grad(loss, w1)afterward can now reuse the kept graph. Whether a later call also needs the setting depends on whether something else still needs the graph after it.
The call that comes before the reuse must keep the graph. Setting retain_graph=True on the first grad call lets the second request use the same loss.
Mistakes That Produce Wrong or Missing Gradients
Common mistake Creating a parameter without requires_grad set to True
What it looks likeA weight is created as a plain tensor, and then grad(loss, w1) is called on it.
Why it failsAutograd only tracks tensors created with
requires_gradset to True, so the parameter is not one whose gradient this script can ask for.Do this insteadCreate the parameters with requires_grad=True and confirm that the loss was built from them.
Common mistake Omitting retain_graph=True on the call that needs a shared graph
What it looks likegrad(loss, b) runs first, and then grad(loss, w1) runs against the same loss with no setting.
Why it failsPyTorch frees the graph by default after gradients are calculated, so the second request can fail.
Do this insteadAdd retain_graph=True to the earlier call that must leave the graph available for reuse.
Common mistake Reading the tuple wrapping as part of the value
What it looks likeA printed gradient shows a parenthesis and a trailing comma and is taken as a different kind of result.
Why it failsThe returned value is wrapped in a tuple, which changes how the printed output looks but not the gradient itself.
Do this insteadCheck the tuple wrapping first when a printed gradient looks unfamiliar.
Common mistake Confusing the target y with a parameter, which means asking for its gradient when the script never tracked it
Finding Where a Gradient Goes Wrong
When a manual gradient script gives a result you did not expect, work through it in order rather than guessing. The source recommends three checkpoints, in this sequence: the setup, each grad call, and then the printed output. The flow below shows how to move through them.
- 1Check the setup: confirm which tensors were created with
requires_gradset to True and that the loss was built from them. - 2Check each grad call: confirm which variable is being differentiated and whether
retain_graph=True is present on every call that needs the shared graph. - 3Check the printed output: remember that the values come back wrapped in a tuple before concluding the gradient is wrong.
A script builds a loss from a weight w1 and a bias b, both created with requires_grad=True. It calls grad(loss, b) first with no retain_graph setting, and then calls grad(loss, w1). Which step is the most likely origin of the failure, and what single change fixes it?
Hints
- Ask which call is the first to use up the graph.
- Think about what the default behavior is after gradients are calculated.
Key Takeaways From the Gradient Trace
- 1The computation graph is the recorded chain of operations from inputs to loss, and the gradient of the loss with respect to a parameter is requested with
grad(loss, tensor). - 2Parameters must be created with
requires_gradset to True for Autograd to track them. - 3PyTorch frees the computation graph by default after gradients are calculated;
retain_graph=True keeps it available for a later request against the same loss. - 4When a result diverges, check the setup, then each grad call, then the printed tuple output, in that order.
At a glance
- The computation graph is the recorded chain of operations, from inputs through z and a to the loss, that PyTorch builds as the forward pass runs.
grad(loss, tensor)from torch.autograd asks how the loss changes with respect to a chosen parameter, and the parameter must be created withrequires_grad=True.- PyTorch frees the computation graph by default after gradients are calculated;
retain_graph=True keeps it available when a later request needs the same loss. - When a manual gradient script misbehaves, check the setup, then each grad call, then the printed output, which comes back wrapped in a tuple.