Skip to main content
© 2026 ePowerAI — free, open courseware. No login required.
CoursesContact
ePOWERAI
CoursesContact
Week 11: Automatic Differentiation
Adv. Optimization

Duality & KKT

  1. 01Week 1: Optimality Conditions Revisited
  2. 02Week 2: Lagrangian Duality in Depth
  3. 03Week 3: KKT and Sensitivity

Cones & IPMs

  1. 04Week 4: LP, QP, and Conic Form
  2. 05Week 5: Semidefinite Programming
  3. 06Week 6: Interior-Point Methods
  4. 07Week 7: Proximal Methods and ADMM

Second-order & scale

  1. 08Week 8: Newton and Quasi-Newton
  2. 09Week 9: Variance Reduction
  3. 10Week 10: Large-Batch and Distributed Training

Geometry & training science

  1. 11Week 11: Automatic Differentiation
  2. 12Week 12: Sharpness and SAM
  3. 13Week 13: Landscapes and Implicit Bias

Capstone

  1. 14Week 14: Capstone — Structure Meets Scale
Week 11· Adv. Optimization· Geometry & training science10 min read

Week 11: Automatic Differentiation

Learning Outcomes
  • Distinguish numeric, symbolic, and automatic differentiation
  • Run forward-mode AD using dual numbers on a simple expression
  • Compute a reverse-mode VJP on a tiny computational graph by hand
  • Explain why reverse mode dominates deep learning (one scalar loss, many parameters)
Prerequisites

Background: Intermediate Optim Week 1 (chain rule for vector functions, Jacobians). Week 8 (Newton — uses second derivatives; AD can compute those too). Basic graph traversal concepts.

Later weeks: AD is the computational engine behind every gradient-based training recipe. Weeks 12–13 discuss what those gradients reveal about the loss surface and what optimisers do with them.

  • Also see: Optimization Week 1: Optimization for Learning — gradients and the chain rule, before autodiff automates them.

Why AD exists#

Every optimiser in this course needs gradients. How do we get them?

  • Numerical differentiation: f′(x)≈(f(x+h)−f(x))/hf'(x) \approx (f(x+h) - f(x))/hf′(x)≈(f(x+h)−f(x))/h. Simple but O(d)O(d)O(d) evaluations per gradient and suffers from truncation vs roundoff trade-off. Unacceptable for d=107d = 10^7d=107 parameters.
  • Symbolic differentiation: Apply calculus rules to the closed-form expression of fff. Produces exact derivatives but suffers from expression swell — the derivative expression can be exponentially larger than the original function. Impractical for programs with loops and conditionals.
  • Automatic (algorithmic) differentiation (AD): Compute derivatives by applying the chain rule to the sequence of elementary operations that the program executes. AD evaluates the derivative at a specific numeric input, producing exact (to machine precision) derivatives at a cost proportional to evaluating the function itself. This is what PyTorch, JAX, and TensorFlow do.

AD works because every computer program — no matter how complex — is a composition of elementary operations (+,×,sin⁡,exp⁡+, \times, \sin, \exp+,×,sin,exp, etc.) whose derivatives are known. The chain rule threads through this computational graph.

Forward mode#

Forward-mode AD propagates derivatives from inputs to outputs. Associate each variable vvv with a dual number v˙\dot{v}v˙ (its derivative with respect to a chosen input direction). For each elementary operation w=ϕ(u1,…,uk)w = \phi(u_1, \ldots, u_k)w=ϕ(u1​,…,uk​), compute:

w˙=∑j=1k∂ϕ∂uju˙j\dot{w} = \sum_{j=1}^k \frac{\partial \phi}{\partial u_j} \dot{u}_jw˙=∑j=1k​∂uj​∂ϕ​u˙j​

Initialise x˙=1\dot{x} = 1x˙=1 for the input of interest and y˙=0\dot{y} = 0y˙​=0 for all others.

Worked example — Dual numbers for sin⁡(x2)\sin(x^2)sin(x2) at x=2x = 2x=2:

The elementary operations are v1=x2v_1 = x^2v1​=x2, v2=sin⁡(v1)v_2 = \sin(v_1)v2​=sin(v1​). With x˙=1\dot{x} = 1x˙=1:

  • v1=22=4v_1 = 2^2 = 4v1​=22=4, v˙1=2x⋅x˙=2⋅2⋅1=4\dot{v}_1 = 2x \cdot \dot{x} = 2 \cdot 2 \cdot 1 = 4v˙1​=2x⋅x˙=2⋅2⋅1=4
  • v2=sin⁡(4)≈−0.7568v_2 = \sin(4) \approx -0.7568v2​=sin(4)≈−0.7568, v˙2=cos⁡(v1)⋅v˙1=cos⁡(4)⋅4≈−2.6146\dot{v}_2 = \cos(v_1) \cdot \dot{v}_1 = \cos(4) \cdot 4 \approx -2.6146v˙2​=cos(v1​)⋅v˙1​=cos(4)⋅4≈−2.6146

