Hello! Welcome back to our deep dive into JAX's automatic differentiation capabilities.
In our last lesson, we learned how to compute full Jacobian and Hessian matrices using jax.jacfwd, jax.jacrev, and jax.hessian. We also established that jax.hessian is typically an efficient composition of forward-over-reverse mode autodiff. However, as we concluded, for the large-scale models you're familiar with from your deep learning work, with millions or billions of parameters, computing and storing a dense Hessian matrix is often computationally infeasible.
This lesson tackles that exact problem. We will explore how to compute Hessian-vector products (HVPs), which are central to many advanced optimization algorithms and model analysis techniques, without ever forming the full Hessian. You will learn to implement this by composing JAX's fundamental grad and jvp primitives, and we'll analyze the performance of different compositional strategies, which will offer some fascinating insights into JAX's compiler.
1. The "Why": Applications of Hessian-Vector Products
Before diving into the "how," let's briefly touch on the "why." Why is the operation , the product of the Hessian matrix with a vector , so important? In many cases, it's all the information we need.
- Second-Order Optimization: Methods like the Newton-Conjugate Gradient algorithm aim to find an update direction by solving the linear system . Iterative solvers for this system, such as the conjugate gradient method, only require the ability to compute products of the Hessian with arbitrary vectors—they never need the full matrix.
- Curvature Analysis: The Hessian's eigenvalues describe the curvature of the loss landscape. Algorithms like the Lanczos method can approximate the dominant eigenvalues and eigenvectors of the Hessian using only HVP computations. This is invaluable for understanding properties like sharpness/flatness of minima, which relates to generalization.
The following blog post provides an excellent overview of these applications.
How to compute Hessian-vector products? | ICLR Blogposts 2024
This ICLR 2024 blog post provides great context on why HVPs are a critical tool in modern machine learning.
Please read the introductory section, "What are HVPs and where are they useful?". Focus on how both inverse Hessian-vector products in optimization and eigenvalue analysis via Lanczos' algorithm are built around the HVP primitive.
The key takeaway is that we can unlock the power of second-order information without paying the prohibitive cost of materializing the full Hessian, if we can compute HVPs efficiently.
2. The "How": HVP as a Directional Derivative of the Gradient
The efficient computation of HVPs is based on a crucial mathematical insight, famously articulated by Barak Pearlmutter in 1994. The Hessian-vector product is equivalent to the directional derivative of the gradient function.
Let's break this down.
- We have a scalar-valued function , like a loss function.
- Its gradient, , is a function that maps from the parameter space to the gradient space, .
- The Hessian, , is the Jacobian of the gradient function, .
- The Hessian-vector product, , is therefore the Jacobian of the gradient function multiplied by the vector : .
This is precisely the definition of a Jacobian-vector product (JVP) that we've already studied! Specifically, the HVP is the JVP of the gradient function .
This identity provides a clear recipe for implementation in JAX:
- Obtain the gradient function using reverse-mode AD (
jax.grad). - Compute the JVP of that gradient function using forward-mode AD (
jax.jvp).
This "forward-over-reverse" approach is the most direct and generally most efficient way to compute an HVP.
3. Implementing HVPs by Composition
Let's translate this insight into code. JAX's compositional nature makes this incredibly straightforward. We will explore the three primary ways to compose differentiation transforms to compute an HVP.
Method 1: Forward-over-Reverse (Recommended)
This method is the direct implementation of the logic we just derived.
import jax
import jax.numpy as jnp
def hvp_forward_over_reverse(f, primals, tangents):
# f: the function R^n -> R
# primals: the point x at which to compute the HVP
# tangents: the vector v to multiply the Hessian by
# The JVP of the gradient function of f.
# jax.jvp(g, (x,), (v,)) returns (g(x), J_g(x)v)
# We want the second element.
return jax.jvp(jax.grad(f), (primals,), (tangents,))[1]
This is elegant and efficient. It involves one reverse-mode pass (for the inner grad) and one forward-mode pass (for the outer jvp). As jvp has a lower memory overhead than a reverse-mode pass, this is a very memory-friendly approach.
The JAX documentation's "Autodiff Cookbook" formalizes this exact method.
The Autodiff Cookbook — JAX documentation
This section of the cookbook shows the canonical implementation of an HVP in JAX.
Please read the section titled Hessian-vector products. Pay close attention to the argument: we define the gradient function g(x) = \partial f(x) and then simply compute its JVP. The provided code is exactly what we wrote above.
Method 2: Reverse-over-Reverse
Another way to think about the HVP is to first define a new scalar-valued function , where is treated as a constant vector. The gradient of this new function, , is precisely the HVP we want.
Since this involves two gradient operations, we can implement it by nesting jax.grad.
def hvp_reverse_over_reverse(f, primals, tangents):
# Define the intermediate scalar-valued function.
# Note: tangents (v) are closed over by the lambda.
h = lambda x: jnp.vdot(jax.grad(f)(x), tangents)
# The gradient of this function is the HVP.
return jax.grad(h)(primals)
This approach performs two full reverse-mode passes. As reverse-mode AD requires storing intermediate values from a forward pass for the backward pass, nesting them (grad of grad) can lead to significantly higher memory consumption compared to the forward-over-reverse method.
Method 3: Reverse-over-Forward
Finally, we could flip the first approach. We can define a function that computes the JVP of the original function , which gives the directional derivative . We can then take the gradient of that result with respect to the original parameters .
def hvp_reverse_over_forward(f, primals, tangents):
# Define a function that computes the directional derivative.
jvp_fn = lambda x: jax.jvp(f, (x,), (tangents,))[1]
# The gradient of the directional derivative is the HVP.
return jax.grad(jvp_fn)(primals)
This also involves one forward and one reverse pass, and its performance is generally similar to the forward-over-reverse method. However, the ICLR blog post you read earlier notes a practical advantage of forward-over-reverse: it naturally computes the gradient as an intermediate value, which can be useful since many optimization algorithms need both the gradient and the HVP.
The following video provides a quick summary of how these higher-order derivatives can be formed by composition.
What is Automatic Differentiation?
This video by Ari Seff provides a concise explanation of automatic differentiation. The final section touches on exactly the topic of this lesson.
Watch the short segment on higher-order derivatives from 13:07 to 13:46. It specifically highlights using a reverse-on-forward approach to compute Hessian-vector products efficiently without forming the full Hessian.
4. Performance in Practice: The Power of JIT Compilation
We have three methods, and a theoretical reason to prefer forward-over-reverse for memory efficiency. Let's see what happens in practice when running on real deep learning models. The ICLR blog post provides excellent benchmarks.
How to compute Hessian-vector products? | ICLR Blogposts 2024
This article not only explains the different HVP methods but also benchmarks them on common deep learning models.
Please read the sections HVPs without explicit Hessian computation and Benchmark with deep learning architectures. These sections provide the JAX code for all three methods and then compare their wall-clock time and memory usage.
Let's analyze the results for JAX from that article.

