3.3.2 · D3Deep Learning Frameworks

Worked examples — Autograd and computational graphs in PyTorch

2,244 words10 min readBack to topic

This page is the practice deep-dive for Autograd and computational graphs in PyTorch. The parent note built the machine; here we run it on every kind of input it can face. Before any code, we map out the full space of cases so nothing surprises you later.

If a word here feels unfamiliar (gradient, chain rule, .backward()), the parent note defines it — this page assumes you have read it and now want to feel the mechanics through examples.


The scenario matrix

Autograd is a small idea (record operations, replay them backward) but it has many corners. A gradient can be positive, negative, or exactly zero; a graph can be used once or many times; a branch can be alive or cut off. The table below lists every corner. Each later example is tagged with the cell it lands in.

# Case class What is special about it Covered by
A Positive gradient derivative : output rises when input rises Ex 1
B Negative gradient derivative : output falls when input rises Ex 2
C Zero gradient (dead branch) derivative at the operating point (ReLU on a negative, saturated point) Ex 3
D Multiple paths to one node a variable feeds two operations — gradients must sum Ex 4
E Gradient accumulation across calls .backward() twice without zeroing — the .grad adds up Ex 5
F Detached / stopped gradient .detach() cuts the graph — no gradient flows past it Ex 6
G Non-scalar output .backward() on a vector needs a seed vector (Jacobian-vector product) Ex 7
H Real-world word problem linear layer + loss, compute weight gradients Ex 8
I Exam twist / degenerate requires_grad=False leaf, or in-place error Ex 9

Every numeric answer below is machine-checked in the verify block.


Example 1 — Positive gradient (Cell A)

Steps.

  1. Forward: . Why this step? We must run the forward pass so PyTorch records the graph (a PowBackward and an AddBackward node).

  2. Hand-derivative: . Why this step? Power rule on gives ; the linear term gives . Adds because derivative of a sum is the sum of derivatives.

  3. Plug : . Why this step? Autograd evaluates the derivative at the current point, not symbolically.

x = torch.tensor([2.0], requires_grad=True)
z = x**2 + 3*x
z.backward()
print(x.grad)   # tensor([7.])

Verify: , so increases with — matches the intuition that both and rise for . ✓


Example 2 — Negative gradient (Cell B)

Steps.

  1. Forward: . Why? Record the graph.

  2. Derivative: . Why? ; by the power rule, with the minus sign carried through.

  3. Plug : . Still positive here. Try : negative. Why the second point? To actually hit Cell B we need a point where the slope is negative; delivers it.

x = torch.tensor([2.0], requires_grad=True)
z = 4*x - x**3
z.backward()
print(x.grad)   # tensor([-8.])

Verify: at , increasing shrinks (the term dominates). Sign confirms Cell B. ✓


Example 3 — Zero gradient / dead branch (Cell C)

Figure — Autograd and computational graphs in PyTorch

Steps.

  1. Forward: . Why? ReLU clamps negatives to zero — look at the red flat segment on the left of the figure.

  2. Slope of ReLU: it is for and for . Why? On the left the graph is a flat horizontal line — flat means zero slope. On the right it is the line — slope .

  3. At we sit on the flat part, so . Why this matters? A zero gradient means no learning signal flows back through this unit — the "dying ReLU" problem.

x = torch.tensor([-1.5], requires_grad=True)
y = torch.relu(x)
y.backward()
print(x.grad)   # tensor([0.])

Verify: the branch is dead — moving a tiny bit still gives , so the change per unit input is . ✓


Example 4 — One variable, multiple paths (Cell D)

Figure — Autograd and computational graphs in PyTorch

