Installing nrhjsurrogate

How to set up and evaluate the AA surrogate (nrhjsurrogate.NRHJSurAA) on a fresh machine. Three tiers, in increasing effort:

  1. Python-only (numpy backend) — works anywhere, no C++.
  2. CPU-OpenMP kokkos backend — the compiled evaluator, ~2.4x faster single-call, ~4-5x at batch. Already inside the published wheels (section 1a); built automatically by pip install of the source distribution otherwise, Kokkos included (section 2a).
  3. CUDA kokkos backend — for NVIDIA GPUs; includes the honest caveats for GeForce cards (GTX 1650 Ti / Turing sm_75 specifically).

Plain pip install nrhjsurrogate gets you tier 2 on every platform we publish a wheel for, with no flags and no compiler (section 1a). Elsewhere it compiles, and NRHJ_BACKEND (section 1b) chooses what: tier 2 where the compiler allows it, tier 1 where it does not.

The CPU tiers are the well-trodden ones. The CUDA tier builds and links, but no waveform has yet been evaluated on the device and its float32 route is untested, so treat it as the least exercised path here.

The acceptance test for any tier is one command (section 6):

nrhjsur-smoke

To check the install AND the downloaded model files against shipped reference values (load, waveform, derivatives, error estimate, hybridisation, dynamics, remnant, file integrity), run the verification suite; it ships inside the package:

nrhjsur-verify            # or: python -m nrhjsurrogate.verify

0a. Installing from PyPI — and where the models come from

There is no public git repository for this release. The package goes to PyPI; the model artifacts are distributed separately as an archive deposit. git lfs pull is therefore a developer step, not a user one.

pip install nrhjsurrogate

spectools (gitlab.com/vaishakp/spectools, MIT) is on PyPI and is a declared dependency, so pip install nrhjsurrogate pulls it and its own dependencies automatically.

The pin is spectools>=2026.8.8. Do not lower it: earlier releases were missing a module this package imports at import time, so import nrhjsurrogate fails against them.

Then get the models. Either point at a directory you filled yourself:

export NRHJ_MODEL_DIR=/path/to/models

or let the package fetch and verify them by name:

import nrhjsurrogate
nrhjsurrogate.fetch_model("NRHJSur3dq8_AA_v3")     # sha256-checked before use

The two released models are NRHJSur3dq8_AA_v3 (action-angle) and NRHJSur3dq8_AdA_v3 (adiabatic-angle, 14-element), together with the shared merger-ringdown arm NRHJSur3dq8_MR_v3 that both of them name. The version digit is the model generation, not a patch level; the previous generation (_v2) is deposited alongside and still loads by name, so published v2 numbers stay reproducible. Both models are 3dq8 — aligned-spin (q, chi1z, chi2z) with q ≤ 8, matching the NRHybSur3dq8 convention — and both are built on the N2 NR extrapolation, which the names do not encode: each HDF5 records it as the root attribute extrapolation = "N2".

Loaders take a name or a path:

from nrhjsurrogate import NRHJSurAA
model = NRHJSurAA.load_h5("NRHJSur3dq8_AA_v3")
out = model(2.0, 0.3, -0.1)

Resolution order: explicit path → $NRHJ_MODEL_DIR → $LAL_DATA_PATH → per-user cache ($NRHJ_MODEL_CACHE_DIR, else platformdirs if installed, else $XDG_CACHE_HOME/nrhjsurrogate, else ~/.cache/nrhjsurrogate) → a source checkout's model_release/ → a download from the deposit into that cache, checked against the registry's SHA-256 before the file is moved into place. Failing all of it, a FileNotFoundError naming the exact file, its SHA-256, every directory searched, and how to obtain it. NRHJ_AUTO_FETCH=0 turns the download off. Nothing degrades silently — the same rule as the merger-ringdown arm.

Verify the install:

nrhjsur-smoke

0. What the package needs — and what it does NOT

Run-path dependencies (traced, then verified by running in a clean env)

dependency why verified version
Python ≥ 3.10 pyproject.toml 3.12.13; tier-1 suite also run on 3.10.22 and 3.11.17
numpy everything 2.3.5
scipy splines, quadrature, windows 1.16.3
h5py reading the SWSpH merger-ringdown HDF5 3.16.0
spectools (spectools.chebyshev only) Chebyshev basis/diagnostics used by surrogate_prediction.py and spectral.py 2026.8.8 (PyPI)

spectools is pure numpy on this path (basis.py, chebyshev.py, diagnostics.py). It is on PyPI and declared as a dependency with a floor of 2026.8.8 — the first published version whose wheel ships chebyshev/diagnostics.py (older wheels were missing it and broke import nrhjsurrogate). A plain pip install resolves it; no checkout is needed. See section 0a.

Explicitly NOT needed to evaluate waveforms

Verified by running the full smoke test under env -i with a PYTHONPATH containing only this repo and spectools:

Artifact manifest

Released (distributed separately from the package — see section 0a):

