eqxbatch
Batched evaluation of Equinox modules: a
vmap that lives in the PyTree.
pip install eqxbatch
Why
Equinox's position is that a Module is a PyTree, so you batch by transforming
functions — eqx.filter_vmap at the call site — rather than by wrapping modules. That is
the right answer whenever the module is the outermost object you call, and you should keep
doing it.
It stops being available when the batched module has to remain a node inside a larger tree that is driven through one generic entry point:
Sum(Batched(inner_a), inner_b) # no call site to hang a filter_vmap on
Here the vmap has to live in the tree itself. Batched is that node.
Usage
import equinox as eqx
import jax
from eqxbatch import Batched, broadcast, stack
keys = jax.random.split(jax.random.key(0), 8)
mlps = [eqx.nn.MLP(2, 2, 8, 2, key=k) for k in keys]
ensemble = Batched(stack(*mlps)) # list[Module] -> one batched Module
x = jax.random.normal(jax.random.key(1), (2,))
ensemble(x).shape # (8, 2) -- one shared input, eight models
ensemble[3] # mlps[3], as an ordinary module again
len(ensemble) # 8
One input per batch element instead of a shared one:
per_member = Batched(stack(*mlps), arg_axes=eqx.if_array(0))
xs = jax.random.normal(jax.random.key(1), (8, 2))
per_member(xs).shape # (8, 2)
Same starting parameters, diverging during training:
ensemble = Batched(broadcast(eqx.nn.MLP(2, 2, 8, 2, key=keys[0]), 8))
Or build it the idiomatic Equinox way and wrap the result:
ensemble = Batched(eqx.filter_vmap(lambda k: eqx.nn.MLP(2, 2, 8, 2, key=k))(keys))
Attribute forwarding
Any public attribute of the wrapped module is re-evaluated under the same vmap, so a module only has to implement a method once, unbatched, for it to work through any number of batch layers:
ensemble.some_method(y) # (8, ...) -- called per batch element
ensemble.some_property # (8, ...) -- evaluated per batch element
Arguments are shared across the batch by default; pass arg_axes to override:
ensemble.some_method(ys, arg_axes=(eqx.if_array(0),))
The batch axis goes first in every output by default; out_axes moves it, either for
every call or for one:
Batched(stack(*mlps), out_axes=-1)(x).shape # (2, 8) -- batch axis last
ensemble.some_method(y, out_axes=-1)
For anything that spans several attributes at once, drop to the primitive:
ensemble.map(lambda m: (m.weight @ m.bias, m.activation_count))
Unlike a raw jax.vmap, map tolerates non-array leaves in the output: they are
partitioned out and returned unbatched.
Nesting and composition
Batched is an eqx.Module, so it nests and is transparent to jit, grad and outer
vmaps:
Batched(Batched(broadcast(broadcast(model, 2), 3)))(x).shape # (3, 2, ...)
eqx.filter_grad(loss)(ensemble, x) # gradients keep the batch axis
API
Batched(inner, *, in_axes, arg_axes, kwarg_axes, out_axes, axis_size) |
the batched node |
Batched.map(fn, *args, arg_axes=..., kwarg_axes=..., out_axes=..., **kwargs) |
run fn(inner, *args) per batch element |
Batched[i], len(Batched) |
unstack one element, batch size |
stack(*modules) |
list[Module] -> Module with a leading axis |
broadcast(module, size) |
one module repeated size times |
in_axes, arg_axes, kwarg_axes and out_axes take eqx.filter_vmap specs: an int, None, a callable such as
eqx.if_array(0), or a pytree prefix for per-leaf control.
Caveats
eqx.filter_vmapdoes not accept keyword arguments in the vmapped function.Batchedcloses over any**kwargsyou pass, so they are shared across the batch and cannot be given an axis spec.- Under
jitthe wrapper is free — it compiles to exactly the program a hand-writtenvmapwould produce. In eager mode it costs a fixed overhead per call plus one copy of the output, sincefilter_vmapalways applies amoveaxisthat XLA elides but the op-by-op path does not. __getitem__and__len__address the outermost batch axis only.
License
MIT
Metadata
Release files for eqxbatch 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| eqxbatch-0.3.0.tar.gz | 7.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| eqxbatch-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 14.9 kB
Release files / eqxbatch-0.3.0.tar.gz
| Download URL | eqxbatch-0.3.0.tar.gz |
|---|---|
| Size | 7.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a10bab9b6e89787c04226b02bdee94fb7f39fc717c5737b53f8676c23441773
|
|
BLAKE2b-256 checksum How to use checksums |
0bcac446fee7740c525eac0fc9054ac72593025ae98cefb59596b4bee8297b7f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logRelease files / eqxbatch-0.3.0-py3-none-any.whl
| Download URL | eqxbatch-0.3.0-py3-none-any.whl |
|---|---|
| Size | 7.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
07d22f3b5a60a99be76d0e23fcf81fc0f6fce719769488dfe6e48e6937832f45
|
|
BLAKE2b-256 checksum How to use checksums |
068262c73d3609ba2e5b31860e3a0dc0949dfef04fd8bcad25dda8e8625b4f50
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency log