Skip to content

Higher-Order Gradients & Jacobians

Overview

First-order training (loss.backward()) is the 99% case. But meta-learning, adversarial robustness, scientific ML (PDE solvers), and deep equilibrium models all need Jacobians, Hessians, and gradient-of-gradient. PyTorch gives you two tools: autograd with create_graph=True, and the torch.func (functorch) functional API.

  • torch.autograd.grad(loss, params, create_graph=True)— first forward, second derivative via chains.
  • torch.func.jacrev/jacfwd/jvp/vjp/hessian— explicit Jacobians for any callable.
  • Hessian-vector products (HVP) are cheap (jvp(vjp(...)))— never build the full Hessian.
  • Double backward requires the graph to be kept: retain_graph=True or recompute.

Rule: for anything requiring a full Jacobian, use torch.func/vmap; for HVP/GN updates, compose jvp and vjp symbolically.


autograd.grad with create_graph

import torch

x = torch.tensor([2.0, 3.0], requires_grad=True)
loss = (x ** 3).sum()

g1 = torch.autograd.grad(loss, x, create_graph=True)[0] # 3x^2
g2 = torch.autograd.grad(g1.sum(), x)[0] # 6x
print("first:", g1.tolist(), " second:", g2.tolist())

retain_graph vs create_graph

  • retain_graph=True → reuse the same graph for another backward (memory heavy).
  • create_graph=True → build another graph that will itself be backwarded later.
  • Prefer autograd.grad for selective params; backward(create_graph=True) for everything.

Vector-Jacobian Products (VJP)— the natural primitive

vjp(f, inputs, v) computes v^T J— exactly what autograd does, exposed directly.

from torch.func import vjp, jvp

def f(x):
 return x.sin().sum()

x = torch.tensor([1.0, 2.0], requires_grad=True)
v = torch.tensor([1.0, 1.0])
out, vjp_fn = vjp(f, x)
grad = vjp_fn(v)[0]
print("vjp (grad):", grad) # sum of cos

-

Full Jacobians— jacrev / jacfwd

from torch.func import jacrev, jacfwd, vmap

def vec_fun(x):
 return torch.stack([x[0] ** 2, x[0] * x[1]])

x = torch.tensor([2.0, 3.0])
J = jacrev(vec_fun)(x) # 2x2
print(J)
# [[4., 0.], [3., 2.]]
API Cost Use when
jacrev output-dim backwards output small, input large
jacfwd input-dim forwards input small, output large
jacrev(jacrev(...)) Hessian dims tiny

-

Hessian-Vector Products Without the Hessian

from torch.func import jvp, vjp

def hessian_vector_product(f, x, v):
 _, vjp_fn = vjp(f, x)
 _, jvp_fn = jvp(lambda z: vjp_fn(z)[0], (x,), (v,))
 return jvp_fn(x)[0] # careful: use correct composition

def g(z):
 return (z ** 3).sum()

x = torch.tensor([2.0])
Hv = hessian_vector_product(g, x, torch.tensor([1.0]))
print("Hv (should be 6x*1 = 12):", Hv)

Cost of HVP ≈ 2 forwards— this is why second-order optimizers (Newton/L-BFGS/K-FAC-ish) are feasible at scale.


vmap— Batch Without a For-Loop

from torch.func import vmap

def single(x):
 return (x @ torch.ones(3)).sum()

xs = torch.randn(10, 3)
outs = vmap(single)(xs) # implicit batch dim
print(outs.shape)

Use case: per-sample gradients, per-sample losses, Jacobians-of-batches.

params = [torch.randn(3, 3, requires_grad=True)]
def loss_fn(p, x):
 return (x @ p).square().mean()

# per-sample grads in one call:
batch_grads = vmap(lambda x_: torch.autograd.grad(loss_fn(params[0], x_), params[0])[0])(xs)
print("per-sample grads shape:", batch_grads.shape) # (10, 3, 3)

Practical Guards

import torch

def safe_second_order(f, x, v=None):
 """Stable Hessian-vector product with NaNs guarded."""
 g1 = torch.autograd.grad(f(x).sum(), x, create_graph=True)[0]
 if v is None:
 v = torch.ones_like(x)
 g2 = torch.autograd.grad((g1 * v).sum(), x)[0]
 assert torch.isfinite(g2).all()
 return g2

Decision Table

Need Tool
Gradient of one scalar wrt params .backward() / autograd.grad
Second derivative for meta-loss create_graph=True double-backward
Full Jacobian (small dims) jacrev/jacfwd
Batch of small problems vmap
HVP for implicit/Hessian-based optim jvp ∘ vjp
Per-sample grads (fisher, robust) vmap over autograd.grad

-

Key Takeaways

  • create_graph=True extends the graph for a second backward.
  • jvp/vjp are the atoms; Jacobians and Hessians are compositions of them.
  • HVP ≈ 2 forwards; never materialize a full Hessian.
  • vmap replaces batch loops and unlocks per-sample methods.
  • Verify with torch.autograd.gradcheck on custom fns before trusting results.

-