model file size (bytes) sha256 extrapolation
NRHJSur3dq8_AA_v3 NRHJSur3dq8_AA_v3.h5 31,952,616 b6d2d300035f5c9f7dac005980e8f01536ee59c08f8efaf57643f46811ebf483 N2
NRHJSur3dq8_AdA_v3 NRHJSur3dq8_AdA_v3.h5 15,734,472 c0ca99222f8972e3d13f8273782e37fe84292ef9741e06893d2a7f18ff549a1a N2
NRHJSur3dq8_MR_v3 NRHJSur3dq8_MR_v3.h5 19,606,296 dd1bf00860d01e16cf740bd227f5f7fb163284cb42309febd7194bc8d235059a N2

The previous generation is deposited too, so that published v2 numbers stay reproducible. It is not the default; name it explicitly to get it.

model file size (bytes) sha256 extrapolation
NRHJSur3dq8_AA_v2 NRHJSur3dq8_AA_v2.h5 30,743,544 a05f6e0288e9896d2becff83b7eac0121c39285a4905c3d6f62741be97bd06b4 N2
NRHJSur3dq8_AdA_v2 NRHJSur3dq8_AdA_v2.h5 15,830,464 20dad0f5de344d347b605540fb0320570c6672411bffa23d093adee858bc61ce N2
NRHJSur3dq8_MR_v2 NRHJSur3dq8_MR_v2.h5 19,690,832 da0ea1b031a5ff4ab1805566d2c3d2047948dee778929eb446fa8ce59f67546b N2

Both inspiral models are de-embedded: they reference the shared merger-ringdown arm of their own generation — NRHJSur3dq8_MR_v3.h5 for the v3 pair — by name rather than each carrying a copy, so all three files of a generation are required. Do not mix generations: the arm is checked by content digest at load, and a crossed pair is refused rather than served.

The same table lives in code as nrhjsurrogate.models.MODELS, which is what fetch_model and nrhjsur-verify check a file against.

NRHJSur3dq8_AA_v3.h5 is the trained AA surrogate (dynamics + radiative GPRs, the phase grid, blend calibration, and the predicted (J, E) action content). NRHJSurAA.load_h5 resolves the shared merger-ringdown arm by name at load and verifies it by content digest — a missing or wrong arm raises loudly; there is no silent inspiral-only fallback. NRHJSur3dq8_AdA_v3.h5 composes the same way — a 14-element inspiral plus the shared arm — not a merger-ringdown-only file.


1. Python-only quickstart (numpy backend)

Works on any OS/CPU; no compiler.

# 1. code — Python 3.10 or newer
pip install nrhjsurrogate

# 4. artifacts — already in the checkout, nothing to copy (section 5)
#    (git lfs pull above already fetched the released *_v3.h5 files,
#     including the shared merger-ringdown arm the models need)

# 5. acceptance test
nrhjsur-smoke

First use:

from nrhjsurrogate import NRHJSurAA

model = NRHJSurAA.load_h5("NRHJSur3dq8_AA_v3")   # a NAME; see section 0a

out = model(2.0, 0.3, -0.1)                # geometric units, native grid
h22 = out[(2, 2)]                          # t = 0 at the (2,2) peak

r = model.physical(40.0, 25.0, 0.3, -0.1,  # LAL-style physical units
                   delta_t=1.0 / 4096, f_min=20.0,
                   distance=440.0, inclination=0.7)
hp, hc = r["hp"], r["hc"]

The legacy NRHJSurAA.load(pkl, mr_path=..., allow_pickle=True) path still exists — for a locally-trained model (NRHJSurAA.build / .save), not the shipped artifact. From 1.5 it needs allow_pickle=True, because unpickling runs code chosen by whoever wrote the file; without it a .pkl path raises ValueError. Its behavior on an unresolvable mr_path="auto" changed: it now raises a clear FileNotFoundError naming what to pass, instead of silently degrading to an inspiral-only model.

mr_path, updated: NRHJSurAA.load_h5 needs no mr_path in the normal case, but not because the arm is inside the model file — it is not. The shipped h5 records the arm's name, and the loader resolves NRHJSur3dq8_MR_v3.h5 down the same search path as the model itself and checks its content digest. Put the arm anywhere on that path (or fetch it, which happens automatically) and you pass nothing; leave it out and the load raises FileNotFoundError naming the file and every directory searched, rather than returning a model that stops at the merger. merger_ringdown_path= overrides the resolved arm when you deliberately want a different one. For the legacy NRHJSurAA.load(pkl, ...) path, mr_path="auto" now raises that FileNotFoundError (naming what to pass) rather than silently falling back to inspiral-only; mr_path=None is unchanged and still means an explicit, intentional inspiral-only load.

With 8 threads: model load 0.08 s; full IMR generation ≈ 114 ms per waveform single-call; physical() ≈ 120 ms. The numpy backend is a ~0.1 s-per-call class — fine for interactive work and small studies. (The ~16 ms/waveform figure quoted elsewhere is the compiled batched path, not this one; see sections 2–3.) For sampling-scale throughput use the compiled backend.


