Computational graphs and autodiff — how frameworks compute every gradient in one backward pass
Every framework call to .backward() is backprop. What matters isn't the calculus — it's understanding what the framework is doing under the hood: how a graph of tensor operations turns into a single efficient gradient computation.
This page covers the deep-learning framing: computational graphs and automatic differentiation. The chain-rule derivation itself is covered in Calculus → Backpropagation — read that first if you want the math from scratch.
Picture the forward pass as a DAG: each node is an operation (matmul, add, activation), each edge carries a tensor. Autodiff attaches a "local gradient recipe" to every node when it runs forward. The backward pass then walks the graph in reverse, multiplying local gradients together via the chain rule to get the gradient of the loss with respect to every parameter — all in one traversal.
Nothing is symbolic or numeric-differencing here — it's exact derivatives, computed mechanically, reusing the same graph the forward pass built.
Memory cost scales with depth because every cached forward activation must be kept alive until its node is visited on the way back — this is why very deep nets are memory-hungry and why techniques like gradient checkpointing (recompute instead of cache) exist.
Naively, you could estimate each weight's gradient by perturbing it and re-running the forward pass (finite differences) — that's one forward pass per parameter, infeasible at millions of parameters. Backprop instead reuses shared intermediate gradients: a gradient computed once at a shared node is reused by every path that depends on it, so the total cost of computing all gradients is about the same as one extra forward pass.
requires_grad=True — marks a tensor so operations on it are recorded onto the graphloss.backward() — traverses the graph in reverse, accumulates into .grad on every leaf tensoroptimizer.step() — applies the update rule (see Optimization) using those accumulated gradientsoptimizer.zero_grad() — clears .grad before the next forward pass; gradients accumulate by default, a classic bug source