Concepts / Computation Graphs and Backpropagation

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.

  • Programming

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?

  • Yes, the graph stays around by default

  • No, the graph is normally destroyed after the gradients are calculated

Reveal answer
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.

x1input, no gradientrequestedx1 times w1zz = x1 w1 + basigmoid of zlossbinary cross-entropy of aagainst yw1weight, requires_grad=Trueytarget, no gradientrequestedbbias, requires_grad=TrueforwardforwardaddaddsigmoidBCEBCE
Which nodes does the loss depend on, and in which order does the data move from inputs to the loss? Gradients later travel the same edges in reverse, from the loss back toward the parameters.

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.

gradfunction fromtorch.autogradlossthe output beingdifferentiatedbthe variable differentiatedwith respect toretain_graph=Truekeep the graph after thiscallarg 1arg 2keyword
Which part of the call identifies the loss, which identifies the variable being differentiated, and which controls whether the graph is kept?
python
Notes on the code
  • 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.

Graph builtforward ops recordedFirst grad callgrad(loss, b)Graph freeddefault behaviorSecond grad callgrad(loss, w1)Graph releasedrequest can failGraph keptretain_graph=TrueSecond gradientcomputed from the samegraphbackwarddefaultreuseno retain_graphretain_graph=Truereuse
What happens to the graph's stored values after the first grad call, and why does a second call fail unless retain_graph=True was set?

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.

Worked example

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.

  1. 1
    Identify the shared loss

    Both gradients are requested from the same loss, so both calls depend on the same computation graph.

  2. 2
    Consider 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.

  3. 3
    Set 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.

  4. 4
    Make 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.

Result

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 like

    A weight is created as a plain tensor, and then grad(loss, w1) is called on it.

    Why it fails

    Autograd only tracks tensors created with requires_grad set to True, so the parameter is not one whose gradient this script can ask for.

    Do this instead

    Create 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 like

    grad(loss, b) runs first, and then grad(loss, w1) runs against the same loss with no setting.

    Why it fails

    PyTorch frees the graph by default after gradients are calculated, so the second request can fail.

    Do this instead

    Add 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 like

    A printed gradient shows a parenthesis and a trailing comma and is taken as a different kind of result.

    Why it fails

    The returned value is wrapped in a tuple, which changes how the printed output looks but not the gradient itself.

    Do this instead

    Check 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.

  1. 1
    Check the setup: confirm which tensors were created with requires_grad set to True and that the loss was built from them.
  2. 2
    Check each grad call: confirm which variable is being differentiated and whether retain_graph=True is present on every call that needs the shared graph.
  3. 3
    Check the printed output: remember that the values come back wrapped in a tuple before concluding the gradient is wrong.
MEDIUM

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

  1. 1
    The 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).
  2. 2
    Parameters must be created with requires_grad set to True for Autograd to track them.
  3. 3
    PyTorch frees the computation graph by default after gradients are calculated; retain_graph=True keeps it available for a later request against the same loss.
  4. 4
    When 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 with requires_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.

Related concepts

More concepts starting with C