1a. Which route does pip install take?

pip install nrhjsurrogate prefers a wheel, and the wheels carry the compiled evaluator with OpenMP enabled — there is nothing to build and nothing to configure:

interpreter platform what you get
3.10 – 3.14 Linux x86-64 (manylinux_2_28) wheel: compiled Kokkos, OpenMP ON
3.10 – 3.14 macOS arm64 (Apple Silicon, 14+) wheel: compiled Kokkos, OpenMP ON, libomp bundled in nrhjsurrogate/.dylibs/
3.10 – 3.14 Windows x64 wheel: compiled Kokkos, OpenMP ON, scalar evaluator (see below)
anything else (3.15, free-threaded 3.14t, Intel Mac, aarch64) source distribution, compiled on your machine (section 2a)

3.14 is new in 1.4; the free-threaded 3.14t builds are deliberately not published. There is no macOS x86-64 (Intel) wheel: GitHub retired that runner image, so Intel Macs build from the source distribution.

What the Windows wheel does not have. 1.4 vectorised four evaluator kernels over the sample axis. MSVC refuses the OpenMP simd directive they used (error C7660) unless switched to an experimental mode, so in 1.4 those kernels defaulted OFF on MSVC and the Windows wheel ships the same scalar kernels 1.3 did. It is not less accurate -- every platform is checked against the same reference values by nrhjsur-verify --strict -- only not faster.

From 1.5 (unreleased) the kernels spell the directive through one macro (cpp/simd_pragma.hpp), which cl.exe sees as its own loop(ivdep) hint, and the options default ON on MSVC as they do everywhere else. Linux and macOS compile exactly as before. Measured on GitHub's windows-latest runner (wheels run 38654735855 (2026-10-14)): cl.exe compiles the vectorised kernels, nrhjsur-verify --strict passes 17 of 17, and on one thread they are about 1.4x faster than the scalar ones for a single waveform and up to about 1.5x batched (that runner's timings were noisy). A source build that wants the 1.4 Windows behaviour can pass -DNRHJ_EVAL_SAMPLE_SIMD=OFF -DNRHJ_TAU_SAMPLE_SIMD=OFF -DNRHJ_EVAL_NO_CONTRACT=OFF.

Measured 2026-09-10 against the published 1.3 wheel, in a clean venv with no repository on the path: default_backend kokkos, kokkos_openmp ON, nrhjsur-verify --strict 17 of 17. On a user's Apple Silicon machine the same wheel loads exactly one libomp (the bundled one) and is 4.2x faster at B=64 than the same build held to one thread.

So --no-binary is not how you get the compiled backend, and using it where a wheel exists makes things worse: it discards a tested binary, spends minutes compiling Kokkos, and re-opens the possibility of a Serial-only build. Three good reasons to force a source build anyway:

The x86-64 wheels need AVX2

The Linux wheels compile the evaluator with -march=x86-64-v3 (AVX2, FMA, BMI1/2, F16C, LZCNT, MOVBE) and the Windows wheel with /arch:AVX2, while the wheel tags only say x86_64 / amd64. Every x86-64 server and desktop CPU since about 2015 qualifies; some older or low-power ones do not.

From 1.5 the package checks before it imports the compiled module: it reads the ISA flag from the build record (python -m nrhjsurrogate.compiled_backend shows it as native_isa_flag) and the CPU's features from /proc/cpuinfo (Linux), sysctl machdep.cpu.* (macOS) or IsProcessorFeaturePresent (Windows; it cannot report FMA/BMI, so only AVX/AVX2 are checked there). If a feature is missing, the compiled module is not imported, one RuntimeWarning names the missing features, and backend="auto" (the default) uses the NumPy backend, which gives the same waveforms more slowly. backend="kokkos" raises ImportError with the same reason. Before 1.5 such a CPU crashed with an illegal instruction (SIGILL) on first use.

To get the compiled backend on such a machine, build it there from the source distribution, which targets the CPU doing the build:

pip install --no-cache-dir --force-reinstall --no-binary nrhjsurrogate nrhjsurrogate

The macOS arm64 wheel carries no x86 ISA flag and is not affected.

macOS, and the one trap

Apple's clang ships no OpenMP runtime, so a source build on macOS falls back to Kokkos Serial unless one is installed first, and a Serial build is a success to pip and single-threaded to you. This is the single most common macOS surprise, and it is a source-build problem only — the wheel carries its own copy.

conda install -c conda-forge llvm-openmp        # conda / pixi
brew install libomp                             # or Homebrew (keg-only)

Homebrew's libomp is keg-only, i.e. on no default search path, so CMake may also need to be pointed at it:

CMAKE_ARGS="-DOpenMP_ROOT=$(brew --prefix libomp)" \
  NRHJ_BUILD_KOKKOS=require NRHJ_BACKEND=openmp \
  pip install --no-cache-dir --force-reinstall --no-binary nrhjsurrogate nrhjsurrogate

