Backends and batching

Two implementations of the same evaluation, NumPy and compiled, and what a batch buys on each. Written 2026-09-02. Numbers marked "measured here" were taken that day on a shared login node, OMP_NUM_THREADS=1, OMP_PROC_BIND=false, best of five, through the consumer route on the released v3 artifacts, with the compiled module aa-fanout/kernel-mode-kokkos/cpp/build-mrsafe (md5 48a74a07, built from commit 4336162d). Everything else says where it was measured.

Two backends, and which one you get

model = NRHJSurAA.load_h5(RELEASE_AA)                       # "auto" (default)
model = NRHJSurAA.load_h5(RELEASE_AA, backend="kokkos")
model = NRHJSurAdA.load(RELEASE_ADA)                       # "auto"

Since 1.1 both classes default to "auto", which takes the compiled backend when it is usable and NumPy when it is not. NRHJSurAA defaulted to "numpy" before that.

"numpy" is the reference path and needs nothing beyond the declared dependencies. "kokkos" routes the time-domain hot path, the tau inversion, the piecewise-Chebyshev synthesis of every mode at every sample and the e^{i m s Phi} rotation, through the compiled nrhjkokkos extension; the coefficient-space work stays in NumPy on both (nrhjsurrogate/execution/batched_evaluator.py module docstring). The adiabatic-angle class defaults to "auto": kokkos if the module imports, NumPy otherwise (nrhjsurrogate/driver/api.py:288-294); measured, the same call resolved to numpy without a module and kokkos with one.

Where the module comes from. $NRHJ_KOKKOS_DIR names one directory and nothing else is consulted; otherwise every cpp/build* beside the package holding an nrhjkokkos*.so is a candidate and the NEWEST wins (nrhjsurrogate/execution/compiled_module_loader.py:34). The first rule is why a shell that has sourced one of the checked-in env_*.sh files hands its compiled build to every process it starts, including one that believed it was testing a clean install. The wheel is py3-none-any and carries no compiled object, so a pip install user has the NumPy backend and only that, until checklist B1 (fetching and compiling Kokkos at install time) lands; the deposit gate proves the six files plus the wheel suffice on the NumPy route and refuses to pass if a compiled module is importable.

Agreement between the backends

Measured here on one waveform at (4, 0.3, -0.2), same 15775-sample grid:

comparison agreement
AA kokkos against NumPy, per mode, of that mode's peak 4.9e-9 to 2.2e-8, worst on (2, 2)
AA hybridised f_low = 0.004, kokkos against NumPy, (2, 2) 2.0e-10
AdA kokkos against NumPy, (2, 2) 1.36e-8; the two grids differ by 3.2e-9 M
batched value evaluator against per-point scalar calls bitwise for sur_e and sur_h, 5.8e-13 for sur_o (kernel-mode port, 2026-08-27)

The AA number is the tau-inversion class (kernel Newton against CubicSpline) and is what tests/test_aa_api.py::test_vs_numpy_reference gates at 2e-5 of peak (ACCURACY.md section 3). On the derivative side, the compiled and NumPy paths agree to 1.4e-14 relative, and a batched against an unbatched contraction to about 1e-10, which is not a defect but the conditioning of the contraction arriving at evaluation time: the worst element cancels by 8.3e6, so neither side is accurate below that.

A parity baseline is therefore backend specific. Measured here at the branch point of this page's own branch: the NumPy-only capture against parity_baseline_v4.npz fails on 143 of 447 arrays, all of them the 80 adiabatic-angle gradient arrays the NumPy route cannot produce, the 62 action-angle gradient arrays that differ at up to 1e-14, and the sentinel that records the skip; the capture with the compiled module above is 447 of 447 bitwise. Compare like with like.

Cost, one waveform and a batch

Measured here, milliseconds per waveform:

NumPy compiled
AA, one waveform 85.8 10.2
AA, batch of 64 (array input), per waveform 97 8.2
AdA, one waveform 46.9 11.5
AdA, modes_grad, one point no path without the module 62.4
AA, modes_grad, one point, full span 437 to 688 across runs (host-parallel batch, below)

One thread on a shared node is a floor, not a throughput figure. The throughput figures, at scale, recorded where they were taken:

Timing on the ms-per-waveform convention throughout; a batched figure is amortised over the batch it names.

Threads

Importing the package sets OMP_PROC_BIND=close and OMP_PLACES=cores if neither is already set, never OMP_NUM_THREADS (nrhjsurrogate/__init__.py:30). The runtime reads the binding once, at its first thread pool, so a value set after the import is silently ignored; nrhjsurrogate.thread_binding() reads back what the runtime actually did rather than echoing what was asked. Measured here with OMP_PROC_BIND=false already in the environment, the import applied nothing and the readback said false, which is the contract. NRHJ_NO_THREAD_DEFAULTS opts out.

The gain is modest and was restated once: 1.08 to 1.11x on the whole call at 32 threads on an exclusive AMD EPYC 7742, with a demonstrated 1.3 percent noise floor; an earlier 1.25 to 1.28x "one stage" figure is WITHDRAWN because that run was not exclusive (INSTALL.md section 5). Two traps: a forked worker pool under the default binding pins EVERY process to core 0, so the checked-in preamble exports OMP_PROC_BIND=false for multi-process work, and srun without -n 1 fans a command out silently.

What each backend refuses or lacks

Batching, the shapes

Where the code is

what where
module discovery, guards nrhjsurrogate/execution/compiled_module_loader.py:34, :136, :188, :273
AA compiled evaluator nrhjsurrogate/execution/batched_evaluator.py
AA physical batch nrhjsurrogate/driver/aa_batch.py
AA batched gradients, device stages nrhjsurrogate/driver/aa_batch_gradients.py, execution/aa_gradient_device.py, execution/mr_gradient_device.py
AdA backend selection nrhjsurrogate/driver/api.py:204-334
thread defaults nrhjsurrogate/__init__.py, execution/_affinity.py
fast path and dispatch table nrhjsurrogate/execution/fastpath.py, execution/dispatch.py
kernels cpp/evaluator.cpp, cpp/gpr.cpp, cpp/mr.cpp, cpp/pack.cpp
user documentation docs/USAGE.md section 9; INSTALL.md sections 2 and 5; examples/usage/09_backends.py