The native backend: fast CPUs and GPUs#

solprop’s physics is decided before a backend sees it: every backend executes the same translated problem, and the NumPy reference backend is the oracle that the others are tested against (see Choosing a backend). The native backend is the fast one. It is the package solprop-native, built from the native/ directory of the solprop repository at solprop’s own version: C++ kernels written with Kokkos, bound to Python with nanobind, and compiled for the machine that runs them —

  • on a CPU with OpenMP threads, FFTW and a vendor BLAS (Accelerate, MKL, OpenBLAS or AOCL). A locally built CPU backend is the fastest backend measured on the coherent-wall and general modal cases, and its lead over the in-tree numba_cpu backend grows with the thread count: on a large graded-index guide it takes about 0.6 of numba_cpu’s time at one thread and about a quarter at eighteen;

  • on an NVIDIA GPU with CUDA (cuFFT and cuBLAS), or an AMD GPU with HIP (rocFFT and rocBLAS).

You never need it for correctness: without it solprop runs everything on numpy_ref and numba_cpu. With it installed, backend="auto" — the default of every entry point — selects it.

What it runs#

The native backend runs:

  • field and envelope grids; single-mode, multimode and polarisation-resolved modal runs;

  • Kerr and third-harmonic terms, field and envelope plasma, scalar and vector Raman responses, gas mixtures with per-species channels, noise and absorbing boundaries;

  • the general modal route (guides that supply their own vector E and H, including guides that vary along z), the coupled flow of the coherent capillary wall and of tapers, with its constant_schur and frozen_residual sections;

  • radial, Cartesian, line and planar free-space and planar-guide geometries;

  • fixed-schedule batches (lockstep scan members);

  • the Chebyshev z table: a linear operator or nonlinear prefactor that varies along z in a way that has to be tabulated, rather than expanded in the medium’s parameters. Examples are a SpecifiedMode whose dispersion moves along z, the adiabatic opt-out taper_coupling=False of a multimode capillary taper whose expansion does not resolve, and a gas mixture of three or more species on independent density paths. The CPU build holds the table’s coefficients and evaluates it at each step’s positions in its own kernels (z tables).

Everything numba_cpu runs, the native CPU build runs too (SEP 29, §6.6). The GPU builds do not run the z table: they refuse it before anything is allocated, giving the table’s size, since it would be held resident on the device; run such a problem on a CPU build, or with backend="numba_cpu" in a process that has the GPU build installed.

Adams snapshots and resume history run on the native backend as on the others. It keeps the history of the default Multistep() integrator resident in its engine and copies the history’s rows to the host only where a snapshot is kept: at each save plane of an HDF5 output that keeps the resume history, and for each periodic snapshot. A resume installs them again and continues the history (resuming). A frozen-residual coupled flow’s history is kept in a native form (each sample’s generator applied to its state), so a snapshot of it continues only on the native backend, and a host engine’s snapshot of it only on numpy_ref or numba_cpu; a mismatch starts the history afresh and says why. Other histories cross between backends.

Separately, the cf4 section method of a coupled flow (CoherentWall(method="cf4")) runs on numpy_ref only, with the Runge–Kutta march; neither compiled backend runs it. The planar guide resolved in y is built into the GPU builds but has not been qualified on a device. solprop never falls back silently: an import failure, an incompatible build or a refused problem raises, naming the backend to use instead. The one fallback is announced: on a processor below the installed build’s CPU baseline, backend="auto" uses numba_cpu with a warning (choosing a backend).

Installing#

The native backend is solprop’s native extra, which requires solprop-native at solprop’s own version:

pip install "solprop[native]"

Precompiled wheels serve macOS on Apple silicon (osx-arm64), Linux on x86-64 (linux-64), Linux on aarch64 (linux-aarch64) and Windows on x86-64 (win-64). They are the distribution build: compiled for a CPU baseline (x86-64-v3, any Apple silicon Mac, Neoverse N1; processor requirements) and carrying a BLAS that shares the backend’s threads. Elsewhere, and for an NVIDIA or AMD GPU, build the package from source. A source build can also be the local profile, compiled for the machine that runs it with its own vendor BLAS, which is a few per cent faster than the wheel on the measured cases. A third profile, reference, is for development checks only and is noticeably slower. The profiles agree to rounding, not bit for bit. Building the native backend gives the requirements, the commands for Linux and macOS, every build option, and the CUDA and HIP builds.

Check the installation:

python -m solprop_native            # the build record, one JSON object
python -c "import solprop_native as sn; sn.check_build()"