There is no PyPI package that could be declared as a dependency to solve this for you: llvm-openmp, libomp and openmp do not exist on PyPI, and intel-openmp publishes no macOS files (checked 2026-09-10). A runtime dependency would also be installed after the build, which is too late to affect it.

Python versions without a wheel

Wheels cover CPython 3.10 to 3.14 (table above). On anything else, such as 3.15 or the free-threaded 3.14t, pip compiles the source distribution, and on macOS that runs into the OpenMP trap above. Either install an OpenMP runtime first, or use a 3.10 to 3.14 environment and take the wheel.

Checking what you actually got

python -m nrhjsurrogate.compiled_backend   # the full build record
nrhjsur-bench --quick                      # header says "Kokkos OpenMP: ON" or "OFF"

1b. Choosing the backend: NRHJ_BACKEND

One variable picks what gets compiled. It is orthogonal to NRHJ_BUILD_KOKKOS, which only decides whether to compile at all, so NRHJ_BUILD_KOKKOS=require NRHJ_BACKEND=cuda means "a CUDA build or no install" — what a GPU CI job wants, and what neither variable can say alone.

Both are build-time only. Neither has any effect on a wheel install, because a wheel is not compiled; to make either of them matter you must also be taking the source route (section 1a).

NRHJ_BACKEND what you get notes
auto (default) Serial + OpenMP if the compiler has it, else Serial never fails the install
numpy no compile at all, pure-Python wheel same as NRHJ_BUILD_KOKKOS=0
serial Serial only, single-threaded for reproducing a single-thread number
openmp Serial + OpenMP, fails the configure if OpenMP is missing turns a silent degradation into a loud error
cuda Serial + OpenMP + CUDA needs NRHJ_CUDA_ARCH
NRHJ_BUILD_KOKKOS=require NRHJ_BACKEND=openmp \
  pip install --no-binary nrhjsurrogate nrhjsurrogate
NRHJ_BACKEND=cuda NRHJ_CUDA_ARCH=sm_75 \
  pip install --no-binary nrhjsurrogate nrhjsurrogate

Pinning NRHJ_BUILD_KOKKOS=require alongside NRHJ_BACKEND=openmp is the belt-and-braces form: the first refuses to install without a compiled backend, the second refuses to configure without OpenMP. Together they make every silent degradation a loud failure.

NRHJ_CUDA_ARCH takes either spelling — sm_75 or the Kokkos name TURING75. It is required and never guessed, because nvcc_wrapper's own default is sm_70 and CUDA 13 rejects that outright, which would surface as a confusing mid-build failure instead of a clean one.

CMAKE_ARGS is still honoured verbatim and is applied after these, so anything you spell out by hand overrides the shorthand.

What each mode leaves in the build record, which you can read back with python -m nrhjsurrogate.compiled_backend:

mode status serial / openmp / cuda
numpy skipped —
serial built ON / OFF / OFF
openmp built ON / ON / OFF
cuda built ON / ON / ON, plus the architecture

A CUDA build carries the host spaces too: it is CPU and GPU in one module, not GPU instead of CPU.

2. CPU-OpenMP kokkos backend (compiled)

The compiled evaluator (cpp/ → nrhjkokkos) accelerates the element-evaluation and assembly stages; the Python package auto-detects it (the import is deferred, so nothing breaks if it is absent).

2a. The source route: pip install of the source distribution

This is what pip does by itself wherever no wheel matches (section 1a), and what --no-binary asks for explicitly. Since 2026-09-02 (release checklist item B1) a source install builds the compiled backend itself, including Kokkos. Nothing has to be installed beforehand except a C++17 compiler — and, on macOS, an OpenMP runtime (section 1a):

pip install --no-binary nrhjsurrogate nrhjsurrogate     # from PyPI, forcing the sdist
pip install nrhjsurrogate-1.0.0.tar.gz                  # or the tarball itself

What happens, in order:

  1. pip installs the build tools (pybind11, cmake) into its isolated build environment; these are wheels, no compile.
  2. setup.py runs CMake on cpp/. CMake looks for an installed Kokkos first (Kokkos_ROOT, Kokkos_DIR, CMAKE_PREFIX_PATH, or the conda environment it runs in). If none is found it downloads Kokkos 5.2.0 (tag 5.2.0, SHA-256 checked, 1.8 MB) and builds it into the module as a static library, with the Serial backend always and the OpenMP backend when the compiler has OpenMP. 5.2.0 is the version every measurement in this repository was taken on; the pin and its reason are in cpp/cmake/kokkos_dependency.cmake.
  3. The module lands at the root of site-packages as a top-level nrhjkokkos, which the package imports before it looks anywhere else.

On an 8-core machine the CMake step takes about 30 s from download to link, and the whole pip install about two minutes, most of it dependency downloads. With an installed Kokkos found through Kokkos_ROOT nothing is downloaded and only the module's seven files compile.

