Skip to content

Optional compiled Diffrax terms

op_engine.diffrax.compiled_diffrax_term binds a current compiler RHS to a caller-owned Diffrax ODETerm. The optional adapter keeps native JAX arrays and runtime parameters. Diffrax owns numerical integration, output storage, the controller, adjoint and result handling. Python, NumPy and the core solver do not acquire a Diffrax or compiler runtime dependency.

Install the diffrax extra from the checkout containing this adapter. The qualified producer is the merged op_system compiler contract at 38cc97721922597310cf6e2049f206ade224e0be; the required CI installs that source explicitly. Consumer plugins written against older compiler releases are design references and need fresh qualification against the current fields and outputs.

python -m pip install '.[diffrax]'
python -m pip install 'op-system @ git+https://github.com/ACCIDDA/op_system.git@38cc97721922597310cf6e2049f206ade224e0be'

Native input and output contract

Surface Qualified boundary
PyTree state, the default Plain dict of native JAX arrays, keyed and shaped exactly as compiled.template_shapes; scalar templates use zero-dimensional leaves. Requires pytree_eval_fn.
Flat state Select state_layout="flat"; shape (len(compiled.state_names),) in that expanded compiler ordering, using eval_fn.
Parameters A plain dict passed as Diffrax args. Include scalar param_names and shaped parameters declared in metadata. Shaped leaves use declared axis sizes, including repeated axes.
Precision Homogeneous float32 or float64 state and parameter leaves. Shapes, keys, namespace and output dtype are checked without host transfer or coercion. Choose compatible native time precision in the solve.
RHS output The same state structure, shapes and dtype; malformed producer output raises.
Unsupported contributions Declared operators, history, time-varying parameter tables and forcing breakpoints raise NotImplementedError before term construction. The caller declares any further smoothness assumptions.
Devices Required CPU evidence below. GPU execution and additional device qualification remain on maintainer hold.

The adapter snapshots static layout metadata and the selected compiler evaluator. Build the term once and pass changed numeric values through args and y0. Changed values with the same native shapes/dtypes keep the term identity; changed models get distinct terms. Parameter values are not captured in static closures. Module import leaves JAX precision, compilation-cache settings and backend initialization to the caller. There is no module-global solve cache or override dictionary. Term ownership and output retention remain explicit. JAX/Diffrax compiler caches have their own lifetimes; dropping a term does not guarantee process-RSS release.

Final-only solve and a parameter derivative

This example uses the compiler's scalar PyTree path and retains one requested output. It preserves initial-state and parameter differentiation. Save policies control returned arrays; checkpoints, compilation and stage work add separate peak-memory costs.

import diffrax as d
import jax
import jax.numpy as jnp
import numpy as np
from op_system import compile_spec
from op_engine.diffrax import compiled_diffrax_term

compiled = compile_spec({
    "kind": "expr", "state": ["x"], "equations": {"x": "-rate*x"}
})
term = compiled_diffrax_term(compiled)
dtype = jnp.float32
t0, t1, dt0 = (jnp.asarray(value, dtype=dtype) for value in (0.0, 1.0, 0.02))

def final_x(rate):
    solution = d.diffeqsolve(
        term, d.Dopri5(scan_kind="bounded"), t0=t0, t1=t1, dt0=dt0,
        y0={"x": jnp.asarray(2.0, dtype=dtype)}, args={"rate": rate},
        stepsize_controller=d.PIDController(rtol=1e-5, atol=1e-7),
        saveat=d.SaveAt(t1=True), adjoint=d.RecursiveCheckpointAdjoint(),
        max_steps=256, throw=True,
    )
    return solution.ys["x"][-1]

rate = jnp.asarray(0.7, dtype=dtype)
np.testing.assert_allclose(final_x(rate), 2*np.exp(-0.7), rtol=3e-5)
np.testing.assert_allclose(jax.grad(final_x)(rate), -2*np.exp(-0.7), rtol=3e-5)

For requested output times use explicit SaveAt(ts=...). For measured output reductions use an explicitly qualified Diffrax save callback. The factory does not expand a reporting cadence, stack an unrequested state history or convert solver failures into a statistical score. throw=False preserves Diffrax's solution.result; the caller must inspect it before using the values. The failure test exercises an actual step-limit failure with both throw policies.

Adjoint and mesh choices

