Benchmarks#
How solprop’s performance is measured and judged, and what the measurements so far indicate. A performance claim names its measured window, build, machine and result; the rules below say how to take it. The numbers in the last two sections are indicative: each was measured once, on one machine, for one change. Re-measure with the standard suite before relying on any of them.
Where things are#
The standard benchmark suite is
benchmarks/(SEP 30), run withuv run poe bench(python -m benchmarks). It measures bounded windows of full-resolution problems in fresh processes and reports warm time, startup time, peak memory and numerical check status. Its default is a quick Kokkos CPU run of the four core cases (four-mode RDW, six-mode comb-to-continuum, a 384² Kerr beam and a 256-node radial filament); further sets cover modal capillaries, mode-averaged and GNLSE cases, free space, coherent walls, tapers, the general modal route and particular backend paths. Quick mode takes one sample and gives no verdict; standard mode takes paired, shuffled fresh-process samples at one thread and at the machine’s declared production width and applies the adoption rule below. Cases are versioned by their workload, so a changed definition is a new revision and is never compared with the old one. Running the benchmark suite is the manual: case sets, timing boundaries, collection, numerical qualification, native coverage and export.Results stay local. Runs, machine profiles, references and coverage reports go to the ignored
.benchmarks/suite/directory; no generated timing, reference or coverage report is committed.poe bench exportwrites a sanitised snapshot when one is wanted for a report.CPU performance: what was tried records the compiled CPU backend’s performance experiments: what was adopted, measured and rejected, and what remains open. Read it before repeating an experiment.
backends.md describes each backend’s execution design and the performance policies it adopts.
Rules#
State thread counts and plan flags; never compare across different ones.
A hot-path change needs a benchmark entry in its pull request or commit message.
Estimates are labelled as estimates in the docs until a measurement exists.
Do not tune for the benchmark: the cases are the target simulations at their real sizes.
A performance change is judged on the aggregate, not case by case: the equal-weight geometric mean of its per-case timing ratios over the cases it can affect, declared before timing, taken separately for the production thread policy and for one thread, with the complete per-case table beside it and a whole-suite aggregate when the work closes. A change is adopted when the production saving is significant: more than 5 %, and the geometric mean of the conservative per-case ratios
(candidate median + 2 MAD) / (before median − 2 MAD)below one, a non-positive denominator counting as unresolved. Small regressions on individual cases, or at one thread, are accepted when the aggregate improves; they stay visible in the table and do not veto the change. Numerical bounds are never relaxed to make a change pass. The standard suite’s comparison adds its clean-pair, repeatability and qualified reference requirements (collection and comparison).
Measurement windows#
Timing is measured on short windows, not on complete propagations: a complete comb-to-continuum run takes hours, and its cost is the window’s times a step count the window already predicts.
A timing window is about ten accepted steps of the adaptive run, frozen from an untimed pilot so that every repeat takes the same prescribed steps, or a fibre short enough that a complete run takes seconds.
Report the window that was measured: its z extent, accepted steps, what the timed region includes (propagation, observation and public statistics) and excludes (translation, backend construction, HDF5), and the A/A resolution indicator of the alternating pairs. Do not extrapolate a window to a complete-run cost.
A later state of a long propagation is timed by replaying a saved witness into a short window, not by regenerating the propagation.
Compare two source trees with alternating fresh-process pairs (A/B/A/B…), and report the A/A spread beside the ratio; a difference inside the spread is not a result.
The suite’s windows and what its timers include are listed under timing boundaries and accounting.
Reporting a performance claim#
The machine record. Every measurement carries the CPU (or device) model, core count, memory, operating system, Python and dependency versions (including the Kokkos build and its profile), thread counts, FFTW plan flags and wisdom state, the source revision and whether the tree was dirty, and the load before and after. The suite’s machine profile and run manifest record these; a hand-run measurement states them. Results are compared only within one machine.
A quiet machine. Absolute numbers are taken on a dedicated workstation or an exclusively allocated node. A busy laptop is fit only for same-process A/B comparisons, labelled as such: read their ratios, not their times. Report the A/A spread of repeated baseline runs beside every ratio; a difference inside it is not a result.
What goes in a pull request. The measured window and what it includes, the build and machine record, the per-case table with medians and spreads, the declared aggregate and its verdict, and any case that regressed. Concise evidence belongs in the change description; raw runs stay local or go into an exported snapshot.
Numba’s on-disk cache. Clear it
(find src -name '__pycache__' -type d -prune -exec rm -rf {} +) before timing in a
worktree that has run the suite many times: tens of thousands of cache files slow every
tool that walks the tree. Numba’s startup includes compilation or cache loading and is
not a reproducible cold-compilation measurement.
Indicative results#
Indicative ratios, not absolute times. Each is a short window (see above) on one machine; the rows’ cases, grids and step lengths differ, so rows share no scale. They say which backend or method to expect to win, and by roughly how much.
Backends and builds#
Speed-up over numpy_ref, one thread, ten-step windows on an 18-core AVX-512 server
(one socket, one NUMA node) with a local Kokkos build (vendor BLAS, native architecture).
The capillary cases have 3 or 4 channels on a 513-bin grid; the general-route cases are a
two-channel step-index pair and a graded-index guide at 2304 quadrature nodes.
case |
|
Kokkos CPU |
|---|---|---|
M1 soliton (GNLSE), reduced |
2.6× |
2.1× |
GNLSE soliton with Raman |
2.6× |
2.6× |
M2 (RDW capillary), reduced |
4.9× |
5.9× |
M2 (RDW capillary), full |
7.1× |
8.6× |
capillary, no wall |
2.5× |
3.9× |
capillary, scalar coherent wall |
2.1× |
3.5× |
capillary, TE/TM coherent wall |
4.2× |
8.8× |
capillary taper with wall (coupled flow) |
2.4× |
2.8× |
general route, step-index pair |
2.7× |
5.5× |
general route, graded index |
2.9× |
4.7× |
Threads. numpy_ref does not thread. At four threads Kokkos CPU ran the general route
2 to 6 times faster than numba_cpu and the capillary cases 1.5 to 3 times faster. On
larger general-route and coherent-wall workloads (2 to 32 channels, up to 16 384 fine
samples) at 18 threads it was 1.2 to 4 times faster than numba_cpu at 18 threads and 6 to
40 times faster than single-threaded numpy_ref. Small capillary grids stop scaling beyond
a few threads on every backend.
Kokkos build profiles, same server, seven ten-step capillary and general-route cases.
The vendor BLAS and the instruction set are separate gains. Against the reference build
(no vendor BLAS, generic x86-64), linking OpenBLAS alone made the straight capillaries 2.6
to 3.8 times faster at one thread (the tapers 1.7) and left the general route unchanged;
compiling for x86-64-v3 alone made the general route 1.4 to 1.5 times faster and left
the capillaries unchanged. A distribution-style build (x86-64-v3 and the
scipy-openblas32 OpenBLAS, ADR-034) ran within about 5 % of the local build
(-march=native, the system OpenBLAS) on every case at one and four threads and beat
numba_cpu on all seven; x86-64-v4 added 7 to 14 % on the general route only. The
distribution wheels now target a lower baseline (CPU performance).
At one thread, in milliseconds per ten steps:
case |
reference |
distribution-style |
local |
|
|---|---|---|---|---|
scalar wall |
61.4 |
21.8 |
22.8 |
35.0 |
TE/TM wall |
1547 |
390 |
402 |
480 |
taper wall |
90.3 |
54.6 |
56.6 |
64.7 |
general pair |
188 |
118 |
120 |
241 |
general GRIN |
358 |
245 |
244 |
454 |
On the same server MKL was 1.4 to 1.8 times faster than OpenBLAS on the TE/TM wall, where the MKL build keeps the custom complex kernel, and level elsewhere.
GPU against a whole CPU socket. On the same seven cases, the Kokkos CUDA build on a data-centre NVIDIA GPU and the HIP build on an AMD Instinct GPU ran 2.7 to 18 times faster than the Kokkos OpenMP build on all 18 cores of the server, the graded-index general route most. These grids are small for a device; the ratios say nothing about large grids.
Kokkos CPU operation-size threading#
The Kokkos CPU backend chooses each operation’s thread width from its dimensions at construction (tuning; the thresholds are under reference tables). It was calibrated on an 18-core arm64 laptop and a two-socket, 36-core AVX-512 server, against one thread on the small cases and against the previous default build on the large windows (best of complete paired rounds):
window |
arm64 laptop |
two-socket server, all 72 logical CPUs |
|---|---|---|
small cases (eight case/track configurations), geometric mean |
2.00× faster |
2.06× faster |
averaged helium RDW |
same within noise |
same within noise |
resolved blueshift |
2.0–2.6× faster |
1.8–2.4× faster |
reduced comb-to-continuum |
2.8–3.1× faster |
2.4–3.6× faster |
RDW ten-step window |
1.81× faster |
2.33× faster |
reduced comb-to-continuum ten-step window |
3.18× faster |
3.85× faster |
full comb-to-continuum ten-step window |
7.9 % faster |
2.7 % faster, noisy |
general modal |
4–11 % faster |
same, noisy |
Cartesian and radial filaments |
same within noise |
within 5 %, noisy |
scalar / TE-TM walls |
44 % / 17 % faster |
TE-TM same |
Separate thresholds for copies and arithmetic matter: an 8,192-element temporal copy is slower threaded, while arithmetic stages of the same size gain from the full pool. A wide response chunk followed by a full-width FFT needs both at full width, and a serialised coupled-flow matrix function on the scalar wall took about 20 times its threaded kernel’s time; both were corrected before the comparison above. “Noisy” rows did not meet the predeclared stability check; taper, short-line and planar controls were not requalified.
Radial analytic chunk dispatch#
Admitting partial analytic radial node chunks to the interleaved real GEMM, instead of a
scalar contraction that took about 94 % of the source time, made the radial filament’s
adaptive Adams window (256 and 512 nodes, 12 accepted steps, construction excluded) 2.8
to 8.8 times faster on Kokkos CPU, on an exclusively allocated two-socket AVX-512 server.
At one thread Kokkos then took 0.91 (256 nodes) and 0.95 (512) times numba_cpu’s time,
at 36 threads 0.10 and 0.12 times.
Multimode CPU step costs#
Sharing real cubic monomials across output components, planning general-modal tail
transforms over active rows only and seeding serial transforms with measured FFTW wisdom
brought Kokkos CPU to 1.00–1.07 times numba_cpu’s best time at one thread on an arm64
laptop, over 1–2 mm entrance windows of the general modal and reduced
comb-to-continuum cases with both adaptive methods. In ten-step profiles the general
route’s tensor work fell from 0.44 to 0.15 s and its ordinary FFTs from 1.00 to 0.84 s.
Measured FFTW planning adds about 18 ms (general modal) and 120 ms (reduced
comb-to-continuum) of construction. A direct FFTW execution adapter, a pooled allocator
and vector transcendental functions were measured and not needed
(CPU performance).
Threaded FFT plan selection#
FFTW’s measured planning sometimes chose a slower threaded plan: on a 32-core AVX-512 server, for 204 rows of 2048 complex samples at 32 threads, a 64 × 32 decomposition instead of 32 × 64 made the warm general-modal RK window about 12 % slower, almost all of it in complex transforms. Measured wisdom is now seeded only for serial transforms; threaded ones default to estimate planning. Against the previous build, the general modal case’s window then took about 0.80 times as long at one thread and the same at 32 threads; reduced comb-to-continuum stayed within its repeatability.
Low-storage interaction-picture RK#
The low-storage march (four evaluations a step) against the IP-DOPRI5 replay (six, with first-same-as-last) on a Cartesian envelope Kerr beam with no collar, fixed 0.5 µm steps, ten timed steps after one untimed one, same-process comparisons on an 18-core arm64 workstation. Shape A is 192 × 192 × 300 (one state 177 MB), shape B 384 × 384 × 600 (1.42 GB). Ratios are how many times less time the march takes than the replay on the same backend:
backend |
shape, threads |
per step |
per evaluation |
field volumes held, march / replay |
|---|---|---|---|---|
|
A, 1 |
1.83× |
1.22× |
4 / 16 |
|
A, all |
1.77× |
1.18× |
4 / 16 |
|
B, all |
1.73× |
1.15× |
4 / 16 |
Kokkos CPU |
A, 1 |
2.02× |
1.35× |
4 / 15 |
Kokkos CPU |
A, all |
1.81× |
1.21× |
4 / 15 |
Kokkos CPU |
B, all |
1.85× |
1.24× |
4 / 15 |
Recomputing the flow factors instead of tabulating them holds three volumes and costs
3–13 % more. Most of a march step is now the source evaluation’s core (transforms, the
Kerr column, packing): 69 % on numba_cpu at one thread, the fused finish 27 %. At all
threads the Kokkos march was 1.6 (shape A) to 2.1 (shape B) times faster than
numba_cpu’s.
Band-rule plasma integration#
The band-exact plasma integrals (SEP 18) against the trapezoid, on the four-mode RDW window (33 nodes, 8192 fine samples, ten fixed steps from the launch), for the band column as first built:
backend |
threads |
band / trapezoid window |
|---|---|---|
|
default |
1.51–1.58 |
|
1 |
2.4–2.5 |
Kokkos CPU (arm64 laptop) |
18 / 4 / 1 |
1.3 / 1.9 / 2.7 |
Kokkos CUDA (data-centre NVIDIA GPU), direct halo reads |
— |
1.10 |
Kokkos HIP (AMD Instinct GPU), halo staged in team scratch |
— |
1.25 |
On a CPU each integral is a nine-tap filter inside a serial traversal; on a device the
filters are parallel maps over the row, so the band rule costs about a tenth more than
the trapezoid there. Staging the halo in team scratch loses on the NVIDIA device (1.21)
and wins on the AMD one (1.36 with direct reads), so each build defaults to its winner.
numba_cpu’s column has since been rewritten as whole-row passes, 1.7 times faster than
the traversal measured here (algorithms.md). A finer grid, the
alternative SEP 18 rejects, costs about twice the whole nonlinear term for a quarter of
the error.
Dispersive electronic response#
The dispersive Kerr response (SEP 6, backends.md §3.11) is judged by its nonlinear stage time against the constant cubic’s on the same route: the symmetric evaluator’s target is 1.3, the polynomial’s 4. Same-process medians of ten-step windows from the launch, on the RDW grid and on Cartesian, line, planar and general-route problems:
evaluator |
|
Kokkos CPU |
GPUs (NVIDIA and AMD) |
|---|---|---|---|
symmetric, one species |
0.92–1.23 (1.31–1.35 on one averaged node, analytic sector) |
0.81–1.21 at one thread |
0.86–1.30 |
symmetric, mixture (one filter from the mixture’s Kerr diagonal) |
0.90–1.11 |
0.97–1.21 at one thread |
0.86–1.30 |
polynomial, one species or equal leg bounds |
1.16–3.72 |
1.52–3.65 at one thread; 4.2–5.0 on the general route |
1.01–3.53 |
polynomial, mixture of distinct leg bounds |
4.1–7.6 (3.3–3.7 for helium and argon in the default pools) |
3.2–8.3 at one thread, at most 2.2 in the default pools |
within 4 (air 1.5–2.1) |
Ten-step windows, which carry the integrator’s own work, move less than the stage. A rebuild of the mixture filter when the densities change costs about 2 % of a symmetric stage. A polynomial mixture is one group per distinct leg bound, which is why mixtures such as air miss its target on the CPUs.
Far-field statistics pass#
The banded far-field pass (six aperture statistics and the on-axis spectrum, 30 slots)
on the last state of a Cartesian envelope Kerr run (192 × 192 × 256 and 384 × 384 × 512),
same-process comparisons: about 2.3–2.6 % of one Runge–Kutta step on both numba_cpu
and Kokkos CPU, paid at save planes and periodic rows only. On numba_cpu it costs about
as much as the energy statistic and is 30 to 37 times faster than the host pass it
replaced (which cost 77–84 % of a step). On Kokkos CPU it is compute-bound (square roots
and saturated tanh weights), 15–16 times energy.
Raman kernel rebuild#
On a density gradient the Raman kernel is rebuilt at every stage, and its cost scales with
the response’s rows. numpy_ref’s rebuild at \(2^{14}\) fine samples (median of 35,
same-process, a loaded laptop) took 0.31 ms for a two-term N₂ model (rotation and
vibration), 0.47 ms for optmat’s H₂ (4 rows), 6.0 ms for optmat’s N₂ (72 rows) and
10.2 ms for dry air (126 rows), against 0.25 ms for one convolution. With tens of rows
the rebuild dominates the step; Kokkos’s native rebuild is slower than the host’s at one
thread and overtakes it at four. SEP 10’s low-rank
representation is what removes it.
General-route symmetric contraction#
Summing the general route’s real cubic over its ten symmetric monomials instead of the
dense 81 + 3 products (algorithms.md), on a two-channel step-index pair
(288 nodes, 256 samples) at one thread: the respond kernel alone 2.5 times faster on
numba_cpu; a ten-step window 1.45 times faster on numba_cpu and 1.35 times on Kokkos
CPU. The gain shrinks as channels are added.
Resident Adams storage#
At maximum order five the Kokkos resident Adams engine held 29 complex field volumes,
eleven of them dense-output slopes and saved fields allocated even when planes are landed
on. Allocating those only for interpolate_planes=True reduces it to 18
(backends.md §8). Peak RSS of a reduced-resolution
kerr_beam_384 (same box, 600 temporal bins, ten accepted steps, four threads, median of
three fresh processes on an 18-core arm64 laptop), in GiB; the 384² row is a projection
from the other two (constant plus transverse area), not a measurement:
grid |
|
Kokkos, 29 volumes |
Kokkos, 18 volumes |
18 volumes / |
|---|---|---|---|---|
128² |
4.31 |
7.53 |
5.92 |
1.37 |
256² |
15.7 |
28.2 |
21.8 |
1.38 |
384², projected |
34.8 |
62.7 |
48.2 |
1.39 |
Field export adds a further host copy (up to 1.43 times numba_cpu’s peak). No
field-sized workspace scales with the thread count. The allocations are itemised under
reference tables.
Free-space cases at the package defaults#
Per-step time of the free-space cases at the package’s defaults, production thread policy, on a 64-core AVX-512 cloud server (seconds per step, medians of two repeats):
case |
stepper |
Kokkos CPU |
|
|---|---|---|---|
Kerr beam 384² |
march |
0.348 |
0.796 |
Kerr beam 384² |
IP-DOPRI5 |
0.998 |
2.49 |
Kerr beam 768² |
march |
1.51 |
3.34 |
Kerr beam 768² |
IP-DOPRI5 |
4.09 |
10.1 |
Cartesian filament 128² |
default |
0.076 |
0.300 |
Cartesian filament 256² |
default |
0.296 |
1.19 |
The march scales from 384² to 768² as 4.33 (Kokkos) and 4.20 (numba_cpu) against
\(N \log N\)’s 4.30. Against a per-evaluation reference built from the cost of its parts
(transforms and response), the march’s evaluations take 0.94–1.37 times the reference and
IP-DOPRI5’s 1.21 (Kokkos) and 1.80 (numba_cpu)
(SEP 28).
Reference tables#
These tables back the indicative results above.
Kokkos CPU sizing thresholds#
Counts are elements or scalar product work, never allocated bytes. Stages and reductions use the field’s element count; batch updates use the fused batch’s total. Packing uses matrix entries and temporal copies use output samples, without multiplying by the dense transform’s inner dimension. Responses use active columns × components × fine-grid samples. Dense products use output rows × inner dimension × samples; coupled matrix functions instead use active spectral bins × modes³. FFT and Raman choices use each plan’s actual real-side batch dimensions, including a kernel transform and the last partial chunk.
Operation |
Serial below |
Full pool from |
Intermediate rule |
|---|---|---|---|
Stage/update/reduction |
8,192 |
8,192 |
none |
Packing/temporal copy |
65,536 |
65,536 |
none |
Response |
32,768 |
196,608 |
up to four threads; at least 40 component columns or 65,536 fine samples per column also enables the full pool |
Native/custom GEMM |
1,048,576 |
1,048,576 |
none |
General-modal synthesis/projection/maps |
65,536 |
65,536 |
none |
Coupled matrix functions |
32,768 |
32,768 |
none |
Modal/spatial FFT |
32,768 |
131,072 |
up to four threads |
Raman FFT |
65,536 |
262,144 |
up to four threads |
A response column is one active node and one component. Every width is capped by the
global thread setting. Explicit per-pool requests bypass this table, including the
short-FFT rule. build_info() exposes the constants and override mask; backend metadata
exposes effective widths and sizing inputs. Non-OpenMP host builds report that
per-operation sizing is unsupported and keep their existing execution policy.
Resident Adams allocations#
F = N² × 600 × 16 bytes is one complex128 field of the reduced kerr_beam_384 above
and C = 5592 × 600 × 16 bytes one node chunk (a 1 GiB node budget). Each pair is
numba_cpu / Kokkos capacity in GiB; arrows show the 29 → 18 volume change. The
384² capacities are calculated. These are owned allocations, not resident pages.
Allocation |
Field count, |
128² |
256² |
384² |
|---|---|---|---|---|
Adams history ring |
7 / 7 |
1.025 / 1.025 |
4.102 / 4.102 |
9.229 / 9.229 |
Adams moments |
8 / 7 |
1.172 / 1.025 |
4.688 / 4.102 |
10.547 / 9.229 |
Adams flow / propagator |
1 / 1 |
0.146 / 0.146 |
0.586 / 0.586 |
1.318 / 1.318 |
Native Adams source, exponent, saved state |
0 / 3 |
0 / 0.439 |
0 / 1.758 |
0 / 3.955 |
Dense slopes |
0 / 7 → 0 |
0 / 1.025 → 0 |
0 / 4.102 → 0 |
0 / 9.229 → 0 |
Dense start, end, exponent, held observation |
0 / 4 → 0 |
0 / 0.586 → 0 |
0 / 2.344 → 0 |
0 / 5.273 → 0 |
Current state |
1 / 1 |
0.146 / 0.146 |
0.586 / 0.586 |
1.318 / 1.318 |
RK stages |
7 / 7 |
1.025 / 1.025 |
4.102 / 4.102 |
9.229 / 9.229 |
Solution, error, advanced, recycled stage |
4 / 4 |
0.586 / 0.586 |
2.344 / 2.344 |
5.273 / 5.273 |
RK exponent / weighted residual |
1 / 1 |
0.146 / 0.146 |
0.586 / 0.586 |
1.318 / 1.318 |
RK propagators |
2 / 1 |
0.293 / 0.146 |
1.172 / 0.586 |
2.637 / 1.318 |
Transverse FFT scratch |
1 / 1 |
0.146 / 0.146 |
0.586 / 0.586 |
1.318 / 1.318 |
Field-volume subtotal |
32 / 44 → 33 |
4.688 / 6.445 → 4.834 |
18.750 / 25.781 → 19.336 |
42.188 / 58.008 → 43.506 |
Temporal FFT buffers ( |
fixed chunk |
0.200 / 0.200 |
0.200 / 0.200 |
0.200 / 0.200 |
Absorber scratch ( |
fixed chunk |
0 / 0.100 |
0 / 0.100 |
0 / 0.100 |
Statistics spectrum, time, power ( |
fixed chunk |
0 / 0.125 |
0 / 0.125 |
0 / 0.125 |
The remaining ledger entries (reduction partials, axes, spatial coefficients, statistics
outputs, host problem tables, masks, weights and plan heap) total a few MiB. Outside the
ledgers, the input field adds one F; Kokkos keeps another F of host state, and field
export passes through two more F at the Python boundary. Host Adams trials form about
4F of temporary arrays. Capacities and transients do not all become resident at once,
so their sum overstates the measured peak.