Skip to content

Persistent JIT compilation cache

Jaxonomy compiles each diagram + solver combination into XLA on first use. Single-shot ODE simulations compile in well under a second (roughly 60–250 ms, depending on whether the model has zero-crossings or an acausal DAE); simulate_batch ensembles take longer. JAX provides a persistent on-disk cache that recovers most of this cost across processes — Jaxonomy ships a one-call helper.

import jaxonomy
jaxonomy.enable_persistent_jit_cache()              # ~/.cache/jaxonomy/jit/
# or
jaxonomy.enable_persistent_jit_cache("/scratch/jit")  # custom dir

The first run after enabling pays the normal compile cost and writes the artefact to disk. Subsequent processes (with matching JAX version, JAXPR, and target device) read from disk in tens of milliseconds rather than re-running XLA lowering.

What is cached

The cache keys on the JAXPR, JAX version, and target device. Bumping JAX, switching CPU↔GPU, or changing a static parameter that flips a control-flow branch produces a fresh entry. Stale entries persist; periodically delete the cache directory on big version bumps to reclaim disk space.

Tunables

enable_persistent_jit_cache configures three JAX options:

option value rationale
jax_compilation_cache_dir the cache directory required
jax_persistent_cache_min_compile_time_secs 1.0 trivial computations recompile faster than disk read
jax_persistent_cache_min_entry_size_bytes -1 size threshold disabled — gating on time alone is enough

For different thresholds, call jax.config.update(...) directly afterwards.

When to enable

Always for interactive work and long-running CI jobs; skip for short-lived single-shot scripts where the cache write itself dominates. See benchmarks/compile_time.py for per-case timings.

Repeated gradients: hoist the jit

Every bare call to jaxonomy.simulate builds a fresh traced closure, so JAX's in-process jit cache misses on function identity and you pay a full re-trace + XLA compile per call — the numeric solve itself is milliseconds. This dominates design loops that differentiate through the simulator repeatedly:

# SLOW — each value_and_grad call re-traces + recompiles the whole
# forward + adjoint (seconds per call; ~30 s on a 24-state acausal pack):
def objective(theta):
    ctx = base_context.with_parameter("g0", theta)
    res = jaxonomy.simulate(model, ctx, (0.0, tf), options=opts)
    return res.context.continuous_state[1]

for step in range(5):
    J, dJ = jax.value_and_grad(objective)(theta)   # re-traces every time
    ...

The fix is to define the objective once as a pure function of (theta, context) and wrap the outer value_and_grad in jax.jit, so tracing happens exactly once:

# FAST — one compile, then ~milliseconds per call:
@jax.jit
def value_and_grad_fn(theta, context):
    def objective(theta):
        ctx = context.with_parameter("g0", theta)
        res = jaxonomy.simulate(model, ctx, (0.0, tf), options=opts)
        return res.context.continuous_state[1]
    return jax.value_and_grad(objective)(theta)

for step in range(5):
    J, dJ = value_and_grad_fn(theta, base_context)  # cached after call 1
    ...

This works for the implicit BDF/DAE path too (with SimulatorOptions(enable_autodiff=True)): the simulation context and BDF solver state are ordinary pytrees and trace cleanly. Measured on the index-2 pendulum DAE (9 states, BDF, 2 s horizon, CPU): unjitted ~1.8 s per call; jitted 1.8 s once, then ~10 ms per call (~180×). Cost envelope: per-call cost of the naive pattern is almost entirely trace+compile and scales with model size (states, blocks, solver machinery), not with the horizon; the compiled kernel's runtime scales with horizon and stiffness. Combine with the persistent cache above to also amortise the one-time compile across processes.

Requirements for the pattern: pass the context (and any other non-differentiated inputs) as arguments of the jitted function rather than closing over mutable state, keep t_span and options static, and don't rebuild the diagram inside the traced function (acausal compilation is not jit-safe — build once, outside).