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;

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

It is the backend to use, and a required dependency: pip install solprop installs it, and backend="auto" — the default of every entry point — selects it. numpy_ref, which is unthreaded and roughly 6 to 40 times slower than threaded native (which backend to use), remains the numerical reference, and runs the few features native does not.

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).

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, in a second environment, or with backend="numpy_ref" 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; 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; the native backend does not run it. The planar guide resolved in y is built into the GPU builds but has not been qualified on a device. solprop has no fallback: an import failure, an incompatible build, a processor below the build’s baseline or a refused problem raises, naming what to do instead (choosing a backend).

Installing#

The native backend is a required dependency of solprop, at solprop’s own version, so there is no extra:

pip install solprop

Precompiled wheels serve macOS on Apple silicon (osx-arm64), Linux on x86-64 (linux-64), Linux on aarch64 (linux-aarch64), Windows on x86-64 (win-64) and Windows on Arm (win-arm64), and solprop runs where they install (installation). They are the distribution build: compiled for a CPU baseline (x86-64-v3 with x86-64-v2 beside it, any Apple silicon Mac, Neoverse N1; processor requirements) and carrying a BLAS that shares the backend’s threads. A source build is the way past a wheel’s limits: an NVIDIA or AMD GPU, or a platform or processor no wheel serves, where it may work, with no support promise. It 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.

An x86-64 wheel holds two builds of the extension, x86-64-v3 and x86-64-v2, and loads the best one the processor runs. SOLPROP_NATIVE_CPU_VARIANT=x86-64-v2 asks for the lower one, for tests and comparisons; a build above what the processor runs is refused by name, as an unknown name is. solprop_native.build_info() reports which build is loaded.

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, a plain sync builds the native package from native/ and installs it editable into the repository’s environment beside solprop:

uv sync

It needs a C++20 compiler; a contributor to the Python or the documentation can do without one (contributing). The editable install does not rebuild itself: run uv sync --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 (KERNEL_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. Install the CPU wheel in a second environment for such problems, or pass backend="numpy_ref" in a process that has only the GPU build, if the problem is small enough for it. 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.

A job that must run on a device#

A new release replaces a source build with that release’s CPU wheel, with no message, because solprop requires the native backend at its own version (upgrading a source build). So a GPU job says where it must run, and a job that lost its GPU build stops before it starts:

SOLPROP_REQUIRE_DEVICE=cuda python my_run.py

SOLPROP_REQUIRE_DEVICE

meaning

unset or empty

no requirement, the default for every CPU user

cuda

the run must execute on a CUDA build that sees a device

hip

the run must execute on a HIP build that sees a device

any

the run must execute on a device build of either kind

anything else

raises a BackendError naming the accepted values

It is an environment setting with no keyword, and scan workers inherit it. It is checked at backend selection, before any state is allocated or a step taken, by reading the build’s build_info().device_backend and device_count, never a version label. It refuses, with a BackendError, a CPU build, the wrong device backend and a device build that sees no device. It also refuses numpy_ref and mlx, by name or through backend="auto" for a TDSE or single-precision run, because such a run would not meet the job’s requirement; unset it for such a run.

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), their physical cores, 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, or both from one checkout as in Building the native backend).

  • backend="auto" refuses. The message says why: the native package is missing, the processor is below the build’s baseline, or its features could not be read (installation). A contributor’s lightweight setup has no native package, so name backend="numpy_ref" there.

  • A job stops at SOLPROP_REQUIRE_DEVICE. The installed build isn’t the device build the job asked for, or it sees no device. A new release replaces a source build, so rebuild it, or check the job’s allocation.

  • “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. solprop does not use pyFFTW, but a script or another package may. 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. A cpu_baseline of x86-64-v2 runs about 20 to 25 % longer; if the processor should run x86-64-v3, a virtual machine may be hiding a feature, and the solprop_native._cpu logger names it at INFO when the package is imported. Then see Performance tuning.

The native backend’s place in this repository and its precompiled wheels were proposed and carried out by SEP 29, and SEP 43 made it a required dependency.