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_schurandfrozen_residualsections;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
SpecifiedModewhose dispersion moves along z, the adiabatic opt-outtaper_coupling=Falseof 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_DEVICESorHIP_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
|
meaning |
|---|---|
unset or empty |
no requirement, the default for every CPU user |
|
the run must execute on a CUDA build that sees a device |
|
the run must execute on a HIP build that sees a device |
|
the run must execute on a device build of either kind |
anything else |
raises a |
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, orcheck_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 namebackend="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
RuntimeErroron macOS aboutpyfftw, 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. Importsolprop_nativeat the top of the script, before anything importspyfftw(the package raises the error when it detects the wrong order); see Performance tuning.Slower than expected. Check
python -m solprop_native: areferenceprofile, no vendor BLAS (blas_backendnone) or a generic architecture (neithernative_arch_enablednor acpu_baseline) each cost a large factor. Acpu_baselineofx86-64-v2runs about 20 to 25 % longer; if the processor should run x86-64-v3, a virtual machine may be hiding a feature, and thesolprop_native._cpulogger 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.