Diffrax's adjoint contracts require separate transform choices. RecursiveCheckpointAdjoint supports reverse-mode through the numerical solve with checkpointing. Select ForwardMode for JVP or jacfwd. The bounded DirectAdjoint configuration has separate fixed-mesh float64 PyTree Hessian evidence. Higher derivatives of live adaptive methods, third derivatives and every solver/adjoint combination are outside that claim.

Use StepTo and an identical accepted mesh for a numerical-method comparison. A live PID solve is a different execution task; its accepted mesh and controller cost must be reported separately. A recursive checkpoint adjoint differentiates the numerical solution; it is not a continuous backsolve adjoint. Controller choices, tolerances and step limits remain explicit Diffrax solve arguments. The factory does not accept or silently translate an OP Engine RunConfig.

Required CPU evidence

The two dedicated hosted jobs require current JAX 0.12.0 / NumPy 2.5.4 / array-api-compat 1.15.0 and minimum JAX 0.4.38 / NumPy 2.0.2 / array-api-compat 1.12.0, both with Diffrax 0.7.2 and the same current compiler. Run python scripts/qualify_backend.py jax --diffrax; 31 selected cases must execute with zero skips. Baseline optional skips do not qualify this contract. The base suite separately tests a fresh import with JAX, Diffrax and the compiler blocked, including an actionable missing-extra error.

Closed-form reversible-transfer values and derivatives cover flat/PyTree, float32/64, fixed and live adaptive values/reverse gradients, changed runtime parameters and parameter-dependent initial states. Forward derivatives and independent batches use fixed meshes. A fixed float64 PyTree Hessian has an independent analytic reference. Different models with equal names/shapes remain isolated; invalid shapes, keys, host arrays and dtypes fail explicitly. Current compiler aliases, shaped rates and global reductions also match independently written nonlinear cell balances. No application training/calibration or device speedup follows from these synthetic contracts.

Representative measurements

The complete receipt contains 88 serial fresh CPU workers: two shapes (1×1, 4×2), coupled/independent and stiff/nonstiff regimes, native replay and handwritten/compiled Diffrax controls. Eighty cases cover trajectories and reverse derivatives. Eight additional small fixed nonstiff cases cover forward derivatives and Hessians. All passed independent SciPy/refinement or derivative finite-difference checks; maximum trajectory absolute error was 2.06e-9. Every worker used source commit 9bf3be12b03f4628fba645d668be9ca3c344fe0a with a clean tree. The receipt verifies that its compiler checkout and the qualified public merge have identical Git trees.

The host was an Intel Core Ultra 7 265U, Python 3.12.14, with the current versions listed above, one pinned logical CPU, single-threaded OMP/OpenBLAS/MKL and persistent compilation caching disabled. Each condition has one fresh process and five warm samples. These are descriptive engineering measurements, without a statistical speedup, default-selection or application-adoption claim. Earlier smoke rows remain separate. The summary also records all dependency versions, accuracy limits and selected costs.

The coupled 4×2 nonstiff case has 16 states and 18 dynamic rate/initial-value parameters. Fixed methods share the same 19 accepted steps; live PID uses 17. Trace+compile excludes term/controller setup (0.30–0.34 s here). Cold first execution, all warm samples and native/handwritten controls remain in the receipt.

Execution Task Steps Trace+compile (s) Median warm (ms) Worker peak RSS (MiB)
Fixed mesh Trajectory 19 0.686 0.039 308.2
Fixed mesh Reverse gradient 19 2.360 0.079 351.7
Live PID Trajectory 17 0.708 0.040 306.5
Live PID Reverse gradient 17 2.623 0.090 352.8

Across all compiled cases worker peak RSS ranged from 302 to 611 MiB. These peaks include imports, independent verification and auxiliary compilations; they are not per-solve allocation or training-memory measurements. XLA argument, output and temporary-buffer estimates are retained separately. Output shape, adjoint choice, stiffness and compilation all affect memory.

To repeat, extract reproduction_files from the gzip JSON, install the recorded CPU versions and compiler source into an isolated environment, and provide the checkout containing this implementation:

/path/to/cpu-env/bin/python reproduce.py \
    --engine-checkout /path/to/op_engine --cpu 0 --output /path/to/new-results

Choose an available CPU. The coordinator reuses the existing isolated benchmark worker and case protocol, writes each success/failure/timeout to its journal and fails unless all 88 required conditions complete successfully.