What it needs, and what Kokkos 5.2.0 itself insists on: CMake ≥ 3.22 (pip provides it), GCC ≥ 10.4, Clang ≥ 14, Apple Clang, or MSVC 19.30 (Visual Studio 2022), and network access the first time unless Kokkos_ROOT points at an install. A compiler below the floor is refused by Kokkos's own configure step, which brings us to:

A failed build does not fail the install. If the compile cannot happen (no compiler, one that is too old, no network), the install still succeeds with the complete NumPy backend. It says so once: a banner in the pip install -v transcript, and one RuntimeWarning the first time import nrhjsurrogate runs in a process. The reason and the tail of the CMake log are recorded inside the installed package; read them with

python -m nrhjsurrogate.compiled_backend

After fixing the cause, rebuild with

NRHJ_BUILD_KOKKOS=require NRHJ_BACKEND=openmp \
  pip install --no-cache-dir --force-reinstall --no-binary nrhjsurrogate nrhjsurrogate

The two variables are what stop the rebuild from repeating the degradation you are rebuilding to fix: without them a second Serial build is another success. --no-cache-dir is not decoration: pip caches the wheel it built, degraded or not, and a plain reinstall would reuse it without compiling.

Controls, all environment variables so that they pass through pip's build isolation unchanged:

variable effect
NRHJ_BUILD_KOKKOS=0 do not attempt the compiled backend at all; the install is pure Python and the warning never appears. Use this when you want the NumPy path and do not want to wait for a compile.
NRHJ_BUILD_KOKKOS=1 the compile must succeed or the install fails (for CI and the release acceptance test). Default when unset: try, and degrade to NumPy on failure.
Kokkos_ROOT=<prefix> use that installed Kokkos; nothing is downloaded. Kokkos_DIR and CMAKE_PREFIX_PATH work the same way.
CXX=<compiler> which C++ compiler CMake uses (the standard CMake variable).
CMAKE_ARGS="-D... -D..." any option of cpp/CMakeLists.txt, for example -DNRHJ_MARCH_NATIVE=OFF for a build that does not target the build host's ISA, or -DFETCHCONTENT_SOURCE_DIR_KOKKOS=/path/to/kokkos-5.2.0 for an offline build from an unpacked Kokkos source tree.
CMAKE_BUILD_PARALLEL_LEVEL=N compile jobs; default min(cpu count, 8).

NRHJ_KOKKOS_DIR is a different variable: it tells the loader where an already built nrhjkokkos module is, and is not involved in building one.

Building on a cluster: read this before you install on a login node

By default the compiled backend is built with -march=native, which targets the instruction set of the machine running the compile. On a workstation that is what you want. On a cluster it is a trap:

  1. you pip install on a login node,
  2. the build succeeds and nrhjsur-smoke passes there,
  3. your job lands on a compute node with an older or simply different CPU,
  4. and the module dies with an illegal instruction (SIGILL) — with nothing in the error naming a build-time flag as the cause.

The install now prints a warning at configure time whenever this flag is applied, and records the flag together with the CPU it was chosen for, so python -m nrhjsurrogate.compiled_backend will show you both after the fact.

If the nodes you will run on are not the node you are building on, choose one:

# portable baseline: runs anywhere, gives up AVX2/FMA tuning
CMAKE_ARGS="-DNRHJ_MARCH_NATIVE=OFF" \
  pip install --no-binary nrhjsurrogate nrhjsurrogate

# or name the floor your compute nodes actually meet (usually better)
CMAKE_ARGS="-DNRHJ_MARCH_NATIVE=OFF -DCMAKE_CXX_FLAGS=-march=x86-64-v3" \
  pip install --no-binary nrhjsurrogate nrhjsurrogate

x86-64-v3 is AVX2 + FMA, which every server CPU since roughly 2015 has. Building on a compute node, in an interactive job, avoids the question entirely and is the simplest answer where the queue allows it.

Note that NRHJ_MARCH_NATIVE=OFF is also the configuration the bitwise reference gates are defined on (cpp/CMakeLists.txt), so a portable build is if anything the more conservative choice numerically, not less.

Platforms. Verified: Linux x86-64 with GCC, macOS arm64 with Apple Clang, and Windows x64 with MSVC — the last two since 2026-09-09, on hosted runners for every published wheel (each one loads its compiled backend, exits cleanly and passes nrhjsur-verify --strict 17 of 17 before it may be uploaded) and, for macOS, independently on a user's Apple Silicon machine. Intel macOS remains unbuilt: GitHub retired that runner image and no Intel Mac was available, so those users take the source route. The CMake logic chooses -march=native on x86-64, -mcpu=native on Apple Silicon and no ISA flag on MSVC (cl.exe has no "native" option; -DNRHJ_MSVC_ARCH_FLAG=/arch:AVX2 through CMAKE_ARGS opts in), confirms each flag with the compiler before using it, and builds Kokkos Serial-only on MSVC and on an Apple Clang without libomp. cpp/cmake/compiler_flags.cmake states each branch and its status.