Analytic check: ddxsin⁡(x2)=2xcos⁡(x2)\frac{d}{dx}\sin(x^2) = 2x \cos(x^2)dxd​sin(x2)=2xcos(x2), at x=2x=2x=2: 4cos⁡(4)≈−2.61464 \cos(4) \approx -2.61464cos(4)≈−2.6146. Matches.

Cost of forward mode. For f:Rd→Rpf: \mathbb{R}^d \to \mathbb{R}^pf:Rd→Rp, computing the full Jacobian requires ddd forward passes (one per input dimension). Forward mode is efficient when d≪pd \ll pd≪p (few inputs, many outputs).

Reverse mode#

Reverse-mode AD (a.k.a. backpropagation) propagates derivatives from outputs back to inputs. For a scalar output f(x)f(x)f(x), this computes the full gradient ∇f(x)\nabla f(x)∇f(x) in a single backward pass.

The algorithm works in two phases:

  1. Forward pass: Evaluate the function, recording the computational graph (the "tape") — store every intermediate value and the operations that produced them.
  2. Backward pass: Traverse the graph in reverse topological order. For each operation w=ϕ(u1,…,uk)w = \phi(u_1, \ldots, u_k)w=ϕ(u1​,…,uk​), having already computed wˉ=∂f/∂w\bar{w} = \partial f / \partial wwˉ=∂f/∂w, compute the adjoint of each input:

uˉj=wˉ⋅∂ϕ∂uj\bar{u}_j = \bar{w} \cdot \frac{\partial \phi}{\partial u_j}uˉj​=wˉ⋅∂uj​∂ϕ​

accumulating contributions from all uses of uju_juj​. At the end, xˉi=∂f/∂xi\bar{x}_i = \partial f / \partial x_ixˉi​=∂f/∂xi​ for all iii.

The backward pass computes a vector-Jacobian product (VJP): given wˉ\bar{w}wˉ, compute uˉ=Jϕ(u)Twˉ\bar{u} = J_\phi(u)^T \bar{w}uˉ=Jϕ​(u)Twˉ, where JϕJ_\phiJϕ​ is the Jacobian of ϕ\phiϕ. The full gradient is the VJP of the scalar 111 with respect to all parameters.

Worked micro-graph#

Consider f(a,b,c)=(a⋅b+c)2f(a, b, c) = (a \cdot b + c)^2f(a,b,c)=(a⋅b+c)2 with inputs a=1,b=2,c=0.5a = 1, b = 2, c = 0.5a=1,b=2,c=0.5.

f = (a·b + c)² a = 1 b = 2 c = 0.5 × + (·)² v1 = 2 v2 = 2.5 v3 = 6.25 5 5 1 FORWARD VALUE ADJOINT

Evaluating f(a,b,c)=(a⋅b+c)2f(a, b, c) = (a \cdot b + c)^2f(a,b,c)=(a⋅b+c)2 at a=1,b=2,c=0.5a = 1, b = 2, c = 0.5a=1,b=2,c=0.5: the forward pass fills in v1=2v_1 = 2v1​=2, v2=2.5v_2 = 2.5v2​=2.5 and v3=6.25v_3 = 6.25v3​=6.25, then the backward pass starts from the seed vˉ3=1\bar{v}_3 = 1vˉ3​=1 and propagates vˉ2=5\bar{v}_2 = 5vˉ2​=5, vˉ1=5\bar{v}_1 = 5vˉ1​=5 back along the chain, ending with the parameter gradients ∂f/∂a=10\partial f/\partial a = 10∂f/∂a=10, ∂f/∂b=5\partial f/\partial b = 5∂f/∂b=5 and ∂f/∂c=5\partial f/\partial c = 5∂f/∂c=5.

Forward pass (building the graph):

  • v1=a×b=1×2=2v_1 = a \times b = 1 \times 2 = 2v1​=a×b=1×2=2
  • v2=v1+c=2+0.5=2.5v_2 = v_1 + c = 2 + 0.5 = 2.5v2​=v1​+c=2+0.5=2.5
  • v3=v22=2.52=6.25v_3 = v_2^2 = 2.5^2 = 6.25v3​=v22​=2.52=6.25 (output f=v3f = v_3f=v3​)