Steps.

  1. Draw the graph (figure): splits into a square path () and a linear path (), then an Add merges them. Why? The red node has two outgoing edges — this is exactly the multi-path case.

  2. Path 1 gradient: . Path 2 gradient: . Why separately? The multivariate chain rule says each path from to contributes independently.

  3. Sum the paths: . Why sum, not multiply? The paths run in parallel into the same output. Parallel contributions add (they'd multiply only if they were in series, one after the other).

x = torch.tensor([3.0], requires_grad=True)
z = x*x + x
z.backward()
print(x.grad)   # tensor([7.])

Verify: direct derivative matches the sum-of-paths answer. ✓


Example 5 — Accumulation across two backward calls (Cell E)

Steps.

  1. First backward: x.grad . Why? .grad starts empty, so it just stores .

  2. Second backward (graph retained): PyTorch adds to the existing .grad. Why add? By default .backward() accumulates so that mini-batch gradients sum automatically (parent note, Step 3).

  3. Result: .

x = torch.tensor([2.0], requires_grad=True)
y = x**2
y.backward(retain_graph=True)   # x.grad = 4
y.backward()                    # x.grad = 4 + 4 = 8
print(x.grad)   # tensor([8.])

Verify: . ✓


Example 6 — Detaching to stop gradient flow (Cell F)

Figure — Autograd and computational graphs in PyTorch

Steps.

  1. Compute the value: , (detach copies the value, drops the history). Why? .detach() returns a tensor equal in value but with no grad_fn — see the red scissors cutting the edge in the figure.

  2. Now where is treated as a constant . Why? Autograd cannot see through back to ; that path is severed.

  3. So (the visible only), not . Why? Only the un-detached occurrence of contributes.

x = torch.tensor([2.0], requires_grad=True)
a = x**2
b = a.detach()
z = b * x
z.backward()
print(x.grad)   # tensor([4.])

Verify: treating as constant, . Detach removed the that the path would have added (). ✓


Example 7 — Non-scalar output needs a seed (Cell G)

Steps.

  1. .backward() on a scalar means "start with ". A vector has no single output to start from. Why the error? Autograd needs to know how much each output matters — a weight per component.

  2. Supply that weight vector . Autograd computes the Jacobian-vector product . Why this tool (Jacobian-vector product) and not a full Jacobian? Storing the whole Jacobian is wasteful; deep learning only ever needs (that is exactly what the chain rule multiplies), so autograd computes it directly.

  3. Each component: , weighted by : .

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x**2
y.backward(torch.tensor([1.0, 1.0, 1.0]))
print(x.grad)   # tensor([2., 4., 6.])

Verify: each entry equals : . Using is the same as calling y.sum().backward(). ✓


Example 8 — Real-world word problem: a linear layer (Cell H)

Steps.

  1. Forward: . Why? Standard weighted sum — the forward pass of one neuron.

  2. Since , . Why? Identity loss; its derivative is .

  3. Chain to each weight: . So , , . Why input? In , differentiating w.r.t. leaves ; the bias has coefficient .

x = torch.tensor([1.0, 2.0])
w = torch.tensor([0.5, -0.3], requires_grad=True)
b = torch.tensor([0.1], requires_grad=True)
z = (w * x).sum() + b
z.backward()
print(w.grad)   # tensor([1., 2.])
print(b.grad)   # tensor([1.])

This is the atom of 3.2.04-Backpropagation-algorithm and of every layer in 3.4.01-Building-neural-networks-with-torch.nn.Module.

Verify: , , independent of the (zero) value of . ✓


Example 9 — Exam twist: a non-tracked leaf (Cell I)

Steps.

  1. Only tensors with requires_grad=True are tracked. Why? Autograd records operations on tracked leaves; untracked ones are treated as constants (efficiency — you rarely need gradients w.r.t. fixed data).

  2. , so w.grad = 2. Why? In , differentiating w.r.t. leaves the constant .

  3. x.grad = None, because was never marked for tracking. Why not zero? None means "no gradient buffer was ever created", which is different from a computed value of .

x = torch.tensor([2.0], requires_grad=False)
w = torch.tensor([3.0], requires_grad=True)
z = w * x
z.backward()
print(w.grad)   # tensor([2.])
print(x.grad)   # None

Verify: ; is None. ✓


Recall

Recall Why do parallel paths from one node

add their gradients? Because they are separate routes into the same output; the multivariate chain rule sums contributions along parallel paths (series paths would multiply). ::: See Example 4.

Recall What is the difference between

x.grad = None and x.grad = 0? None means the tensor was never tracked (requires_grad=False); 0 means it was tracked but the slope at that point is zero (e.g. ReLU on a negative). ::: Examples 3 and 9.

Recall Why does

y.backward() fail for a vector y? A vector has no single scalar to seed ; you must pass a weight vector so autograd can compute . ::: Example 7.

Connections: gradient flow feeds 3.5.02-Gradient-descent-variants; detaching is central to 4.2.03-Batch-normalization running stats and 5.3.01-Transfer-learning frozen backbones; retaining/rebuilding graphs relates to 6.4.02-Gradient-checkpointing. Tensor basics: 3.301-Introduction-to-PyTorch-tensors-and-operations.