Check what you got:

python -m nrhjsurrogate.compiled_backend      # built / skipped / failed, with the reason
nrhjsur-smoke                                  # the kokkos arm must say PASS, not SKIP

2b. Developer route: build cpp/ by hand in a checkout

The recipe every recorded measurement used, with the conda-forge toolchain (all of it comes from setup_env.yml):

# an environment providing kokkos 5.2, pybind11, cmake and a C++17 compiler
ENV=$CONDA_PREFIX
cmake -S cpp -B cpp/build \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_CXX_COMPILER=$ENV/bin/x86_64-conda-linux-gnu-g++ \
  -DKokkos_ROOT=$ENV \
  -Dpybind11_DIR=$($ENV/bin/python -m pybind11 --cmakedir) \
  -DPYTHON_EXECUTABLE=$(which python)
cmake --build cpp/build -j 8

Notes:

Use it:

model = NRHJSurAA.load_h5(h5_path, backend="kokkos")   # or model.backend = "kokkos"

Verify: nrhjsur-smoke also exercises the kokkos backend and gates numpy-vs-kokkos parity (worst-mode relative L2 below 1e-6; it typically comes out around 3e-9).

For what the compiled backend buys you, see docs/USAGE.md section 9, which carries the current measured numbers for both backends taken on one machine at OMP_NUM_THREADS=8. They are one machine's numbers, not a specification; measure on yours.


3. CUDA build — GTX 1650 Ti (Turing, sm_75) target

This route has been built and run on a Turing-class card (sm_75) with CUDA 13.0; the compile takes a couple of minutes. It is the least exercised path in this document — see the note in the introduction.

The short way. The backend selector does all of this for you; you should not need the manual recipe below unless you are debugging it:

export PATH=/path/to/cuda/bin:$PATH        # nvcc must be findable
NRHJ_BUILD_KOKKOS=require NRHJ_BACKEND=cuda NRHJ_CUDA_ARCH=sm_75 \
  pip install --no-binary nrhjsurrogate nrhjsurrogate

setup.py fetches the pinned Kokkos itself to obtain nvcc_wrapper, makes it the project's C++ compiler, and hands the same unpacked tree back to CMake through FETCHCONTENT_SOURCE_DIR_KOKKOS so nothing is downloaded twice. Override the wrapper with NRHJ_NVCC_WRAPPER=/path/to/nvcc_wrapper, or let an installed Kokkos supply it via Kokkos_ROOT.

Two things worth knowing if you build this by hand.

  1. A CUDA build does not require an installed Kokkos — the fetched one supplies nvcc_wrapper.
  2. nvcc_wrapper has to be the compiler for the whole project, not just for Kokkos. Enabling Kokkos_ENABLE_CUDA while leaving your host g++ as CMAKE_CXX_COMPILER compiles everything and then fails at link with symbol 'fatbinData' is already defined, because the toolchain's link-time-optimisation flags collide with the CUDA fatbins. Using nvcc_wrapper throughout avoids it.

3.0 Read this first: is the GPU worth it on a 1650 Ti?

3.1 Build Kokkos with CUDA (once)

# needs: CUDA toolkit (nvcc in PATH) and a gcc compatible with that CUDA
git clone https://github.com/kokkos/kokkos && cd kokkos
cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX=$HOME/Installations/kokkos/cuda \
  -DKokkos_ENABLE_CUDA=ON \
  -DKokkos_ENABLE_OPENMP=ON \
  -DKokkos_ARCH_TURING75=ON \
  -DCMAKE_CXX_STANDARD=17
cmake --build build -j && cmake --install build

-DKokkos_ARCH_TURING75=ON is the sm_75 architecture flag — the right one for the GTX 1650 Ti. Keeping OpenMP on in the same install lets you fall back to the CPU backend without a second Kokkos.

3.2 Build nrhjkokkos against it

From the recipe in cpp/CMakeLists.txt (adjust paths):

export PATH=/usr/local/cuda/bin:$PATH          # nvcc must be found
export NVCC_WRAPPER_DEFAULT_COMPILER=$(which g++)
cmake -S cpp -B cpp/build-cuda \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_CXX_COMPILER=$HOME/Installations/kokkos/cuda/bin/nvcc_wrapper \
  -DCMAKE_CXX_FLAGS=-arch=sm_75 \
  -DKokkos_ROOT=$HOME/Installations/kokkos/cuda \
  -Dpybind11_DIR=$(python -m pybind11 --cmakedir) \
  -DPYTHON_EXECUTABLE=$(which python)
cmake --build cpp/build-cuda -j
ln -sf $PWD/cpp/build-cuda/nrhjkokkos.*.so cpp/build/   # where load_module looks

Known sharp edges (already handled in-tree):

Then run nrhjsur-smoke — the parity gate is backend-agnostic and must pass on CUDA exactly as on CPU in float64.