Backward pass:

  • vˉ3=∂f/∂v3=1\bar{v}_3 = \partial f / \partial v_3 = 1vˉ3​=∂f/∂v3​=1 (seed)
  • Operation v3=v22v_3 = v_2^2v3​=v22​: ∂v3/∂v2=2v2=5\partial v_3 / \partial v_2 = 2v_2 = 5∂v3​/∂v2​=2v2​=5, so vˉ2=1⋅5=5\bar{v}_2 = 1 \cdot 5 = 5vˉ2​=1⋅5=5
  • Operation v2=v1+cv_2 = v_1 + cv2​=v1​+c:
    • ∂v2/∂v1=1\partial v_2 / \partial v_1 = 1∂v2​/∂v1​=1, so vˉ1=vˉ2⋅1=5\bar{v}_1 = \bar{v}_2 \cdot 1 = 5vˉ1​=vˉ2​⋅1=5
    • ∂v2/∂c=1\partial v_2 / \partial c = 1∂v2​/∂c=1, so cˉ=vˉ2⋅1=5\bar{c} = \bar{v}_2 \cdot 1 = 5cˉ=vˉ2​⋅1=5
  • Operation v1=a×bv_1 = a \times bv1​=a×b:
    • ∂v1/∂a=b=2\partial v_1 / \partial a = b = 2∂v1​/∂a=b=2, so aˉ=vˉ1⋅2=10\bar{a} = \bar{v}_1 \cdot 2 = 10aˉ=vˉ1​⋅2=10
    • ∂v1/∂b=a=1\partial v_1 / \partial b = a = 1∂v1​/∂b=a=1, so bˉ=vˉ1⋅1=5\bar{b} = \bar{v}_1 \cdot 1 = 5bˉ=vˉ1​⋅1=5

Analytic check: ∂f/∂a=2(ab+c)⋅b=2(2.5)⋅2=10\partial f / \partial a = 2(ab + c) \cdot b = 2(2.5) \cdot 2 = 10∂f/∂a=2(ab+c)⋅b=2(2.5)⋅2=10 ✓. ∂f/∂b=2(ab+c)⋅a=5\partial f / \partial b = 2(ab + c) \cdot a = 5∂f/∂b=2(ab+c)⋅a=5 ✓. ∂f/∂c=2(ab+c)=5\partial f / \partial c = 2(ab + c) = 5∂f/∂c=2(ab+c)=5 ✓.

Complexity#

Reverse mode for scalar-output functions (the case for most ML losses):

cost(∇f)≤c⋅cost(f)\text{cost}(\nabla f) \leq c \cdot \text{cost}(f)cost(∇f)≤c⋅cost(f)

where ccc is a small constant (typically c≈3c \approx 3c≈3–555). The gradient of a scalar function of ddd variables costs roughly the same as evaluating the function itself — independent of ddd. This is the cheap gradient principle, and it is why deep learning with millions of parameters is computationally feasible.

The trade-off:

  • If ddd (inputs) ≫\gg≫ ppp (outputs), use reverse mode. One backward pass computes all ∂fj/∂xi\partial f_j / \partial x_i∂fj​/∂xi​ for a scalar fff, vs ddd forward passes.
  • If p≫dp \gg dp≫d, use forward mode. Each forward pass computes one column of the Jacobian.
  • Deep learning: ddd (parameters) is enormous, p=1p = 1p=1 (scalar loss). Reverse mode wins by a factor of ddd.

Memory cost. The forward pass must store all intermediate activations for the backward pass (or recompute them). For deep nets with large activations, this is the primary memory bottleneck — not the parameters themselves. Gradient checkpointing trades memory for compute by selectively recomputing activations during the backward pass.

Knowledge Check#

Exercise · Multiple choice

In reverse-mode AD on the graph w = u², if the adjoint w̄ = 3, what is ū?

3
6u
3u
6
Question 1 of 3

When is forward-mode AD preferred over reverse mode?

When computing the gradient of a scalar loss with many parameters
When the function has few inputs and many outputs (d ≪ p)
When the function is not differentiable
Never — reverse mode is always better

Browser lab#

Implement forward-mode AD (dual numbers) for sin⁡(x2)\sin(x^2)sin(x2) and compare with analytic derivative. Then implement manual reverse-mode AD on the micro-graph f=(a⋅b+c)2f = (a \cdot b + c)^2f=(a⋅b+c)2.

