Hello! In our last lesson, we dug into the practical mechanics of working with pytrees using jax.tree_util, focusing on the flatten/unflatten paradigm and its elegant abstraction, tree_map. You saw how tree_map is the idiomatic way to apply operations, like an optimizer step, across corresponding leaves of structured data.
Today, we'll connect this understanding of data structures with JAX's core functional transformations. We will explore how grad and vmap are inherently pytree-aware, which is the key to their power and elegance. You'll learn how JAX can compute gradients for entire models represented as pytrees and how it can vectorize operations over these complex structures, for example, to create and evaluate an ensemble of models in one go. This lesson bridges the gap between manipulating data structures and applying the transformations that drive machine learning.
1. Gradients of Pytrees
In PyTorch, automatic differentiation works by tracking operations on Tensor objects that have requires_grad=True and then accumulating gradients in-place via .backward(). JAX takes a different, more functional approach. As the following image illustrates, jax.grad is a transformation that takes a Python function and returns a new function that computes the gradient.

This functional nature is what allows it to integrate seamlessly with pytrees. When you apply jax.grad to a function that accepts a pytree as an argument, JAX understands to compute the gradient with respect to all the differentiable leaves (i.e., floating-point arrays) within that tree. The output is a new pytree, matching the input's structure, containing the gradients.
In pure JAX, you would often use the argnums argument to specify which arguments to differentiate. For example, in a function loss(params, data), jax.grad(loss, argnums=0) would compute gradients with respect to params.
This is demonstrated well in the "Neural Networks in pure JAX" video.
Neural Networks in pure JAX (with automatic differentiation)
This video shows how to build and differentiate a simple MLP using only base JAX functions, providing good context for the conveniences we'll discuss next.
Watch the segment from this timestamp. The presenter defines a loss function and then uses jax.value_and_grad to get both the loss and the gradients. Pay close attention to how they use argnums=(0, 1) to get gradients with respect to both the weights and biases arguments, which are lists of arrays (a simple form of pytree). Notice that the resulting gradient is also a pytree (a tuple of lists of arrays) with the same structure as the inputs.
While powerful, this approach requires you to manually separate your parameters from other data. This is where Equinox, which you're focusing on, provides a more streamlined workflow. An Equinox model is a single pytree that mixes trainable parameters (arrays) with static information (like functions or configuration).
How do you take a gradient of such a mixed pytree without errors? Equinox provides filter_grad. It's a wrapper around jax.grad that automatically finds all floating-point arrays in the input pytree, computes gradients for them, and returns a gradient pytree where the gradients for non-differentiable leaves are None. This is exceptionally convenient.
Transformations - Equinox - Patrick Kidger
The Equinox documentation provides the definitive reference for its filtered transformations.
Read the documentation for equinox.filter_grad and equinox.filter_value_and_grad. Note how it specifies that the gradient is computed with respect to all floating-point arrays in the first argument, and non-differentiable leaves get a None gradient.
Let's see this in action. The video "Neural Networks in Equinox" provides a great walkthrough.
Neural Networks in Equinox (JAX DL framework) with Optax
This video shows the practical application of eqx.filter_value_and_grad in a training context.
Please watch from this clip. The presenter first introduces jax.value_and_grad and then explains why equinox.filter_value_and_grad is often necessary for Equinox models. Critically, observe that the gradient returned is itself an MLP instance, where the weight and bias leaves now contain their corresponding gradient values. This is the pytree concept in its most powerful form.
2. Vectorizing Pytrees with vmap
Just as grad is pytree-aware, so is vmap. When you vmap a function that takes a pytree as an argument, JAX's default behavior is to map over the leading axis of all array leaves while broadcasting all non-array leaves. This simple rule enables powerful patterns.
The most common use is batching. If you have a model and a loss function that works on a single data point, you can make it work on a batch of data by vmapping it.
import jax
import equinox as eqx
# Assume `model` is an eqx.Module and `loss_fn(model, x, y)` works on single examples.
# `x_batch` and `y_batch` have a leading batch dimension.
# vmap the loss function. `in_axes=(None, 0, 0)` means:
# - Don't map over the model (broadcast it)
# - Map over the 0-th axis of x and y
batched_loss_fn = jax.vmap(loss_fn, in_axes=(None, 0, 0))
# Now you can call it on batches
total_loss = batched_loss_fn(model, x_batch, y_batch).mean()
A more advanced and illustrative use of vmap with pytrees is creating and evaluating model ensembles. Imagine you want to train several models with the same architecture but different initializations. You can create them all at once with vmap.
The Equinox documentation has a superb example of this.
Transformations - Equinox - Patrick Kidger
This example crystallizes how vmap interacts with pytrees.
Read the section on equinox.filter_vmap. Focus on the two code blocks in the "Example" section. Ensemble Creation: The make_ensemble function uses filter_vmap on a function that creates a single MLP. The result, mlp_ensemble, is a single pytree where all the array leaves (weights, biases) have an added leading dimension of size 8, while non-array leaves (activation functions) are shared across the ensemble. Ensemble Evaluation: Note how evaluate_ensemble and evaluate_per_ensemble also use filter_vmap to call the ensemble. This is necessary because the ensemble model itself has that extra leading dimension on its parameters. This shows the compositional nature of JAX's transformations.
This pattern of creating an ensemble by vmapping a constructor is incredibly powerful and not easily replicated in other frameworks. It highlights how treating code and data on an equal footing, combined with pytree-aware transformations, leads to expressive and concise solutions.
3. Composition is Key
The real power of JAX emerges when you start composing these transformations. In the batching example above, we derived the standard training recipe:
- Start with a
loss_fn(model, x, y)that works for a single example. - Use
vmapto create abatched_loss_fnthat works on batches of data. - Use
grad(orfilter_grad) on thebatched_loss_fnto get the gradients for the whole batch.
# The canonical training step combines these concepts.
@eqx.filter_jit # JIT-compile the whole step for speed
def make_step(model, x_batch, y_batch):
# 1. Define a loss function for a single example
def loss_fn(m, x, y):
pred_y = m(x)
return jax.numpy.mean((pred_y - y) ** 2)
# 2. Calculate the gradients for the batch
# grad operates on the output of the mean, which is a scalar.
# The mean is taken over the output of vmap.
loss_val, grads = eqx.filter_value_and_grad(loss_fn)(model, x_batch, y_batch)
# We will cover the update step with Optax in a future lesson
# new_model = ... update step using grads ...
return loss_val, grads # In a real step, we'd return the new model
While the code looks slightly different (filter_value_and_grad is applied directly to the single-example loss function), JAX is smart enough to push the vmap inside grad where appropriate. What's happening under the hood is effectively grad(mean(vmap(loss))), which is the most efficient way to compute the gradient of the average loss.
Conclusion
In this lesson, you've seen how JAX's core transformations, grad and vmap, are not just for arrays but are deeply integrated with the pytree abstraction. This synergy is what makes the JAX and Equinox programming model so powerful.
Key Takeaways:
jax.gradandjax.vmapare pytree-aware. They traverse pytree arguments and apply their transformations to the array leaves.- Equinox's
filter_gradandfilter_vmapprovide convenient wrappers that correctly handle pytrees containing mixed types (arrays, functions, static data) by differentiating/vectorizing arrays and broadcasting everything else. - Applying
gradto a function whose first argument is a model-pytree returns a gradient-pytree of the same structure. - Applying
vmapto a model constructor is an elegant way to create model ensembles, where trainable parameters are stacked along a new axis. - The standard pattern for batch training involves composing
gradandvmapover a loss function defined for a single example.
We've now covered what pytrees are, how to manipulate them, and how JAX's main transformations interact with them. But how does a library like Equinox make its classes behave as pytrees in the first place? In our next lesson, you'll learn how to do this yourself by implementing register_pytree_node_class, giving you the power to make any custom Python object a first-class citizen in the JAX ecosystem.