4. Artifact download / copy manifest

Installed users (no repository): you need not do anything. Loading a released model by name downloads what is missing and verifies it. To do it ahead of time, by name, which verifies them the same way —

import nrhjsurrogate
nrhjsurrogate.fetch_model("NRHJSur3dq8_AA_v3")
nrhjsurrogate.fetch_model("NRHJSur3dq8_AdA_v3")
nrhjsurrogate.fetch_model("NRHJSur3dq8_MR_v3")

— or download them yourself and check them:

export NRHJ_MODEL_DIR=/path/to/models
sha256sum -c <<'EOF'
b6d2d300035f5c9f7dac005980e8f01536ee59c08f8efaf57643f46811ebf483  NRHJSur3dq8_AA_v3.h5
c0ca99222f8972e3d13f8273782e37fe84292ef9741e06893d2a7f18ff549a1a  NRHJSur3dq8_AdA_v3.h5
dd1bf00860d01e16cf740bd227f5f7fb163284cb42309febd7194bc8d235059a  NRHJSur3dq8_MR_v3.h5
EOF

nrhjsur-verify does the same check against the package's own registry, for whichever released files it finds, and names any that are missing.

Developers with a checkout: everything is fetched straight into the checkout — no separate copy step onto a cache directory:

git lfs install && git lfs pull      # model_release/*.h5

The checkout copies carry construction provenance that is stripped from the deposited ones, so their sha256 does not match the table above and is not meant to. The digests above are for files obtained from the deposit.

Not needed on a run-only machine: the training-pickle cache the released models were built from, loo_all260_hybrid.npz, the SXS catalogue cache, LAL data.


5. Environment variables that matter

Build-time (read by pip install, not at run time)

variable values effect
NRHJ_BUILD_KOKKOS auto (default), 0/skip, 1/require whether to compile at all; require fails the install rather than degrading
NRHJ_BACKEND auto (default), numpy, serial, openmp, cuda which backend to compile (section 1b)
NRHJ_CUDA_ARCH sm_75, TURING75, … required by NRHJ_BACKEND=cuda; never guessed
NRHJ_NVCC_WRAPPER path use this nvcc_wrapper instead of fetching Kokkos for one
CMAKE_ARGS any CMake flags honoured verbatim, applied last so it overrides the selector
CMAKE_BUILD_PARALLEL_LEVEL integer build parallelism; default min(cpu, 8) (the cap is memory)
Kokkos_ROOT / Kokkos_DIR prefix use an installed Kokkos instead of fetching
CXX path the host compiler

What the install tells you afterwards

pip discards build output on success and offers no post-install hook, so the outcome is written into the installed package and surfaced three ways:

python -m nrhjsurrogate.compiled_backend   # the full build record, any time

The record carries the compiler version (not just its path), the CMake build type and flags, Kokkos_ENABLE_{SERIAL,OPENMP,CUDA,HIP}, the device Kokkos_ARCH_*, the backend you requested, and the verbatim CMAKE_ARGS. A binary without its flags cannot be reasoned about, and a performance or bit-exactness claim has to name what produced it.

Thread binding — set for you, automatically

Importing nrhjsurrogate sets OMP_PROC_BIND=close and OMP_PLACES=cores, unless you have already chosen. You do not need to set them by hand.

This happens in the first lines of nrhjsurrogate/__init__.py, ahead of every other import, because OMP_PROC_BIND and OMP_PLACES are read once, when the OpenMP runtime initialises — setting them later is a silent no-op.

What was measured, and what was not. On an AMD EPYC 7742, paired arms on a genuinely exclusive whole-node allocation (20 rounds, byte-identical null control, 4-thread negative control), binding gives 1.08–1.11x on the WHOLE CALL at OMP_NUM_THREADS=32 — 1.083 at B=1, 1.090 at B=256, 1.088 at B=1024, against a null-control floor of ~1.3%. Kokkos' own printed advice (spread) measured worse than unbound, which is why the default here is close rather than what the warning suggests.

It is bitwise-inert by construction: an environment variable cannot change floating-point arithmetic, so nothing about accuracy is at stake.

Overriding:

Check what the runtime actually did — this reads back omp_get_proc_bind(), it does not echo the environment:

import nrhjsurrogate
print(nrhjsurrogate.thread_binding())
# {'env': {...}, 'applied_by_us': {...}, 'proc_bind': 'close', ...}

OMP_NUM_THREADS is deliberately NOT defaulted. A binding policy generalises across machines; a thread count does not (OMP=32 was the wall on that box — OMP=64 never faster and 13% slower at B=1, a property of that hardware). Set it yourself per the table below.

Note: scripts/bench_memo.slurm, the preamble much of this project copies, exports OMP_PROC_BIND=false. That is respected — and it means the in-repo benchmark preamble opts out of the faster setting. Drop that line to pick it up.