python · runs in browser
import numpy as np

# --- Forward-mode AD (dual numbers) ---
def forward_sin_x2(x):
    """Compute sin(x^2) and its derivative using dual numbers."""
    # v1 = x^2
    v1 = x**2
    dv1 = 2 * x * 1.0  # dx = 1 (seeded)

    # v2 = sin(v1)
    v2 = np.sin(v1)
    dv2 = np.cos(v1) * dv1

    return v2, dv2

x_vals = np.linspace(0.5, 3, 50)

# Analytic: d/dx sin(x^2) = 2x * cos(x^2)
analytic_vals = 2 * x_vals * np.cos(x_vals**2)

print("Forward-mode AD: sin(x^2) and derivative")
for x in [1.0, 2.0, 3.0]:
    f, df = forward_sin_x2(x)
    an_df = 2 * x * np.cos(x**2)
    print(f"  x={x:.1f}: f={f:.6f}, AD df={df:.6f}, analytic={an_df:.6f}")

# --- Reverse-mode AD on f = (a*b + c)^2 ---
def reverse_micro(a, b, c):
    """Manual reverse-mode on f = (a*b + c)^2."""
    # Forward pass
    v1 = a * b          # v1 = a * b
    v2 = v1 + c         # v2 = v1 + c
    v3 = v2**2          # f = v3

    # Backward pass
    d_v3 = 1.0                           # seed
    d_v2 = d_v3 * 2 * v2                 # d(v3)/d(v2) = 2*v2
    d_v1 = d_v2 * 1.0                    # d(v2)/d(v1) = 1
    d_c  = d_v2 * 1.0                    # d(v2)/d(c) = 1
    d_a  = d_v1 * b                      # d(v1)/d(a) = b
    d_b  = d_v1 * a                      # d(v1)/d(b) = a

    return v3, d_a, d_b, d_c

a, b, c = 1.0, 2.0, 0.5
f, da, db, dc = reverse_micro(a, b, c)
print(f"\nReverse-mode on f = (a*b + c)^2 with (a={a}, b={b}, c={c})")
print(f"  f = {f:.4f}")
print(f"  df/da = {da:.4f}  (analytic: {2*(a*b + c)*b:.4f})")
print(f"  df/db = {db:.4f}  (analytic: {2*(a*b + c)*a:.4f})")
print(f"  df/dc = {dc:.4f}  (analytic: {2*(a*b + c):.4f})")

print("Notice: one reverse pass returns df/da, df/db and df/dc, matching the analytic derivatives to four decimals.")

Key takeaways#

  • Numerical differentiation costs one evaluation per input and loses precision; symbolic differentiation suffers expression swell. AD applies the chain rule to the executed elementary operations, giving exact derivatives at roughly the cost of evaluating the function.
  • Forward mode propagates dual numbers from inputs to outputs and costs ddd passes for a full Jacobian. It is efficient when there are few inputs and many outputs.
  • Reverse mode does a forward pass that records the tape, then a backward pass computing vector-Jacobian products in reverse topological order. For a scalar loss, one backward pass yields the full gradient.
  • The cheap gradient principle: for a scalar function of ddd variables, the gradient costs a small constant times the cost of the function, independent of ddd. That is why deep learning is feasible.
  • The memory cost of reverse mode is the stored activations, not the parameters; gradient checkpointing trades compute for memory.
  • Use reverse mode when inputs outnumber outputs (the deep-learning case) and forward mode when outputs outnumber inputs.

Further reading#

  • Atılım Güneş Baydin, Barak A. Pearlmutter, Alexey Andreyevich Radul & Jeffrey Mark Siskind, "Automatic Differentiation in Machine Learning: a Survey" (JMLR, 2018) — the standard survey, freely available.
  • Andreas Griewank & Andrea Walther, Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation (2nd ed., SIAM, 2008) — the definitive reference.
  • Ian Goodfellow, Yoshua Bengio & Aaron Courville, Deep Learning (MIT Press, 2016) — the backpropagation chapter connects AD to training.

Reading is free. PDF and ePub unlock with the newsletter. Join once and every lesson opens in this browser.

← Previous
Week 10: Large-Batch and Distributed Training
Next →
Week 12: Sharpness and SAM
On this page
  • Why AD exists
  • Forward mode
  • Reverse mode
  • Worked micro-graph
  • Complexity
  • Knowledge Check
  • Browser lab
  • Key takeaways
  • Further reading