Here are the key observations, which speak volumes about JAX's design:
- Time Complexity: With JIT compilation, all three methods are very close in performance. The total time for an HVP is roughly 2-3 times that of a single gradient computation, which is consistent with the theory (e.g., one forward pass + one reverse pass).
- Memory Complexity (with JIT): The most surprising result is on the left plot. The memory usage for all three methods is nearly identical! This seems to contradict our theory that reverse-over-reverse should be more memory-hungry. The article correctly points out that this is due to XLA's powerful fusion and memory optimization capabilities. The JIT compiler is smart enough to analyze the entire computation graph and optimize memory allocation, largely erasing the theoretical differences between the methods when compiled.
- Memory Complexity (without JIT): The authors perform a crucial experiment: disabling JIT. In that case, the results match theory perfectly—reverse-over-reverse consumes significantly more memory. This is a fantastic illustration of the value added by the compiler.
Your interest in JAX's internals and compilation makes this a particularly important point. The high-level functional transformations define the what (compute an HVP), but the JIT compiler (XLA) has significant freedom in determining the how (the specific, fused execution kernel), often leading to performance characteristics that are better than what a naive analysis of the uncompiled code would suggest.
Conclusion
In this lesson, we demystified the Hessian-vector product, a cornerstone of many advanced machine learning techniques. You now have the tools to compute second-order derivatives efficiently at any scale.
Key Takeaways:
- Hessian-vector products (HVPs) allow us to leverage second-order derivative information without the prohibitive cost of materializing the full Hessian matrix.
- The most efficient HVP implementations are based on the identity , which frames the HVP as the JVP of the gradient function.
- In JAX, this is implemented by composing
jvpandgrad. The forward-over-reverse method,jax.jvp(jax.grad(f), ...), is generally recommended as it is conceptually direct and memory-efficient. - While other compositions like reverse-over-reverse exist, they are theoretically less memory-efficient. However, JAX's JIT compiler (XLA) can often optimize these differences away, showcasing the power of compilation in high-performance computing.
In our next lesson, we will venture further into the customization of JAX's differentiation engine. You will learn how to define custom differentiation rules for your functions using jax.custom_vjp, giving you complete control over the backward pass. This is an advanced technique essential for implementing novel algorithms or for manually optimizing gradient flow in complex models.