the pytorch path · 0/12
start the path

the pytorch path · chapter 02 of 12 · part i, the model

Autograd

The autograd tape isn't traced once ahead of time; it's built fresh, node by node, every time your Python actually runs.

the goal Given a short sequence of tensor operations, draw the graph autograd records, predict what backward frees and accumulates, and know when to replace the automatic rule with your own.

mastery work · this chapter0/5
  1. go →
  2. go →auto
manual items are your word; auto items complete from your streaks, labs, and can-you ticks · stored in your browser only
§ 01

The tape gets built as you go

Multiply a tensor by itself and sum it, with requires_grad=True set on the input, and PyTorch doesn't wait for you to ask for a gradient before it starts working. Each operation you run appends a node to a graph right then, live, as the Python executes: y.grad_fn is that graph, and it exists the moment y does, not after some separate tracing pass discovers it.

This is define-by-run, and it's worth naming the alternative it's not, because the contrast is the whole personality of the framework. A trace-once system runs your function one time to build a static program, then replays that same program on every later call. PyTorch's tape is rebuilt from scratch on every forward pass: run the same Python twice, with a branch that goes a different way the second time, and you get two different graphs, both entirely valid, because nothing was ever fixed in advance.

The tape is built by running, and consumed by backward.
run it (verified, torch 2.2.2 CPU): the tape appears as you run, and backward consumes it
import torch

x = torch.ones(3, requires_grad=True)
y = (x * x).sum()
print(y.grad_fn)     # <SumBackward0 object ...>: the tape, built as you ran
y.backward()
print(x.grad)        # tensor([2., 2., 2.])
the tape: built by running, consumed by backward, freed unless you ask otherwise
x leafrequires_grad x * x sum forward y scalar loss SumBackward0 y.grad_fn MulBackward0 saved: x backward fills x.grad the graph frees itself as backward passes: the museum has the receipt
§ 02

Backward walks it once, then throws it away

Call y.backward() and PyTorch walks the graph from y back toward every leaf tensor, applying the chain rule at each node and accumulating the result into that leaf's .grad. By default, once that walk finishes, the graph is gone: the intermediate values backward needed are freed, because keeping them around for a second walk nobody asked for would just be wasted memory.

Call .backward() a second time on the same y without telling PyTorch to keep the graph, and it raises, because the very thing the second call needs to walk has already been discarded. That error isn't a bug to route around quietly; it's the direct, honest consequence of a design that frees what it no longer needs. Passing retain_graph=True on the first call is how you tell PyTorch you meant to walk it again.

§ 03

No_grad, leaves, and where .grad accumulates

torch.no_grad() and torch.inference_mode() both suspend the recording itself: operations inside either context don't append anything to a graph at all, which is exactly what you want once you're only running a model forward and never planning to call backward() on the result.

Whether .grad lands anywhere depends on whether a tensor is a leaf. A leaf is a tensor you created directly, with requires_grad=True set, rather than one produced by an operation on other tensors; only leaves accumulate gradients in .grad by default. And accumulate is literal: call backward() twice in a row without clearing gradients between calls, and the second call's gradients get added on top of the first's, not swapped in for them. That's exactly why every training loop in the next chapter opens with zero_grad(): skip it, and your gradients are the sum of every step you've ever run, not just the current one.

§ 04

When the automatic rule is wrong

Every operation's backward behavior comes from a rule PyTorch already knows, registered once for that primitive. Sometimes that rule is the wrong one to use, numerically unstable, or the operation itself is something autograd has no way to see through: a call out to code the tracer can't inspect. torch.autograd.Function is the escape hatch. Subclass it, define a forward and a backward by hand, and you've supplied the derivative yourself instead of trusting the automatic table.

The role this plays is the same one the JAX path's custom_vjp plays on the other side of the two philosophies: a place to stand between the framework not knowing how to differentiate something and training still working. Different tape, same fix.

assigned

Readings

runnable

Labs