RESOLVED — the two measurements never disagreed; one of them was contaminated and the other was about a different regime.

  • The single-process claim of 1.25–1.28x is withdrawn. It was taken on a node whose load climbed from 0.33 to 24 during the run. Retaken on a whole-node exclusive allocation it is 1.08–1.11x whole-call, with a ~1.3% null floor.
  • The fork-pool warning stands and is now handled in code, not by advice: the library declines to bind when OMP_NUM_THREADS<=1, which is exactly the fork-pool mode where a bound master's one-CPU mask is inherited by fork and can collapse an N-worker pool onto one core.

So the shipped default no longer trades one regime against the other. A further measured result: the explicit close/cores pair buys essentially nothing over the library default (1.085 vs 1.083 at B=1) — so setting these variables by hand is not worth doing, and the value is in the library default alone.

If you fork worker pools you should still find the default harmless, but NRHJ_NO_THREAD_DEFAULTS=1 (or an explicit OMP_PROC_BIND=false) remains the escape hatch.

variable run value why
OMP_PROC_BIND, OMP_PLACES close, cores — set for you on import measured 1.08–1.11x whole-call at OMP=32 on a dedicated node (B=1 1.083, B=256 1.090, B=1024 1.088; null-control floor ~1.3%). An earlier 1.25–1.28x was taken on a shared node as its load climbed from 0.33 to 24 and is withdrawn — binding works by exclusive occupancy of the cores it binds, so under contention you measure contention. Any explicit value of either yours wins
NRHJ_NO_THREAD_DEFAULTS unset set to 1 to stop the package touching affinity at all
NRHJ_MODEL_DIR directory (or $PATH-style list) holding the released *.h5 the one users need. Searched by nrhjsurrogate.models.resolve_model right after an explicit path, so it wins over the per-user cache and over a checkout's model_release/
NRHJ_MODEL_CACHE_DIR download destination overrides the per-user cache (platformdirs if installed, else $XDG_CACHE_HOME/nrhjsurrogate, else ~/.cache/nrhjsurrogate) that fetch_model writes into
NRHJ_MODEL_BASE_URL deposit base URL, a local directory, or a file:// URL where the download comes from. Defaults to the Zenodo deposit, so it needs setting only to point at a mirror, a local directory or a file:// URL
NRHJ_AUTO_FETCH unset, meaning on set to 0, off, no or false and a missing artifact raises with instructions instead of downloading. resolve_model(..., fetch=False) does the same for one call
NRHJ_CACHE_DIR default ~/nrhj_cache build-time cache root; the four NRHJSurAA.build/load defaults hang off it. Replaced a set of hardcoded absolute cluster paths
NRHJ_MODEL_PKL path to the training pkl legacy-path only: default artifact for NRHJSurAA.load() and for nrhjsur-smoke --pkl. Not read by NRHJSurAA.load_h5 at all
NRHJ_V4_H5 path to nrhjsur_production_v4_swsph.h5 legacy-path only: MR-arm default for nrhjsur-smoke --pkl and for NRHJSurAA.load(..., mr_path="auto")'s fallback candidate. Irrelevant to the default load_h5 flow, which needs no MR path at all
OMP_NUM_THREADS desktops: the physical core count. Server CPUs: 32–64 (measured plateau on EPYC 7742; all 128 threads THRASH ~10x) kokkos OpenMP + numpy BLAS threading
OMP_PROC_BIND leave it unset The library sets close (+ OMP_PLACES=cores) for you when the process is multi-threaded, in kokkos_eval.load_module() before the compiled module is imported — libgomp reads these once at its own initialisation, so setting them later is a silent no-op. Setting the variable yourself to anything, including false, is treated as a deliberate choice and is never overridden. The library declines to bind when OMP_NUM_THREADS<=1: that is production PE's fork-pool mode, where a bound master thread has a one-CPU mask that fork inherits, so binding can collapse an N-worker pool onto one core — the hazard this row previously recommended false to avoid. NRHJ_PROC_BIND=off disables the default; =force applies it even at one thread.
LAL_DATA_PATH not needed nothing on the run path imports lal
NRHJ_PROD_PKL, NRHJ_LOO_NPZ train-time only NRHJSurAA.build() inputs

Renamed variables. Every variable this package reads uses the NRHJ_ prefix. The older names still work and emit a DeprecationWarning once per process: a variable set in a shell profile or job script cannot be updated by an upgrade, and a hard rename would not raise — it would silently fall back to a default and run the wrong model.

The warning names the replacement, so you do not need a table here: set the variable it tells you to and remove the old one. One pairing is worth calling out because the names are close and the directories are not the same — the old model-cache variable maps to NRHJ_MODEL_CACHE_DIR, not to NRHJ_CACHE_DIR, which is the build-cache root.

The per-user artifact cache is ~/.cache/nrhjsurrogate. If you have artifacts under a cache path from an earlier version, move them across or point NRHJ_MODEL_DIR at it; otherwise they are simply downloaded again.


6. Acceptance testing, deeper layers