check_build() raises if the build is inconsistent (for example a GPU build that sees no device) or does not fit the installed solprop: the two packages share a versioned kernel contract, and check_build() names any solprop member the build relies on that is missing. Install the two at the same version, from one release or one checkout. The build record reports the profile (build_profile), the BLAS (blas_selection, blas_library), FFTW, the compiler flags, the thread counts and, on a GPU build, the device. Then, in Python:

import solprop
from solprop.backends.registry import available, resolve_name

print("installed backends:", available())
print("backend='auto' means:", resolve_name("auto"))

With logging on (solprop.configure_logging("INFO")) every run logs Using native backend, and the backend and its settings are recorded in the output file (run.metadata.backend).

Working on solprop and its native backend. In a development checkout, one command builds the native package from native/ and installs it editable into the repository’s environment beside solprop:

uv sync --extra native

A plain uv sync leaves it out, and removes a copy installed by hand. The editable install does not rebuild itself: run uv sync --extra native --reinstall-package solprop-native after a native change. uv run poe check-native runs the native gate and uv run poe check-all both gates. A build stays valid across solprop changes until solprop’s kernel schema (SCHEMA_VERSION) changes or check_build() names a member the installed solprop lacks. Running solprop’s test lanes against the build is described in the developer notes on testing.

Running on a GPU#

GPU support is chosen when the package is built, and there are no GPU wheels: for an NVIDIA GPU (CUDA) or an AMD GPU (HIP), on Linux, build the package from source with the device enabled and the GPU’s architecture named, as Building the native backend describes. One installation is either a CPU build or a GPU build; to compare, install the two in separate environments. check_build() refuses a GPU build that cannot see a device.

Once installed, nothing in your scripts changes: backend="auto" runs on the device. The field stays resident on the device between steps and is copied back only at saved planes. Things to know:

  • One device per process. Choose it with CUDA_VISIBLE_DEVICES or HIP_VISIBLE_DEVICES. To use several GPUs, run independent problems (scan points, noise seeds) in separate processes, each seeing one device.

  • Small problems belong on the CPU. A single-mode fibre on a few thousand time samples is limited by kernel-launch latency, not arithmetic, and usually runs faster on the CPU. In a process that has the GPU build installed, pass backend="numba_cpu" for such problems. The GPU wins on large grids: multimode and general modal guides with many nodes, coherent walls with many channels, broadband supercontinuum grids, and free-space runs.

  • On-axis plasma statistics. The four modal on_axis_* plasma statistics require a host copy of the field at each requested save plane. Their CPU parity is qualified; they are not yet qualified on a GPU, and there is no device-only axis scan. See the statistics catalogue.

  • Reproducibility. A device answer is not bit for bit the CPU build’s, because the device sums in its own order; the difference is at the level of rounding and is covered by the same derived error bounds that every backend is tested against. A repeat on one build is held to the same bounds, not to identical bits (SEP 25).

  • Time your own problem first. Run ten steps on the device and on the CPU before committing to either for a long campaign.

Threads on the CPU build#

The CPU build uses the process’s thread budget by default: the CPUs in its affinity mask (which taskset or a batch scheduler’s allocation narrows), a container’s CPU quota and SOLPROP_THREADS, solprop’s budget for every CPU backend (performance tuning). SOLPROP_NATIVE_THREADS narrows it further, and four further variables size its individual pools; all are read once, when solprop_native is imported, so set them before starting Python:

SOLPROP_NATIVE_THREADS=8 OMP_PROC_BIND=spread OMP_PLACES=cores OMP_WAIT_POLICY=passive \
    python my_run.py

Performance tuning explains every variable, how the defaults are chosen, the OpenMP and BLAS settings that interact with them, and what to use for one large run, for a scan of many runs and on a cluster.

When something goes wrong#

  • ImportError, a schema mismatch, or check_build() naming a missing member. The installed solprop-native does not fit the installed solprop; install both at the same version (pip install --force-reinstall "solprop[native]", or both from one checkout as in Building the native backend).

  • “refuses this problem”. The message names what was refused and which backend runs it; pass that backend explicitly.

  • A RuntimeError on macOS about pyfftw, or a crash or hang. Two OpenMP runtimes were loaded, or would have been. Import solprop_native at the top of the script, before anything imports pyfftw (the package raises the error when it detects the wrong order); see Performance tuning.

  • Slower than expected. Check python -m solprop_native: a reference profile, no vendor BLAS (blas_backend none) or a generic architecture (neither native_arch_enabled nor a cpu_baseline) each cost a large factor. Then see Performance tuning.

The native backend’s place in this repository, its parity with numba_cpu and its precompiled wheels were proposed and carried out by SEP 29.