Computing Gradients via Autograd
The grad function computes one gradient on request and returns it, while the backward method fills the grad attribute of every tracked tensor on the path to the loss.
Predicting the Sign of a Gradient
Gradient computation is easiest to understand when you have a guess about the answer before the library hands you a number. This article follows one short PyTorch program. The program takes an input, multiplies it by a weight, adds a bias, passes the result through a sigmoid, and measures the error with a binary cross-entropy loss. PyTorch's autograd system then produces gradients for the parameters. The purpose of the program is to show two different ways of asking autograd for those gradients, and how the order of the calls affects what you can read back.
Start with the setup. The bias b begins at 0.0 and is tracked by autograd. The input x1 is 1.1, the weight w1 is 2.2, and the target y is 1.0. Before reading further, decide whether the gradient of the loss with respect to b should be positive or negative. The target is 1.0 and the sigmoid output for this starting point is below 1, so the prediction is too low. Commit to a sign before you read the answer.
What do you think happens?
For the bias b at 0.0, with x1 = 1.1, w1 = 2.2 and y = 1.0, is the gradient of the loss with respect to b positive or negative?
Reveal answer
Negative
The weighted sum is z = 2.2 × 1.1 + 0.0 = 2.42, and the sigmoid output a is about 0.92, which is below the target 1.0. For binary cross-entropy on a sigmoid output, the derivative of the loss with respect to the pre-sigmoid value is a − y, which here is about −0.08. Increasing b would push a toward 1.0 and lower the loss, so the gradient is negative. Gradient descent would therefore move b upward.
Tracing the Forward Computation
Every value in the program is a link in a chain. The weighted sum z is built from w1, x1 and b. The sigmoid output a is built from z. The loss is built from a and the target y. Autograd records these links as a computation graph while the forward values are computed, so that a later request for a gradient can walk back along the same links from the loss toward the parameters.
The tree shows the path a backward pass follows. The loss depends on a, a depends on z, and z depends on w1, x1 and b. The leaf tensors are the ones at the bottom of the tree that autograd can deliver a gradient to. Because x1 was created without requires_grad, it is on the path but does not collect a gradient of its own.
Writing the Program
The code below reconstructs the program described above. It has two phases. The first phase builds the loss and calls grad once to get the gradient of the loss with respect to b. The second phase calls backward on the same loss and reads the grad attributes of the parameters.
- x1 has no
requires_grad, so autograd does not track it as a parameter - w1 and b have
requires_grad=True, so they are the tensors that can receive gradients retain_graph=True keeps the graph alive for the backward call that follows- grad returns a tuple, so [0] selects the gradient for b
Notice that the code never passes w1 or b to backward. The loss already knows which tensors fed into it through the recorded graph. The grad call is different: it answers one question about one chosen tensor and returns the answer to you. The backward call answers the question for every tracked tensor at once and stores each answer in that tensor's grad attribute.
Return Value Versus Stored Attribute
The grad function computes one gradient on request and returns it. It takes the loss and the parameter whose gradient you want. The backward method is called on the loss tensor and computes gradients for every tracked tensor that contributed to it, storing each result in that tensor's grad attribute. The two differ in where the answer goes and how many answers come back.
| Property | grad(loss, tensor) | loss.backward() |
|---|---|---|
| Called on | The loss, with the chosen tensor passed as an argument | The loss tensor only |
| Scope | One gradient for one chosen tensor | Every tracked tensor on the path to the loss |
| Where the result goes | Returned as the value of the call | Stored in each tensor's grad attribute |
| How you read it | Assign the return value, as with grad_L_b | Read tensor.grad, as with w1.grad and b.grad |
How the two gradient calls differ in what they return and where the results are stored
What retain_graph=True Protects
Both phases of the program depend on the same computation graph. The grad call uses the graph to produce grad_L_b. The argument retain_graph=True tells autograd to keep that graph so the later loss.backward() call can still use it to fill in w1.grad and b.grad. This is why the order of the calls and the retain_graph argument matter together.
Which Tensors Receive Gradients
Only tensors created with requires_grad=True receive gradients. In the program, w1 and b have that setting, so they are the tensors whose grad attributes are filled in. The tensor x1 does not, so it takes part in the forward computation but never gets a gradient. This means the requires_grad choice decides what you can read back after the calls.
Tracing an Unexpected Gradient
When a printed gradient looks wrong, do not start with the final number. Start with the program's structure. First check which tensors have requires_grad=True, since only those receive grad attributes. Then check which call produced the value you are reading: grad returns a value, while backward fills grad attributes. Finally, trace the forward values through the same operations to find the first place where your expected value and the computed value disagree.
Tracing the bias gradient by hand
The printed value for b.grad does not match what you expected. Find where the value originates, using the program from this article with w1 = 2.2, x1 = 1.1, b = 0.0 and y = 1.0.
- 1Check the flags
b was created with
requires_grad=True, so it is a leaf that can receive a gradient. If it had been created without the flag, no value would appear in b.grad at all. - 2Recompute the forward value of z
z = w1 × x1 + b = 2.2 × 1.1 + 0.0 = 2.42. If your expected z differs, the error is in the forward line, not in autograd.
- 3Recompute the sigmoid output
a =
sigmoid(2.42), which is about 0.92. This is the link between z and the loss. - 4Apply the chain rule back to b
For binary cross-entropy on a sigmoid output, the derivative of the loss with respect to b equals a − y, which is about −0.08. The sign is negative, matching the prediction made at the start.
- 5Compare against the printed value
The first step where your hand value and the printed value disagree is the link to inspect. Look at which call produced the printed value and whether that tensor was the one you intended.
The gradient for b comes from the link between the sigmoid output and b. A mismatch points to the specific tensor and operation where the two values diverge.
This hand check is tracing, not a replacement for autograd. In practice you let backward compute the value. You trace by hand only to confirm which tensor and which operation a number came from when something looks wrong.
Mistakes That Produce Missing or Stale Gradients
Common mistake Expecting a gradient on a tensor that was created without requires_grad=True
What it looks likex1 = torch.tensor(1.1) followed by reading x1.grad after backwardWhy it failsOnly tensors created with
requires_grad=True receive gradients, so x1 never gets a grad value.Do this insteadCreate the tensor with requires_grad=True if you need its gradient, or read the gradient only from tensors that are tracked.
Common mistake Calling backward without retain_graph=True after an earlier grad call on the same loss
What it looks likegrad(loss, b) followed by loss.backward(), with no retain_graph argumentWhy it failsThe program's second phase depends on the same graph the grad call used, so the graph must be kept.
Do this insteadPass retain_graph=True to the earlier grad call, as the program does.
Common mistake Reading a return value where the answer is stored, or the reverse
What it looks likeExpecting loss.backward() to return the gradient for b
Why it failsgrad returns the gradient as its result, while backward stores gradients in the grad attributes of tracked tensors.
Do this insteadAssign the return value of grad, and read tensor.grad after backward.
Common mistake Assuming the printed gradient is correct without tracing the forward values that produced it
Practice Tracing the Program
Using the program in this article, suppose you change b to be created without requires_grad=True and keep the rest of the code the same. Which tensors would receive a grad value after loss.backward(), and what would happen to the grad call for b? Explain your answer by naming the tensors and the call responsible for each result.
Hints
- Look at which tensors have
requires_grad=True after the change. - Think about whether grad can be asked for a tensor that is not tracked.
Checking Your Understanding
Key Takeaways
- The grad function computes one chosen gradient and returns it, while backward fills the grad attribute of every tracked tensor on the path to the loss.
- Only tensors created with
requires_grad=True receive gradients, so that flag determines what you can read back. retain_graph=True keeps the computation graph available so that a backward call after grad can reuse it.- When a gradient looks wrong, trace the forward values through the same chain of operations to find the first point of disagreement.