Pytrees: nested containers of arrays

JAX · Practical · 12 min

Model parameters, optimizer state and batches are all pytrees; manipulating them is daily JAX work.

A pytree is any nested structure of Python containers (lists, tuples, dicts, None, and registered classes) whose leaves are arrays or scalars. JAX transformations accept and return pytrees: grad of a loss over a dict of parameters returns a dict of gradients with the same keys.

The jax.tree module works on whole trees:

  • jax.tree.map(f, tree) applies f to every leaf; with several trees of the same structure, f gets matching leaves, e.g. an SGD update jax.tree.map(lambda p, g: p - lr * g, params, grads).
  • jax.tree.leaves(tree) gives a flat list of leaves, useful for counting parameters.
  • jax.tree.structure(tree) gives the "treedef" describing the shape; jax.tree.flatten/unflatten convert back and forth.

Your own classes become pytrees once registered. The simplest way is a dataclass with @jax.tree_util.register_dataclass, marking non-array configuration as static metadata so it is not traced. Libraries like Flax and Equinox do this for you.

Things to know:

  • None is an empty subtree, not a leaf, so jax.tree.map skips it.
  • Tree structure is part of a jitted function's cache key: changing a dict's keys triggers recompilation.
  • Trees passed to a multi-tree map must have identical structure, or you get an error naming the mismatch.

Example

import jax
import jax.numpy as jnp
from dataclasses import dataclass, field

params = {"w": jnp.ones((3, 2)), "b": jnp.zeros(2)}

def loss(p, x):
    return jnp.sum(jnp.tanh(x @ p["w"] + p["b"]))

grads = jax.grad(loss)(params, jnp.ones((4, 3)))
print(jax.tree.map(jnp.shape, grads))   # {'b': (2,), 'w': (3, 2)}

new = jax.tree.map(lambda p, g: p - 0.1 * g, params, grads)
print(sum(x.size for x in jax.tree.leaves(new)))   # 8 parameters

@jax.tree_util.register_dataclass
@dataclass
class Linear:
    w: jax.Array
    b: jax.Array
    name: str = field(default="dense", metadata=dict(static=True))

layer = Linear(jnp.ones(2), jnp.zeros(2))
print(jax.tree.leaves(layer))   # [Array([1., 1.], ...), Array([0., 0.], ...)]

Remember

  • Transformations take and return arbitrarily nested pytrees of arrays
  • jax.tree.map over several same-shaped trees implements updates leaf by leaf
  • Register custom classes (e.g. register_dataclass) and mark config as static

Watch out for

  • Storing Python config strings or ints as leaves, so they get traced and cause errors or recompiles.

Check yourself

  1. What does jax.tree.leaves({'a': 1, 'b': (2, None), 'c': [3]}) return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.jax.dev/en/latest/101/pytrees.html