SEP 43 — Retire numba_cpu#
SEP |
43 |
Title |
Retire |
Author |
John C. Travers |
Drafting agent |
Claude Opus 5.5 (2026-11-02) |
Status |
Final |
Review status |
Reviewed |
Reviewers |
John C. Travers (human); GPT-6 (AI) |
Type |
Change |
Created |
2026-11-02 |
Resolution |
Accepted 2026-11-02 by John C. Travers, after the maintainer’s human review, GPT-6’s AI review and the response to it; #393 |
Requires |
SEP 29 (compiled backend distribution; final) |
Supersedes |
— |
Superseded-by |
— |
Implementation |
Complete 2026-11-03: John C. Travers, with Opus 5.5, on |
Manual pages |
Part I — Proposal#
1. Abstract#
This SEP removes the numba_cpu backend and solprop’s dependencies on numba and
pyfftw before the 0.2 release, and makes native the only compiled CPU backend. It first
widens the native wheels to the processors and platforms that only Numba reaches in
practice: x86-64 processors below AVX2, on Linux and Windows, and Windows on Arm. The
documentation build and the continuous integration lanes that run on numba_cpu today move
to the native wheel.
solprop-native becomes a required dependency, so solprop’s supported platforms are the
ones native installs on. On any other platform the installer’s resolution error is the
answer, and the installation page explains it. numpy_ref remains the numerical reference
and the executor for a short list of supported features that native doesn’t run yet.
Success means: the final suite is green on all five wheel platforms with no rehearsal filters, a fresh install outside a checkout propagates on each, every gate of §7 passes, and no result moves outside the rounding budgets and accuracy envelopes of §7.
2. Motivation and scope#
Every test, route and kernel of numba_cpu is a second copy of what native does. In the
0.1.0 benchmark record (#68) it is 1.5 to 2.8 times slower than native by geometric
mean, with no winning configuration among the 22 measured on three machines (§3). That
evidence has limits: the Apple series ran on a busy machine, and there are no rows on Windows
or Linux aarch64, or for scans (#314). Its remaining value is reach, and that reach
turns out to be narrow and cheap to replace.
There are no known users. 0.1.0 was published on 2026-11-01 and hasn’t been announced, so the freeze, deprecation and feedback period that SEP 29 §6.3 anticipated would protect nobody we know of, and this SEP does without them (§9).
In scope:
a second, x86-64-v2 build of the extension in the Linux and Windows x86-64 wheels;
a Windows on Arm wheel;
moving the
docs,slow,oldestand coverage lanes to the native wheel;replacing the Numba-compiled helpers that every backend shares (SEP 29 R2);
removing the backend,
numba,pyfftw, and the tests and golden files only they use;a single stable-ABI native wheel for Python 3.14 and 3.15, measured first;
making
solprop-nativea required dependency (§6.7), with one lightweight setup for Python and documentation contributors;a named safeguard for GPU environments (§5, §6.7);
a processor check that refuses when it can’t establish the processor’s features (§6.1, §6.2);
rewriting the README, installation page, guide and messages to say so (§8).
Out of scope:
any further performance work on
numba_cpu. Issues specific to it close at the removal, and shared obligations stay open (§9);wheels for Intel macOS, ppc64le or musl, and processors below x86-64-v2 or Neoverse N1, all of which become unsupported, by choice (§9);
GPU wheels (#317) and CUDA or HIP source builds, which are unchanged;
changes to
numpy_ref’s arithmetic or to any numerics. Its documented role does change (§5);source builds from an archive or offline, which stay with #312, and the native gaps of §5, each of which stays with its own issue.
3. Context#
Backends. numpy_ref is the reference and native the production backend (Tier 1,
ADR-061). numba_cpu is Tier 2, and backend="auto" selects it when
the native package is absent or the processor is below the wheel’s baseline
(src/solprop/backends/registry.py). SEP 29
§6.3 left its removal to a later SEP, which should settle the freeze, feedback and
rollback of its review item R11, the test suite’s cross-backend tests and the two
dependencies. #316 tracks that decision.
Speed. numba_cpu over native in the 0.1.0 history (benchmarks/history/), geometric
mean over the 22 configurations, all at the same step counts:
Machine |
One thread |
Full width |
|---|---|---|
2× Intel Xeon Gold 6240 |
2.19 |
2.84 |
AMD EPYC 7763 |
1.95 |
2.39 |
Apple M5 Max |
1.53 |
2.11 |
These are the configurations measured, not every route and platform (#314).
Reach. What the Numba project’s own distributions add to the native wheels’ four platforms, from PyPI and conda-forge on 2026-11-01. A row with no wheel may still run in an environment assembled by hand, for example solprop installed by pip beside conda-forge’s Numba, which wasn’t tested. Dropping those systems is a support decision, not something forced on us:
Target |
Numba |
solprop on it today |
After this SEP |
|---|---|---|---|
Windows on Arm |
PyPI wheel |
Blocked: pyFFTW has no wheel |
Native wheel |
Intel macOS |
conda-forge only |
No conda-forge package of solprop |
Unsupported |
ppc64le |
conda-forge only |
No conda-forge package of solprop |
Unsupported |
x86-64 below AVX2 |
Compiles for the host |
Runs on |
Native x86-64-v2 build |
Arm below Neoverse N1 |
Compiles for the host |
Runs on |
Unsupported |
Numba’s real advantage is that it has no fixed processor baseline. A second build at x86-64-v2 removes most of that advantage at a measured cost (§6.1).
Features. The admission code and the SEP 29 capability audit (R1, closed by its native
parity phase) show no CPU problem kind that only numba_cpu runs. This is a reading of
those checks rather than a proof over every path; the test retargeting of phase 2 is
where a gap would appear, and any it finds goes into Part II.
Issues. It closes #316. It answers the Windows on Arm half of #317, and the pyFFTW half of #319. §9 lists what happens to the other open issues that name Numba.
4. Theory#
No physics changes. Comparisons follow SEP 25’s distinction: matched discrete computations are held to rounding budgets fixed before the comparison, and adaptive runs to a per-case accuracy envelope against a tighter or stored reference. Equal step counts establish neither. §7 gives the metrics and bounds.
5. Proposed interface#
Installing. pip install solprop installs native, with no extra. On a platform with no
matching wheel, the installer fails to resolve and solprop’s code never runs. The installation
page explains that error and how to report the platform.
Choosing a backend. Native being installed is a packaging policy. A run doesn’t have to
prove that native could execute on this machine, and nothing loads a CPU or GPU runtime just to
enforce the policy. import solprop doesn’t load native, so reading results works wherever
solprop is present.
Request |
Native usable |
Processor below the floor, or its features unreadable |
Native fails to load |
No native package |
|---|---|---|---|---|
Double precision, |
|
Refuses, naming the missing features or the failed check |
The load’s own error, unchanged |
Refuses, saying how to install it |
|
|
As above |
As above |
As above |
TDSE response, |
|
The same |
The same |
The same |
Single precision, |
|
The same |
The same |
The same |
|
Runs; native isn’t consulted |
The same |
The same |
The same |
|
Runs where MLX is installed; native isn’t consulted |
The same |
The same |
The same |
These cases stay distinct: an arbitrary import failure is never reported as an unsupported
platform. auto has no fallback for lack of native, and today’s two auto exceptions, TDSE and
single precision, are unchanged.
numpy_ref. It is the numerical reference backend, and the executor for these supported
features, which remain supported on the supported platforms:
Feature |
Reached by |
Tracked by |
|---|---|---|
The TDSE response |
|
|
|
By name |
|
Single precision away from Apple silicon |
By name |
|
The general route’s delayed response beyond six terms on three components |
By name |
Running it by name on an unsupported platform may keep working, without a support promise. This list isn’t a general waiver: a new feature still follows SEP 0’s parity rule, and joins the list only as an accepted exception.
A safeguard for GPU environments. SOLPROP_REQUIRE_DEVICE is an opt-in requirement on where
a run executes, set in the documented GPU job recipe:
Value |
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, so there is no precedence to resolve, and scan
workers inherit it. It is checked at backend selection, before any state is allocated or a step
taken, by reading the actual build’s build_info().device_backend and device_count, the facts
check_build already uses. A version label is never consulted. 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 auto for a TDSE or single-precision run, because such
a run wouldn’t meet the job’s requirement. The message says to unset it for such a run.
Other settings.
backend="numba_cpu"raises aBackendErrornamingnativeandnumpy_ref, and the release that removed it, in place of today’s unknown-backend message. §9 covers old files.The x86-64 wheels hold both builds and choose at import.
SOLPROP_NATIVE_CPU_VARIANT= x86-64-v2asks for the lower build, for tests and comparisons; a higher build than the processor runs is refused by name, as an unknown name is.solprop_native.build_info()and the benchmark build string report which build is loaded.SOLPROP_NUMBA_*andSOLPROP_PASS_TIMERSgo;SOLPROP_FFTW_PLANstays, since native reads it. Native has Kokkos regions for what the pass timers measured.
6. Implementation strategies#
6.1 x86-64-v2 beside x86-64-v3#
The Linux and Windows x86-64 wheels carry _core at x86-64-v3 and _core_v2 at x86-64-v2,
side by side. solprop_native chooses before it loads and registers the chosen one as
solprop_native._core, so nothing else knows which it is. CPython finds PyInit__core
from the module’s name rather than the file’s, so the second build needs no C++ change, and
auditwheel and delvewheel repair both alike. On Linux both link the one OpenBLAS with
run-time kernel selection. On Windows each links its own fixed target, HASWELL and
NEHALEM, because OpenBLAS’s run-time selection, built by Clang for Windows, gave wrong
complex results and an access violation (native/ci/build-deps-windows.ps1).
When the processor can’t be asked. Today, a missing /proc/cpuinfo makes select_variant
choose the v3 build and baseline_shortfall return nothing (#443), so a lower processor
could reach an illegal instruction instead of the promised refusal. Windows x64 doesn’t have this
gap: IsProcessorFeaturePresent is in kernel32, and it stays the check there, with its known
limits. On Linux, a wheel with a recorded baseline now asks in this order:
/proc/cpuinfo, as today.NumPy’s
__cpu_features__, if that file is missing, unreadable or has no features line. It comes from NumPy’s owncpuidandxgetbvchecks, and hasX86_V2andX86_V3directly. It is imported only at this point, because_cpu.pyotherwise imports nothing.Refuse, before loading, if neither answers. A wrong guess would be an illegal instruction with no Python error, and with two independent checks this should rarely be reached.
The name is private, which is acceptable for a fallback. A test fails loudly if NumPy moves it, and
at run time a missing name is simply no answer. NPY_DISABLE_CPU_FEATURES can only lower the build
chosen or lead to the refusal. On Linux aarch64 this helps less: NumPy’s names cover only part of
Neoverse N1’s list, with no atomics, CRC32 or RCpc, so there a missing /proc/cpuinfo refuses.
On x86-64, naming a build with SOLPROP_NATIVE_CPU_VARIANT is the deliberate way past that refusal,
on the user’s word about their processor. A source build, which records no checked baseline, is
unaffected. This is a standalone fix, needed whatever happens to this SEP.
The alternatives are in §10. Measured cost and correctness are in §7 and Part II.
6.2 Windows on Arm#
The wheel is built on windows-11-arm with Clang on the MSVC ABI and the runner’s LLVM
libomp, as on x64. FFTW’s CMake build has no NEON option, so the build recipe patches it in.
OpenBLAS is built for NEOVERSEN1. Kokkos and Kokkos Kernels each pass the x86-only
-mcx16 to Clang on Windows, and are patched at fetch.
The core keeps the compiler’s generic Armv8-A baseline, but that doesn’t cover the whole wheel:
it statically links OpenBLAS built with TARGET=NEOVERSEN1 and DYNAMIC_ARCH=OFF, and the merged
wheel has no processor check on this platform. So _cpu.py gains one, using Windows’s
PF_ARM_* feature queries, for what the linked libraries were built to use, and the installation
page’s “any processor that runs Windows 11 on Arm” gets replaced by the requirement that check
enforces. The instruction requirements and the lowest Windows hardware admitted are recorded in
Part II. If a supported device would fall below it, the alternative is a lower OpenBLAS target,
with its cost measured.
6.4 Tests#
A filename doesn’t show that a test is disposable. test_numba_band_bounds.py, for example,
holds an operand-based absolute budget for the plasma column, including cancellation and
pre-ionisation. So phase 2 delivers an inventory, in Part II, of each retiring test family: the
surviving reference or native test that carries its obligation, or the reason it exercises only
the deleted implementation. Scientific invariants, pathological inputs and independently derived
bounds survive. Numba’s cache and dispatcher mechanics don’t.
Native-against-Numba comparisons move to numpy_ref, which about 40 of the test modules
importing numba_cpu already use as well. A Numba-against-reference comparison moves to native
against the reference, never to a comparison of an implementation with itself. The 4 modules that
run only on Numba are ported. The 28 test_numba_* modules and 7 Numba golden files go once the
inventory has accounted for them. Parametrizations drop the numba_cpu value. Convergence records
and benchmark readers keep the rows Numba wrote, as evidence of what it measured, through
RETIRED_BACKENDS in tests/test_convergence_records.py, as they already do for numba_cuda.
Coexistence stays. Removing pyFFTW from solprop’s dependencies doesn’t remove it from users’
environments, and a notebook can still import it before native, which macOS refuses
(#315). native/ci/coexistence.py keeps running in a dedicated environment with NumPy,
SciPy, pyFFTW and Numba installed as outside packages, with numpy_ref replacing its numba_cpu
run. The refusal stays until the order is shown to be safe, its advice stops naming numba_cpu,
and it stays distinct from an unsupported processor. Shared host BLAS and resource management
stay too, with threadpoolctl, since NumPy, SciPy and third-party pools remain.
Coverage. Python’s 100 % gate stays and applies throughout. It says nothing about which C++ capabilities the surviving tests reach, now that production runs only there, so phase 3 commissions one informational native coverage run over the migrated suite, using the build option #324 describes. It has no percentage gate. Its product is an account in Part II of the capabilities not exercised, and the analytic and parity checks that survive.
6.5 Documentation and continuous integration on native#
Moving these lanes was rehearsed before anything was removed (Part II). The rehearsal doesn’t replace the final gates of §7:
Lane |
Today |
After |
Rehearsal |
|---|---|---|---|
|
|
CI-built wheel |
Passes under |
|
|
CI-built wheel, |
164 passed, 1 skipped, 22.5 min on the default runner, including the statistics cost budget |
|
|
Lowest dependencies, wheel installed |
All 3 shards green |
|
Native wheel, driver tests on |
Native wheel, driver tests on |
Report-only on the whole default lane: 99.87 % of what stays, with 44 lines and 32 branches missed (Part II) |
New: |
— |
No native package, a few default-lane modules on |
Measured locally, before |
The wheel on documentation-only changes. native-wheel runs only when lanes == full
today, and docs runs on documentation-only changes as well, so native-wheel must run
for those too, on Linux only. It is cached by the hash of the native sources, so nearly
every run reuses a wheel. A cold Linux build takes 13 to 22 minutes. A documentation-only
change pays that only when the last change to native/ merged without a push to main
building it: such a merge runs no lanes, so it leaves no cache for other pull requests.
Read the Docs stays free of native. It executes no gallery script, because all gallery
output is committed (.readthedocs.yaml, docs/conf.py). It needs only a clean build after
the removal, and its install gains --no-install-package solprop-native, as the lightweight
setup does (§6.7).
What ADR-061 said. Its consequence “the documentation build installs no native backend:
a lesson or tutorial of a feature that numba_cpu does not run names numpy_ref” no
longer holds for the CI lane, and the new ADR supersedes it. A lesson may now use anything
native runs.
Committed output. plot_11_dispersive_kerr and plot_05_moving_solid_guide select
numba_cpu explicitly. They move to auto unless they teach something specific to a backend,
which reduces later documentation churn without bringing back a fallback. Four more gallery
scripts mention it in prose. Regenerating their committed output is part of the removal, since
the build refuses stale output. poe docs-examples runs both on native on the 4-core runner:
plot_11 in 16 s and plot_05 in 884 s, which is a one-off cost when that script changes. The
removal compares their regenerated figures and printed values with the committed ones.
Plain uv sync. In a checkout it builds native, and there is a lightweight alternative
(§6.7).
6.6 Python 3.15#
The native package moves to CPython’s stable ABI: nanobind’s STABLE_ABI, with
wheel.py-api = "cp314" in scikit-build-core. There is then one wheel per platform, tagged
cp314-abi3, and no build per interpreter, which is what makes a new Python cheap. Nothing in
the bindings goes beyond that ABI, so no binding code changes. Part II has the measurements:
abi3audit --strict is clean on all five wheels, CPython 3.15 loads each, and the wheel costs
nothing against the version-specific one.
ABI compatibility isn’t Python support. These are ordinary CPython wheels: free-threaded CPython has a different ABI and isn’t a target. The tested interpreters are 3.14 and 3.15, and a later version may load the wheel with no promise made. An audit and an import don’t qualify the adapter, solprop, NumPy, SciPy, h5py and the TDSE provider together, so 3.15 stays a qualification lane, not a supported version, until the installation page can claim it, which needs h5py’s wheels. It needs:
A short run on 3.15 on each platform: a real propagation, not only a load, in the
python-3-15job. Where h5py has no wheel, that run uses the native package directly.h5py is the one runtime dependency without a 3.15 wheel (h5py issue 2993). It builds from source against a system HDF5, as the Linux lane does, so the installation page says that until h5py publishes wheels, and a 3.15 lane proves solprop in the meantime.
The TDSE provider.
driver/tdse.pyrefuses Python other than 3.14, the version Pyroraptor is pinned to. That stays, and TDSE needs 3.14 until the provider moves. Fourtest_tdse_bindingtests are gated on it.Source builds on 3.15: #399 drops
EXACTfromfind_package(Python 3.14), and one source build on CPython 3.15.2 has built and loaded (Part II).
This doesn’t block the removal, and the removal doesn’t wait for it, though the removal clears Numba and pyFFTW out of its way. The planned Linux suite on 3.15 runs without fake dependencies once they have gone.
6.7 Native as a required dependency#
These are the maintainer’s decisions of 2026-11-02. They replace the fallback that earlier drafts proposed, and the refusal-only wheel that a later draft proposed.
The dependency. solprop-native==<version> moves from the native extra into dependencies,
with no environment markers, which can’t see the C library or the macOS version in any case. In
pyproject.toml the entry is the bare name, so uv resolves it to the workspace member, and
hatch_build.py writes the exact version into the built metadata. A probe of that hook gave the
exact pin in both the wheel’s and the sdist’s metadata (Part II). It changes a statically listed
field, which hatch allows and the metadata standard doesn’t intend, so if hatch stops allowing it
the list moves behind dynamic.
Unsupported platforms. solprop is a pure-Python wheel and native has no sdist, so no solprop
code runs during pip install, and the installer reports only that no matching distribution
exists. The installation page shows that error, says what it means, lists the supported platforms,
and gives a one-line command that prints the platform’s details to include in an issue. Runtime
diagnostics stay wherever solprop can execute them: a processor below the floor and a broken
installation each get their own message, and the first also names where to report it.
Exact pins and development. The exact pin is a contract for released installs. A checkout has three states, and only the first satisfies it. The other two are explicit development exceptions:
Setup |
Native |
Metadata |
|---|---|---|
|
Built from this tree |
Consistent: a workspace member, at the same version |
The lightweight setup, below |
None |
Unmet, deliberately |
|
CI’s wheel from this commit or a compatible ancestor |
Mismatched, deliberately: |
A Python-only commit changes the VCS-derived version, so an ancestor’s wheel can’t satisfy the pin,
and it isn’t relabelled to pretend otherwise: its build_info() keeps the commit it was built from.
So poe fetch-wheel becomes strict. It takes an artifact only if the difference between that
artifact’s commit and the working tree, including uncommitted changes, touches neither native/
nor the solprop modules that solprop_native imports. A test derives that module list from the
imports, so it can’t go stale. Anything else is refused, not warned about, and so is an expired
artifact; each refusal names the two alternatives. At run time, check_build’s comparison of
KERNEL_SCHEMA_VERSION remains the seam’s guard, since solprop’s version is recorded and never
compared. A later plain uv sync or uv run builds native in its place.
uv sync compiles. A plain uv sync in a checkout builds native, since native/ is a required
workspace member, and uv sync --extra native goes, with the extra. A first sync in a fresh worktree
took 104 s at 12 jobs on a machine where FFTW and OpenBLAS were already built, which isn’t a
first-install cost (Part II). Without the C++ toolchain, the sync stops with CMake’s message naming
the compiler, which also comes to name the lightweight setup.
The lightweight setup. One documented command serves Python and documentation contributors with no native package and no compiler:
uv run --no-project tools/setup_light.py
It runs uv sync --locked --no-install-package solprop-native and says how to continue. This is a
development workflow, not a production fallback. It needs two facts explained, because uv can’t be
configured otherwise: uv run has no flag to skip a package, and uv sync ignores UV_NO_SYNC. So
a later plain uv run would build native, and the contributor runs tools with the environment
activated, or with UV_NO_SYNC=1. There, auto refuses, tests take
SOLPROP_TEST_BACKEND=numpy_ref, and native-only tests skip. It supports the reference backend and
the lint, type, test and documentation checks, and a native change still needs a native build and
its qualification. Automation that needs no local build, such as Read the Docs and the lanes on CI’s
wheel, uses the same two settings.
Source and GPU builds. Reinstalling or upgrading at the same version keeps a source build. A new release replaces it with that release’s CPU wheel, with no message, because the exact pin asks for it (Part II). Python has no finished standard for this: Wheel Variants (PEP 817) is still a draft, so it isn’t relied on here.
A released source build takes a PEP 440 local label, set by
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SOLPROP_NATIVE=0.3.0+cuda.==0.3.0accepts it, and it shows inpip list. A real build produced that wheel and version.A development build is already labelled with its commit, and a pin that has a local label is matched exactly, so it takes no further label.
build_info()says what it is.The label is provenance, not evidence. The same probe labelled a CPU build
+cuda, and it reporteddevice_backendnone. That is why the safeguard of §5 reads the build itself.Upgrading means rebuilding from the new tag, as
building-native.mdsays, and the GPU job recipe setsSOLPROP_REQUIRE_DEVICE, so a job that lost its GPU build stops before it starts.
mlx is unchanged: its platforms are within native’s, native is installed beside it, and it
loads only if selected.
6.8 Layer, contract and cost#
This is a backend and packaging change. KERNEL_SCHEMA_VERSION doesn’t change, and no native
kernel changes. The shared helpers’ change is qualified under §6.3. Wheels grow, Linux from 16.5 to 19.2 MB and Windows from 5.2 to 9.7 MB. The Windows on Arm
wheel is 4.7 MB. solprop drops two runtime dependencies, so a default Linux x86-64 install
downloads native’s 19.2 MB in place of about 64 MB for Numba and llvmlite (PyPI, 2026-11-02),
and everyone gets the fast backend. In the release wheels run, the
Windows on Arm build takes about 15 minutes and its default lane about 30. The x86-64
builds stay within the 19 to 30 minutes that they vary over on the hosted runners (22 on
each in a recent run without the second build). The forced-v2 lanes add about 50 minutes
each, and they run only in that workflow, not in ci.yml.
7. Verification and validation#
Every row is a gate on the removal and on Final. Reporting a failure is a phase report, and
doesn’t replace passing.
How results are compared. Equal step counts aren’t a criterion: they can hide a wrong field, and a harmless rounding difference can change an adaptive decision.
Field: the relative L2 difference of the final complex field, \(\|a-b\|_2/\|b\|_2\), as in
benchmarks/ab.py; dimensionless, and absolute in the field’s units if \(\|b\|_2=0\).Observables: relative difference of the pulse energy, and the in-band relative L2 “shape” distance of the spectrum (
accuracy.md).Matched discrete computations (class R): the
accuracy.md§7 ladder and its budgets, fixed beforehand from operation counts, are run unchanged.Adaptive runs (class T): each candidate’s error \(E\) against a reference at
rtol/10 on the v3 build at the same cases, within a safety factor of 2 on the v3 build’s own error at the samertol. The factor is fixed now, before any candidate is measured.Work counts are recorded and reported as a diagnostic, and aren’t an equality gate. Where two runs happen to take the same step sequence, a field difference above 1e-10 is flagged for explanation.
Performance is a separate criterion: median times with the A/A spread and
construction_s, against the last history, with the verdict ofbenchmarks.md.
Claim |
Check |
Bound |
|---|---|---|
The final suite runs everywhere |
The default lane on all five wheel platforms from the release wheels, with no empty |
Green |
Forced v2 and slow lanes |
v2 forced on Linux and Windows x64; the slow lanes on the wheel (#313) |
Green |
No silent skips |
A job fails on any native skip not on the list of expected ones (#313) |
Exact |
Fresh installs |
|
Runs |
Minimum systems |
With no compiler, a smoke test and a short run on macOS 14, a |
Runs, or the claim narrows |
Windows on Arm’s open items |
The Part II list, including |
No unexplained excess |
Selection follows §5’s table |
Each row and column, with a real wheel and no native package, plus a below-floor processor and a forced load failure; messages stay distinct |
Exact |
No package in practice |
With no native package: import, read a saved result, run |
Runs |
v2 selection |
Intel SDE: Haswell gets v3; Nehalem and Sandy Bridge get v2 and |
Exact |
Unreadable processors |
With |
Exact |
Windows on Arm’s floor |
The new check’s cases, and the recorded instruction requirements |
Exact |
v2 is correct |
Default lane with v2 forced (class R); the tracking set against its reference (class T) |
Class bounds |
v2’s cost is acceptable |
Tracking set, Xeon Gold 6240 and EPYC 7763, interleaved fresh processes; native v2 against Numba compiled for Nehalem |
v2 is not slower than Numba at v2 |
Windows v2’s BLAS |
Coupled TE/TM at one thread and the radial real GEMM, which failed under OpenBLAS’s run-time selection on Windows |
Green with v2 forced |
A numba-free tree |
Fresh environment: |
Runs |
Helpers |
§6.3’s budgets and |
Budgets |
No retired obligation lost |
The inventory of §6.4 accounts for every retiring test family |
Complete |
Coverage |
The 100 % gate on |
Holds; run reported |
No regressions |
|
Class bounds; performance verdict |
The metadata |
The built wheel and sdist each carry the exact pin, through the real hook; a checkout’s install succeeds |
Exact |
The workspace |
Plain |
Exact |
The lightweight setup |
No compiler and no native import on the machine: the command, then lint, types, tests on |
Runs |
|
A Python-only commit outside the imported modules is accepted; a seam change, a native change, a dirty tree in those paths and an expired artifact are each refused |
Exact |
A real labelled build |
|
As §6.7 |
The GPU safeguard |
Each value against a CPU build, by name and through |
Exact |
Old runs |
§9’s continuation cases, from files written by the published 0.1.0 |
As §9 |
Coexistence |
|
Green |
Removal is complete |
A search finds Numba only in Final SEPs, convergence provenance, the coexistence environment and the tombstone |
Exact |
Python 3.15, as a qualification lane |
The native wheel loads and runs a short propagation on all five platforms; Linux default lane with no fake dependencies; 3.14 against 3.15 by class R and class T |
Green; class bounds |
8. Documentation#
The documentation has to describe the new position throughout: solprop runs where native
installs, and numpy_ref is the numerical reference, plus the executor of a short list of supported
features. That is more than removing numba_cpu.
For users:
README.md: the install block becomespip install solprop. “Fast CPUs and GPUs” loses the fallback paragraph, thenumba_cputier and the line for a platform with no wheel.introduction/installation.md: the platform and processor tables become the definition of what is supported. It shows the installer’s error on any other platform, what it means, and how to report the platform, and it says that no extra is needed.guide/performance/backends.md:nativeis the backend,mlxthe special case on Apple silicon,numpy_refthe reference and the executor of the list in §5, which it carries. It has §5’s selection table, with no tiers or fallback.guide/performance/native.md,building-native.mdandtuning.md, andnative/README.md: the two builds and the setting, a source build as the way past a wheel’s limits, no extra and no fallback, labels, rebuilding on upgrade, and the GPU job recipe withSOLPROP_REQUIRE_DEVICE.The other guide pages: about 50 files under
docs/guidenamenumpy_ref. Each is reworded so that it reads as the reference, except where it is the supported executor, which says so as a limit of native. TDSE science in particular isn’t described as a development activity.Tutorials: 11 of the 82 gallery scripts pass
backend="numpy_ref". Each moves toautowhere native runs it and it teaches nothing specific to a backend, and keeps its explicit choice where it needs one. Moved output is regenerated and compared, as §6.5 describes. The time this takes isn’t measured yet.Messages: the strings in
solpropandsolprop_nativethat offernumba_cpuorsolprop[native], including those indriver/simulation.py,backends/registry.py, the macOS refusal andCpuBaselineError, follow the same wording, and so does the single-precision message that callsnumpy_ref“not a production backend”.
For contributors: CONTRIBUTING.md, AGENTS.md, native/AGENTS.md, dev/contributing.md,
dev/testing.md, dev/tooling.md, dev/backends.md, dev/benchmarking.md,
dev/dependencies.md and dev/releasing.md lose numba_cpu and --extra native, and state that a
plain uv sync compiles native, what the lightweight setup supports and its limits, and when
poe fetch-wheel is refused. The comments in pyproject.toml and hatch_build.py follow.
A fragment in changelog.d/ records the removal and the required dependency as a breaking
change. Final SEPs stay as history.
9. Compatibility and impact#
SEP 29’s R11 milestones, answered directly. There is no freeze and no feedback period, because
there are no known users to give feedback: 0.1.0 is unannounced. numba_cpu is removed before
0.2. 0.1.0 is therefore the last release with it, and 0.2 ships without Numba. If the gates of §7
aren’t met, the removal doesn’t merge and 0.2 waits, or the maintainers amend this SEP. Rollback is
pinning solprop==0.1.0, which works immediately. Reverting the removal once 0.2 is published
requires a new tested release. This overrides the advice in #316 to wait two minor releases,
and is recorded as the maintainer’s decision.
Accepted losses:
Who |
Before |
After |
|---|---|---|
Intel macOS, ppc64le, musl, older glibc, macOS before 14 |
|
Unsupported, by choice: the installer’s error. A source build of native may still work, with no support promise |
Arm below Neoverse N1, Windows before version 2004 |
|
Unsupported, by choice: |
x86-64 below x86-64-v2 (Intel before 2008, AMD before 2011) |
Nothing from wheels: NumPy 2.5.2’s own baseline is |
Unchanged. Native’s v2 build sits at NumPy’s floor |
Any CPU inside a GPU build |
|
A second environment with the CPU wheel, a real workflow cost, since their admission limits differ (for example #363). |
A plain |
|
|
x86-64 below AVX2 |
Numba compiled for the host |
Native v2, in 0.44 to 0.51 of Numba’s time |
Old runs. Native and numpy_ref can continue a Numba run, with the script’s or recipe’s backend
changed deliberately:
What |
After the removal |
|---|---|
Reading an output, a snapshot or a scan’s execution metadata |
Works, and keeps saying |
A script or recipe that names |
Refused by name, saying what to use. The backend is an execution resource, so changing it doesn’t change the run’s identity (SEP 42) |
Continuing with the backend changed |
Goes through SEP 42’s normal compatibility checks. The original execution record stays, and the new one is added |
Adams history in a snapshot |
Crosses to the new backend, except a frozen-residual coupled flow’s: a host engine’s history continues on |
Anything refused |
Recovered with |
Phase 3 tests a real output, snapshot, recipe and short adaptive continuation, from files written by the published 0.1.0, under class T of §7. It covers the Adams and frozen-residual cases, and that the original record survives. Anything that turns out not to continue is documented as a refusal here.
Open issues. About 80 open issues mention Numba, and phase 3 audits them all. A first pass from their titles, except where noted:
Treatment |
Issues |
|---|---|
Closed at the removal, as specific to the deleted code |
|
Numba’s part removed, the rest kept |
#104, whose Python Adams march remains, which was read; #7, #281, #344 and #314 |
Kept |
Shared physics and performance work; reference-only gaps such as #304; #315 |
Other effects.
Validated results are unaffected:
numba_cpuwas never the reference.No difference from Luna.jl.
KERNEL_SCHEMA_VERSIONand the output schema don’t change.The
nativeextra goes. Installers ignore an unknown extra with a warning, sopip install "solprop[native]"should still install solprop and native.Each solprop release now requires its native wheels, including for a change that touches only Python, because of the exact pin. They are already released together (SEP 29).
A native packaging or load failure is now a complete outage on that platform, which is why §7 qualifies the wheel as the only production path.
10. Alternatives considered#
Keep
numba_cpuin Tier 2 or move it to Tier 3. It stays cheap only while nothing changes the seam; every kernel-contract change and every parity test still pays for it, and its reach is what §6 replaces.Lower the single wheel to x86-64-v2. Simple and one build, at 19 to 25 % by geometric mean, and up to about 50 % at one thread, for everyone with AVX2, to serve processors without it: mostly from before 2013, and later low-power Atom and Pentium parts. Rejected.
Runtime dispatch inside one build. The pool of kernels spread through Kokkos, SLEEF and FFTW makes it far more invasive than a second build, for a smaller wheel only.
MKL on Windows x86-64. MKL runs below AVX2 and would settle the BLAS question for Windows v2, as it was already kept for local builds in SEP 29 (it has no aarch64 or macOS builds). Bundling it means a large binary, Intel’s licence, and a third BLAS in the distribution matrix while Windows on Arm still needs OpenBLAS. Rejected by the maintainer.
autofalls back tonumpy_refwith a warning. Earlier drafts proposed this, and it keeps first-run scripts working. It also leaves a supported-looking path that is 6 to 40 times slower than threaded native, and it hides the platforms worth knowing about. Rejected by the maintainer.Native as an extra, or a dependency with environment markers. An extra leaves the default install without the backend solprop is built around. Markers can’t see the C library or the macOS version, so those systems would still fail in the resolver.
A plain
uv syncwith no native, through a uv override. It keeps every first sync compiler-free, and a probe showed it working, but a checkout then differs from an install by default. Rejected by the maintainer: anyone in a checkout is developing, and the lightweight setup covers those who aren’t working on native.A refusal-only
solprop-nativewheel for unsupported platforms. A small pure-Python wheel at the same version, which installers take only when no platform wheel matches, could print a helpful message at run time. Rejected by the maintainer after review: wheel preference only answers a fresh resolution, so a stub already installed survives an upgrade once a real wheel appears, and “solprop-native is installed” would have gained a second meaning for a nicer error.Refusing at
import solprop. It would break the documentation build,numpy_refby name and reading results on an unsupported machine.Sending the platform report automatically. Nothing leaves the user’s machine unless they submit it.
Other ways to read the processor when
/proc/cpuinfocan’t be read. Defaulting to v3 risks an illegal instruction with no Python error.numpy.show_runtime()is the public view of NumPy’s features, but it prints and returns nothing, so it would mean parsing text meant for reading, and it lists only dispatch targets. A small C probe, built for the lower baseline, avoids a private name at the cost of a compiled file in all five wheels for a rare case;_corecan’t check for itself, being built for v3. A third-party library such as pytorch/cpuinfo adds a dependency for one check.Leave Windows v2 out until someone asks. Considered first. The fixed
NEHALEMOpenBLAS costs one more dependency build and passes the suite, so the exception is not worth keeping.
11. Phases#
SEP 0 puts every phase of an accepted SEP on one branch and one pull request, and exempts tooling, continuous integration, tests and bug fixes that stand on their own (§2). This SEP keeps to that:
Already delivered, standing on their own. These are useful whatever happens to this SEP (Part II has their commits and runs):
Two x86-64 builds and Windows on Arm wheels, #395, merged.
The stable-ABI wheel, #399, on its own pull request, and the fixes that came from this work.
Standalone, before the removal. These are bug fixes:
The processor check of §6.1 (#443) and §6.2.
On feature/sep-0043-retire-numba-cpu, the SEP’s one pull request (#415). It already merges the
drafts #394, #398 and #401, which close unmerged once it carries their content. It takes each phase
in order:
Lanes.
docs,slowandoldeston the wheel,native-wheelon documentation-only changes, theno-nativelane, the strictpoe fetch-wheel.Helpers and tests.
helpers=NUMBAto plain with §6.3’s budgets and the import-blocked test. Comparisons with Numba retargeted, with the inventory of §6.4.Removal. Delete the backend,
backends/bodies.py,_numba_cache.py, and the tests and golden files the inventory has accounted for. Registry tombstone. Remove the dependencies and regenerateuv.lock. The required dependency, the lightweight setup, the selection of §5 andSOLPROP_REQUIRE_DEVICE. Old-run tests and the issue audit of §9. The informational native coverage run. Documentation, as §8 lists. New ADR superseding ADR-061’s Tier 2 and its statement on the documentation build. A changelog fragment. Every gate of §7 passes beforeFinal.
After the removal, standing on its own. The Python 3.15 lane is continuous integration, and doesn’t hold up the removal or 0.2.
Order against 0.2. The removal merges before 0.2, so 0.2 ships without Numba. The required
dependency lands with the removal and not before, since until then numba_cpu is still the
fallback. Until then, a plain install on Windows on Arm has neither pyFFTW nor native, and auto
picks numba_cpu and fails with an ImportError. Installing solprop[native] avoids that. No 0.x
release is expected to carry that state, because 0.2 follows the removal.
12. Open questions#
None. All were settled by the maintainer on 2026-11-02, and these sections give the result:
Q1. What
autodoes without native. It refuses, with the two existing exceptions of §5.Q2. Whether native becomes a default dependency. It is required, with no markers (§6.7).
Q3. Features that run only on
numpy_ref. They stay there and stay supported on the supported platforms (§5). There is no supported solprop install without native, even where a feature uses onlynumpy_ref.Q4. What
uv syncdoes in a checkout. It compiles native, and one lightweight setup serves those who don’t need it (§6.7).Q5. Source builds beside the exact pin. They are labelled when released, rebuilt on upgrade, and GPU jobs can require a device with
SOLPROP_REQUIRE_DEVICE(§5, §6.7).
13. References#
Issues and pull requests of the solprop repository:
#68, the 0.1.0 benchmark record.
#316, the decision on
numba_cpu.#317, Windows on Arm and GPU wheels.
#319, Python 3.15; #92, the question of capping
requires-python.x86-64 microarchitecture levels: System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0.
PEP 440, version specifiers and local labels, and PEP 817, Wheel Variants, a draft.
#313, qualification on clean minimum systems; #324, native coverage; #312, source archives; #443, the processor check; #315, pyFFTW on macOS.
Part II — Implementation record#
What has been delivered, and which evidence applies to what — 2026-11-03#
The maintainer accepted the SEP on 2026-11-02. The phases below were written before most of this.
Item |
Pull request |
State |
Evidence |
|---|---|---|---|
Two x86-64 builds, Windows on Arm wheels |
#395 |
Merged at |
Wheels run 41483897780 on |
Windows on Arm scan workers |
#430 |
Merged at |
Scans on the Arm runner, in its description |
Installation page’s pointer to that warning |
#441 |
Merged |
Documentation lanes |
Native refusal fixes |
#419 |
Merged |
Full CI |
Stable-ABI wheel, 3.15 source builds |
#399 |
Merged |
Its CI; a source build on CPython 3.15.2 on the Xeon |
Lanes on the wheel, |
#398 |
Closed into #415 |
#415’s CI |
Shared helpers |
#394 |
Closed into #415 |
§6.3’s budgets, in the report below |
Tests retargeted |
#401 |
Closed into #415 |
The inventory, in the report below |
The removal, and all of phases 1 to 3 |
#415 |
This pull request |
The report below |
Which numbers belong to which build. The v2 cost, accuracy and stable-ABI measurements in the phase 0 report come from the spike branch, and they are exploratory. The report on phases 1 to 3 measures the final tree, except v2’s cost against Numba, which has no Numba left to run against.
Phase 0 report — the coverage spike#
Scope: 2026-11-01 to 2026-11-02, Claude Opus 5.5 for John C. Travers, on the unmerged branch
spike/native-coveragefrom05ee1f3. The branch itself is never merged; the table above says what has landed from it. It was run before the proposal was written, to find out whether the proposal was possible.Delivered, as a working branch: all five platforms build stable-ABI wheels through
wheels.yml, with two extensions in the x86-64 wheels. New lanes force v2, rehearsedocs,slowandoldeston native, and run 3.15.Verification on hosted runners:
Lane
Result
Windows on Arm wheel
Built and repaired, 4.7 MB; FFTW with NEON, OpenBLAS
NEOVERSEN1with OpenMPDefault lane on
windows-11-arm, native (see below)15,478 passed; every one of 234 failures and 10 errors stops in
numba_cpuat the emptypyfftwDefault lane, v2 forced, Linux
17,902 passed, 44 skipped
Default lane, v2 forced, Windows
17,824 passed, 122 skipped, which includes the two known cases of §7
Intel SDE
Haswell gets v3; Nehalem and Sandy Bridge get v2; Penryn is refused with status 1
docson native§6.5
slowon native164 passed, 1 skipped, 22.5 min
oldeston nativeAll 3 shards green
Stable ABI
abi3audit --strictclean on all five wheels; CPython 3.15.1 loads eachDefault lane on Linux, Python 3.15, native
15,528 passed; failures as on Windows on Arm, plus 4 at the TDSE pin (§6.6)
pyFFTW has no Windows on Arm or 3.15 wheel, so these two lanes install an empty
pyfftwand pass-k "not numba", so that modules which importnumba_cpuat the top still collect. What remains reaches Numba only through tests with nonumbain their names. These lanes will be exact once phase 2 retargets those tests.What Numba’s retirement leaves untested (report-only, EPYC 7763, the default lane on the wheel with
SOLPROP_TEST_BACKEND=native, an emptypyfftwand-k "not numba", coverage omittingnumba_cpu,bodies.pyand_numba_cache.py): 15,559 passed, 234 failed and 10 errors, all stopping at the emptypyfftw, in 31 modules. The numbers match Windows on Arm’s. 150 of the 244 are in three modules. Statement and branch coverage was 99.87 %, missing 44 of 45,046 lines and 32 of 13,374 branches in 18 files. The count is before phase 2 ports those tests.The default lane with no native package (report-only, EPYC 7763, the driver tests on
numpy_refand-k "not numba"): 14,355 passed in 4.1 min on 48 workers, about 85 CPU minutes. Its 3 failures and 144 errors are from the copied tree having no git history and the node’s allocation, and none comes fromnumpy_ref. That is too large for one more lane of every change, sono-nativeholds a few modules.What native doesn’t reach (found while rewriting the documentation in phase 3, from reading the code, not by running each): the general route’s delayed response is limited to six terms on three components, beyond which a problem runs only on
numpy_ref; Adams beside a coupled flow falls back to the slower Python march above 16 channels or off the eigenbasis route, with aHostMarchWarning; and native has no separate override for its transverse FFT threads.Cost of v2 (the tracking set, 22 configurations, length 1.0, 3 rounds, fresh processes interleaved, 16 threads pinned to one socket; every configuration took the same steps):
Comparison
Xeon Gold 6240
EPYC 7763
Native v2 over native v3, geometric mean
1.246
1.193
The same, at one thread / at 16
1.34 / 1.17
1.30 / 1.11
Native v2 over Numba compiled for Nehalem
0.444
0.511
The same, slowest configuration
0.931
0.971
Numba for Nehalem over Numba for the host
1.034
1.013
Native at v2 beats Numba at v2 in all 22 configurations on both machines. Numba loses so little to the lower target because FFTW chooses its kernels at run time and Numba’s own kernels vectorise little.
Accuracy of v2. Final fields against v3 are at or below 9.2e-11 on every configuration, except
he_rdw_averagedat one thread on the Xeon, at 3.1e-10. That case andctchave a floor that high: two v3 builds differing only in their Python ABI differ by 3.8e-10 and 2.0e-10 on them, so this is not a property of the v2 build.Cost of the stable ABI.
cp314-abi3overcp314-cp314, both v3 on Python 3.14, on the EPYC: geometric mean 1.0017, same steps. The same wheel on CPython 3.15.2 over 3.14.7, with the same NumPy and SciPy, on the Xeon: geometric mean 1.0004, same steps, fields at or below 1.3e-10.The required dependency, probes (2026-11-02). Each is a trial on a disposable worktree from
main, on the Xeon Gold 6240 with GCC 16.2, with pip 26.2.2 and uv 0.12.7 where an installer is named. None of it replaces §7’s checks.Probe
Result
The hook writes the pin over a bare name in
dependenciesAccepted by hatch;
Requires-Dist: solprop-native==0.1.1.dev285+gff2937193.d20261102in the wheel and in the sdist’sPKG-INFOFirst plain
uv syncin a fresh worktree, 12 jobs, with FFTW and OpenBLAS already built104 s. A second sync took under a second, and
uv runthen foundautoisnativeThe same with no C++ toolchain in the environment
Stops at CMake’s message naming the compiler
uv sync --no-install-package solprop-native, with no compiler or CMake onPATHNo native package; 493 tests passed, 6 skipped on a sample of modules;
poe typesclean, 270 s. Numba was still present, and the documentation build wasn’t triedNumPy’s
__cpu_features__on 2.4.4, 2.5.0 and 2.5.2, on this XeonX86_V2andX86_V3present and true. WithNPY_DISABLE_CPU_FEATURES=X86_V3, false. This machine supports both, so the masked SDE run of §7 is the real test, and Windows wasn’t runA plain
uv runafter that syncTries to build native, and
UV_NO_SYNC=1avoids it.uv syncignoresUV_NO_SYNC, anduv runhas no flag to skip a packageA real
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SOLPROP_NATIVE=0.3.0+cudabuildsolprop_native-0.3.0+cuda-*.whl, version0.3.0+cuda, accepted by==0.3.0, 105 s. It was a CPU build, and reporteddevice_backendnone,device_count0 and its true commitResolver behaviour, with fake wheels and each platform simulated by its tags:
Installed
Action
pip
uv
1.0+cudaInstall or
-Usolprop 1.0 (pin==1.0)Kept, also with
--upgrade-strategy eagerKept
1.0, no label-Usolprop 1.0Kept, also with eager
Kept
1.0+cuda-Uwhen solprop 1.1 is releasedReplaced by the 1.1 wheel, with no message
The same
A refusal-only wheel was also probed: installers took it only where no platform wheel matched. The review’s further probe found it survives an upgrade once a real wheel appears, and it was dropped (§10).
Deviations from the first plan: the builds sit side by side as
_coreand_core_v2rather than in subdirectories; the Windows on Arm core keeps the generic Armv8-A baseline, so it needs noPF_ARM_*check.Sources checked: Numba, pyFFTW and h5py availability against PyPI and conda-forge on 2026-11-01 and 2026-11-02.
Limitations: Windows and Windows on Arm timings are not recorded, since hosted runners are not fit for it. Below Windows 10 version 2004, the wheels are not tested.
Handoff: #395 brought the wheels over without the spike’s rehearsal jobs, and phase 1 takes the lanes.
Phases 1 to 3 report — all on one branch#
The status of each gate is at the end of this report, and it says plainly which aren’t met.
Scope: 2026-11-02 and 2026-11-03, Opus 5.5 for John C. Travers, on
feature/sep-0043-retire-numba-cpu(#415), taking in #394, #398 and #401, which closed unmerged, as §11 says.Delivered:
Selection and floor:
src/solprop/backends/registry.py(no fallback, the tombstone,SOLPROP_REQUIRE_DEVICE) andnative/src/solprop_native/_cpu.py(/proc/cpuinfo, then NumPy, thenCpuFeaturesUnknownError;armv8.2-aon Windows on Arm, recorded bynative/cmake/HostLibraries.cmake).Packaging:
pyproject.toml,hatch_build.py,uv.lock,tools/setup_light.py,native/scripts/fetch_wheel.py(strict),native/ci/install_solprop.py,native/scripts/check.sh,.readthedocs.yamland the three workflows.Removal:
backends/numba_cpu/,backends/bodies.py,_numba_cache.py, the 28test_numba_*modules and the 7 golden files, as accounted for in the inventory below.Tests:
tests/driver/test_retired_backend_runs.pywith real files intests/data/retired_backends/, and the ported obligations below.native/ci/coexistence.pykeeps pyFFTW and Numba as outside packages.Documentation: the pages §8 lists, ADR-064, the gallery on
autowith regenerated output, andchangelog.d/sep-0043-*.
Deviations from the proposal:
Files written by 0.1.0 don’t load (§9 says they do). Published 0.1.0 wrote output schema 1, and this tree reads schema 2 only, by a policy that predates this SEP (
output/schema.py,_refuse_schema). A real 0.1.0 file is refused on either backend, saying to read it with the version that wrote it. §9’s table holds for a schema 2 file: a real stopped run from the last commit before the removal continues onnativeandnumpy_ref. A resumed run’s own record wasn’t checked beyond the saved planes staying as written.An unpinned install doesn’t always fail (§5, §6.7 say the installer’s error is the answer). Resolvers backtrack: on a platform with no native wheel,
pip install solpropanduv pip install solpropinstall 0.1.0, with Numba and pyFFTW, and no error. On a local index with fake wheels, yanking 0.1.0 restores the error, andsolprop==0.1.0still installs. Yanking is the maintainer’s decision. Until then the installation page says so and suggestssolprop>=0.2.§5’s fourth
numpy_reffeature isn’t one. “Beyond six terms on three components” misreadPAIR_HISTORIES = 6.numpy_refalso requiresDELAYED_TERMS = 2, and the only case native refuses and it accepts is two terms on other than three components, which the public route doesn’t produce. The guide leaves the row out. The drafter’s error, from Part II of the spike’s report.The processor check and Windows on Arm’s floor went on the one branch, not standalone (§11). The maintainer asked to consolidate.
With no visible device, the guard reports a load failure. Kokkos stops as the package loads, before the guard can read the build, so
_check_required_deviceturns that into theBackendError§5 promises, carrying Kokkos’s message.Class T in the old-run test uses
tests/march_bounds.py’s reference atrtol/100 and factor 4, the suite’s standing rule, not §7’srtol/10 and 2, and it takes every plane: at the last plane an uninterrupted run is about 1e-11 from the reference by coincidence, against about 1e-8 at the third, on every backend.The dependency list is dynamic, as §6.7 allowed for. A pin written over a static list passed the wheel and sdist built separately, but a wheel built from the sdist,
uv build’s default, runs no metadata hooks: it kept the bare name, and CI’s distribution check installed native 0.1.0 from PyPI and failed on the kernel contract. So the list is inhatch_build.py, asgui/hatch_build.pyalready does.Provenance mentions Numba outside §7’s four places. The search finds it in no live code: the only imports are
native/ci/coexistence.py’s. The word stays in test comments and docstrings that say which retired test a bound came from, the guide’s “removed backend” section, measurements indocs/dev/labelled as taken on the retired backend, gallery CSVs recorded on it, open SEPs’ notes and ADRs. Rewriting those would lose the record.Finalcomes before fresh installs on every platform (§7 and §11 put every gate first). Those installs come from published files, so onlyqualify-publishedcan show them, after a release. The maintainer chose to setFinalin this pull request rather than hold the SEP open across a release, and #463 carries the check.
Decisions: the coexistence environment installs pyFFTW only where it has a wheel. A test environment with no native package takes
autoasSOLPROP_TEST_BACKENDfor the session, andreal_autokeeps the refusal; a test whose child process runsautoskips there (child_runs_auto). Windows on Arm’s lane ispoe test-windows-arm, which sets no small BLAS thread variables and limits the pools after they load.Verification:
Check
Result
Where
Default lane on native, local
17,530 passed, 46 skipped, 3 xfailed
Xeon Gold 6240, typecheck off
Lightweight setup, no compiler on
PATHfor the sync15,656 passed, 769 skipped, 2 xfailed; documentation build clean under
-WXeon, numpy_ref
Gallery on native under
-WBuilt. Printed values match apart from wall times. Two figures moved visibly through the default shot-noise draw: with
noise=None, native andnumpy_refagree to 1e-16 in the field and 7e-14 in the ionised fractionXeon
Helper budgets, §6.3
Tracking set geometric mean 1.0009, the same work on all 22.
construction_s: at most +0.053 s and +0.9 %, against the larger of 0.5 s and 20 %. Host march 1.0001, 0.9999, 0.9935 against A/A 1.0020, 0.9957, 0.9946.numpy_ref1.0016 against A/A 1.0038, fields bit-identicalEPYC 7763, whole node, fresh processes, against
mainatff29371Field differences in that comparison
At most 1.0e-10 (
he_rdw_averagedat 16 threads, 9.1e-11 onctc), the cases with the spike’s known floor; the rest at or below 1.1e-12EPYC 7763
SOLPROP_REQUIRE_DEVICE, real CUDA build labelled0.3.0+cudaEvery case as §5 says. Device suites 31 and 63 passed, 2
mlxskipsNVIDIA A40
Labelled builds, real wheels, pip 26.2.2 and uv 0.12.7
0.3.0+cudakept at 0.3.0 through install,-Uand eager; replaced by the CPU wheel at 0.3.1Xeon, a CPU build
Pin in built metadata, through the real hook
Requires-Dist: solprop-native==<version>in the wheel and the sdist’sPKG-INFOXeon
Coexistence
With pyFFTW and Numba as outside packages, every wheel lane
CI,
wheels.ymlpip install "solprop[native]"and withuv pip(§9)Both install solprop and native; pip warns that the extra is gone, uv says nothing
Xeon, CI’s wheels
Default lanes from the wheels
Linux x86-64, Linux aarch64 and macOS 17,753 passed, 46 skipped, 3 xfailed; Windows x64 and Windows on Arm 17,673 passed, 126 skipped, 3 xfailed
CI on
d3f0c678Slow lanes from the wheel
Linux and macOS 248 passed, 1 skipped
CI on
d3f0c678Forced x86-64-v2
Linux 17,753 passed and Windows 17,673 passed, with the skips above, including Windows’ two earlier BLAS failures
CI on
d3f0c678Selection under Intel SDE, with
/proc/cpuinfomaskedGreen
CI on
d3f0c678,cpu-floorMinimum systems
almalinux:8with glibc 2.28 and no compiler onPATH, Windows Server 2022: installed from the files,autoisnative, a short run saved. macOS 14.8 the sameCI on
d3f0c678Class T, x86-64-v2 and CPython 3.15.2 against v3 on 3.14, reference at
rtol/10, factor 2All 11 adaptive cases within, at a ratio of 1.000 on both, with the same steps.
kerr_beam_384is fixed-step, so class R applies: v2 differs by 7.1e-16Xeon Gold 6240, 16 threads, the release wheel
§7’s flag, the same steps above 1e-10
v2 over v3:
he_rdw_averaged1.27e-10,ctc1.08e-10. 3.15 over 3.14:ctc8.2e-11, the rest at or below 3.6e-12. These two have the floor already recorded: two v3 builds differing only in ABI gave 3.8e-10 and 2.0e-10Xeon
Plain
uv syncin a fresh cloneBuilt native in 162 s on 18 cores, and
autoisnative. GCC 16.2 and the libraries came from this machine’s environmentXeon
Tracking set, against 0.1.0’s native rows
EPYC 7763: geometric mean 1.0023 over the 20 configurations of unchanged revision, the same steps on all. Numba’s 0.1.0 rows are 2.08 times slower. Ledger:
docs/dev/benchmarks.mdEPYC 7763, the release wheel
Informational native coverage, §6.4
Migrated Python suite and
native/tests: lines 87.48 %, functions 87.80 %, regions 87.99 %, branches 80.80 %.core.cppat 15.7 % is theWorkspaceclass, 101 of 120 functions, which the gtest binary tests and this run left outXeon, Clang 23,
results/native-coverageThe inventory (§6.4):
results/sep43-test-inventory.mdin the campaign directory, not in git: 446 removed functions, of which 192 carried, 147 only the deleted implementation, 102 lost and 5 unconfirmed, under a strict rule that counts a looser bound as lost. Ports followed, with no bound loosened except as listed:P1, items 1 to 16: all ported. Items 1, 2, 10 and 16 have weaker witnesses, because native exposes no time-domain current: the plasma column’s per-sample budget is held at the stage, so an error confined to a few samples could pass. A test-only binding would restore it.
P2, items 17 to 40: ported or weaker, with item 30 not portable, since translation refuses a flow on the only non-identity crop.
P3 and P4, items 41 to 83: ported or weaker. Items 62, 65 and 82 aren’t ported, and neither are the pad and the 1 ulp rate from the unconfirmed rows.
Self-comparisons:
test_the_two_propagator_modes_agreeand the coupled carry’s “no route” are fixed. In a lane with no native package, parity tests still comparenumpy_refwith itself, and the terminal says so.Changed after measuring, for review: item 55’s flow factors are exact on constant flows only. On tapers, the frozen block varies at about 1e-17 with BLAS width and Schur vectors aren’t unique, so these took
2 gamma(n_deg)and10 n^2 u. One accept residual went from per column to normwise, since transverse FFT rounding is normwise.
Sources checked: NumPy’s
npy_cpu_features.c, FFTW’sx86-cpuid.h,avx2.candneon.c, and OpenBLAS’sdynamic.c,dynamic_arm64.c,cmake/cc.cmakeandMakefile.arm64, for how each reads a processor and whatTARGET=NEOVERSEN1compiles for:-march=armv8.2-a -mtune=neoverse-n1. So Windows on Arm’s floor isarmv8.2-a, checked through CRC32 and the LSE atomics, the two parts Windows can report. The lowest Windows hardware admitted wasn’t established.Limitations:
No hardware below the floor was run. Selection is by stand-ins and Intel SDE in CI.
PF_ARM_*’s refusal has run only against a fakekernel32. Its accepting path runs for real on CI’s Windows on Arm lanes.Windows 10 isn’t tested. The installation page says it’s expected to work, but untested (Q15).
plasma_bandon Windows on Arm, explained. The record’sboundaryrow atlambda = 1e-8stores 3.7545630e-06 underrtol = 1e-6, and Windows on Arm gives 3.7545680e-06: 1.33e-6 above, 5.0e-12 absolute. It’s rounding, and the tolerance sits inside its spread. The probe (spike/band-probe, never merged) ran the row on the five platforms, with each one’s own taps and with one fixed set:Platform
Own taps, against the stored value
Fixed taps
Largest tap difference
Windows on Arm
+1.33e-6
+9.8e-7
1.7e-13
Linux aarch64
+3.3e-7
+2.8e-7
1.5e-14
macOS
+2.9e-7
-8.6e-8
2.3e-13
Linux x86-64
-3.0e-7
-1.4e-7
1.3e-14
Windows x64
-8.5e-7
-6.5e-7
1.6e-13
The spread with fixed taps, 1.6e-6, is the FFT’s and the C library’s;
lstsqon each platform’s LAPACK adds the rest. On the Xeon, jittering the spectrum by 1 ulp over 400 draws moved the row by up to 1.17e-6, and 0.8 % of draws fell outside the tolerance. So this is not specific to Windows on Arm, and Windows x64 is a few parts in 1e7 from failing the other way. The test’s real assertion,errors[0] <= 1e-5, holds everywhere. On this evidence the maintainer raised the column’srtolto 3e-6 (Q14). The stored rows are unchanged.Windows on Arm’s other failure: the
gaussian_projectiondocstring asserted exact equality of two mirrored sums, which that platform’s dot product doesn’t give. It now compares to 1e-12.The gates of §7, on
d3f0c678:Gate
State
The final suite everywhere
Met:
Wheelsandci-passedare green, and every lane passes, including Windows on Arm’s wheel lane and its three test shards atplasma_band’s raised tolerance (Q14)Forced v2 and slow lanes
Met
No silent skips
Met where the check runs: no unexpected skip on any default shard, coverage shard or wheel lane. The slow, lowest-dependency and no-native lanes don’t carry the check
Fresh installs
Partly.
uv pipfrom the built wheel and sdist on Linux, and from the wheel on the minimum systems.pipon the built files is local only; every platform isqualify-published’s, after a releaseMinimum systems
The claim narrows. macOS 14, a
manylinux_2_28-age Linux container and Windows Server 2022 pass from the built wheels, with no compiler on Linux’sPATH. Windows 10 has no hosted machine, so the installation page says it’s expected to work, but untested (Q15). #313 keeps the restWindows on Arm’s open items
Explained; the record’s tolerance raised by the maintainer’s decision (Q14)
Selection, no package, v2 selection, unreadable processors, Arm’s floor
Met.
PF_ARM_*now runs for real on the Arm lanesv2 is correct
Met
v2’s cost
Spike’s measurement only: Numba is gone from the final tree
Windows v2’s BLAS
Met
A numba-free tree, helpers, inventory
Met; the inventory’s unported items are listed above
Coverage
Met: 100 % on
solprop, 50,560 statements and 14,988 branches with none missed, on thecoverage-reportjob. The informational native run is reported aboveNo regressions
Met on the EPYC. The Xeon gives no verdict at 36 threads; see the ledger
Metadata, workspace, lightweight setup,
poe fetch-wheel, labelled build, GPU safeguardMet
Old runs
Met, with deviation 1
Coexistence
Met
Removal is complete
Met, with deviation 8
Python 3.15
Short runs on all five platforms, class T on Linux. Linux’s default lane is the standalone step after this merges
Questions the maintainer settled:
Q6, settled 2026-11-03. 0.1.0 is yanked right after 0.2 is published, so that deviation 2 becomes the error §5 describes. That release step is the maintainer’s.
Q7, settled 2026-11-03.
envelope_axesandenvelope_momentsstay contracted, and leave the licence’s list, as the maintainer asked. Contraction is the default, whose one hazard is an exact cancellation (SEP 25). The only candidate is the determinant, and scaling makes one factor 1 when the two components are equal, or one is twice the other. Over 200,000 random linear fields per case in plain Python, with each product fused either way, the ratio’s largest value was 5.96e-17 with or without fusing, and no moment cut left [0, 1]. Numba’s protection isn’t carried over.Q8, settled 2026-11-03. A mistyped
SOLPROP_FFTW_PLANis silently MEASURE (fft_plan.hpp). It is to be refused (#460). Strict xfail innative/tests/test_fftw_plan_refusal.py.Q9, settled 2026-11-03. No code change:
kernel-contract.md§7 states the precondition, and the strict xfail stays.Q10, settled 2026-11-03. Native ignores a truncated
response_order, which exists only at contract level. It is to be refused (#460).Q11, settled 2026-11-03. Accepted as the rounding of plan choice and recorded in
accuracy.md, and #461 takes the unexplained factor of 65.Q12, settled 2026-11-03. An audit (#462), each thread test asserting from
execution_sizingthat the operation really threaded.Q13, settled 2026-11-03. The closing and trimming text of
results/sep43-issue-audit.md, not in git, is posted after this merges, under #392. #300 needs a timing on an idle machine.Q14, settled 2026-11-03. The maintainer raised
plasma_band’sdeviationtolerance fromrtol = 1e-6to3e-6. It’s a recorded value’s regression tolerance, withbound: null, not an accuracy bound, and the tolerance applies to the whole column.Q15, settled 2026-11-03. The maintainer chose to say that Windows 10 version 2004 or newer is expected to work, but untested.
Q16, settled 2026-11-03. #453, on the 0.2 milestone, tracks
kerr_beam’s rise andctc’s construction, which takes 9.8 s longer than at 0.1.0. Neither comes from this branch. The maintainer handles it there.
Completion — 2026-11-03#
Every phase is reported above and the gates of §7 stand as their table says, with deviation 9 for fresh installs. The manual documents the change, and both pages it names end with a link here. Nothing is left open in this SEP; what follows it is in issues:
#392, this SEP’s tracking issue: posting the closures and trims of Q13.
#460: refusing a mistyped
SOLPROP_FFTW_PLANand a truncatedresponse_order(Q8, Q10).#461: the factor of 65 between an adaptive run’s fields and its steps (Q11).
#462: thread tests asserting from
execution_sizing(Q12).#463: fresh installs through
qualify-publishedat 0.2, then yanking 0.1.0 (Q6, deviation 9).#464:
propagator_mode="tabulate"under Runge-Kutta has no effect on native, but the metadata records it.#319: Python 3.15’s standalone Linux lane, and its claim in the installation guide.
#92: the NumPy floors, version derivation and the links to
/en/stable/that follow 0.2.#453:
kerr_beam’s rise andctc’s construction, which don’t come from this branch (Q16).
Part III — History#
2026-11-02: Drafted. Tracking issue #392.
2026-11-02: The maintainer settled Q1 and Q2: native is a required dependency, a platform without it is unsupported and refuses with a request to report it,
numpy_refis a development tool andmlxstays a special backend. §6.7, §8 and Q3 to Q5 added. Q3 then settled: features that run only onnumpy_refstay there, and every supported install has native. Q4 and Q5 then settled: a plainuv syncin a checkout compiles native, and source builds are labelled, rebuilt on upgrade and can opt into refusing a CPU-only build.2026-11-02: AI review by GPT-6, with the maintainer’s decisions on R1, R2, R12 and R13. Part I revised in response: no refusal-only wheel, removal before 0.2, a selection table, the lightweight setup,
SOLPROP_REQUIRE_DEVICE, numerical acceptance criteria, and §11’s phases.2026-11-02: The maintainer settled how the processor is read on Linux:
/proc/cpuinfo, then NumPy’s features, then a refusal (§6.1, R8).2026-11-02: Accepted by John C. Travers. Part I is frozen.
2026-11-02: Implementation started on
feature/sep-0043-retire-numba-cpu, taking in #394, #398 and #401.2026-11-03: Phases 1 to 3 delivered on that branch, with the report in Part II.
Finalwaits for the gates and the maintainer.2026-11-03: Final. Every phase reported in Part II; follow-ups opened as #460–#464; the SEP moved to the archive (John C. Travers, with Opus 5.5).
AI review — 2026-11-02#
Reviewer: GPT-6 (AI). Reviewed the whole proposal at
023a5351,
including Q1–Q5, against the architecture, conventions, SEP 0, SEP 25, SEP 29,
the registry and simulation entry points, shared helpers, packaging and workflows,
and representative tests. Related work read includes
#395,
#398,
#399 and the issues linked below.
This is a review, not acceptance or an implementation report. Part I and Part II
have not been edited; only the review metadata and this subsection were added.
Assessment. I support retiring numba_cpu. Removing a second compiled CPU
implementation reduces the cost of every future seam change, and the reference/native
pair is a sound long-term architecture. Two x86 builds preserve performance on newer
processors without making older ones pay for a source build. The lack of an announced
user base is a reasonable reason to shorten deprecation, provided the final release
and migration guarantees are explicit. I would not make retaining Numba the remedy
for the questions below.
I also support native in the ordinary installation and a refusal instead of an automatic slow fallback. However, retiring Numba, limiting the support matrix, publishing a refusal-only wheel, and compiling native for every contributor are separate decisions. The last two introduce maintenance and usability costs that the speed measurements do not answer. My principal recommendations are to preserve a clearly documented role for reference-only science, reconsider the refusal-only wheel, and make the packaging and qualification contracts precise before acceptance.
Evidence and limits. The code and workflow review is read-only. Small local probes checked version matching, wheel replacement and the CPU selector; their results are recorded with the relevant items. No native wheel, GPU build, long propagation or timing series was rebuilt or rerun for this review. The spike’s numbers are evidence reported by its author, not independently reproduced here. The external sources checked were the official Python, PyPA, pip, uv, nanobind and OpenBLAS documentation. The review’s probes used CPython 3.14.5; the wheel probe used uv 0.13.6, not pip. All R items below remain open for the author/maintainer to answer in a separate response, as SEP 0 requires. High priority means a contract or gate to settle before acceptance; medium priority may be explicitly assigned to a phase.
R1 — High: define support and automatic selection together (§5, §6.7, §8, Q1–Q3). The unconditional statements that
autopicks native and refuses without it conflict with retaining today’s TDSE behaviour:_tdse_entryindriver/simulation.pychangesautotonumpy_refbefore the registry sees it. There is a second exception:_single_backendselectsmlxfor single precision when available. Neither case is an accidental fallback. Conversely, CF4 and the larger general delayed response need an explicit reference choice.Recommendation/question: write one selection table covering ordinary double precision, TDSE, single precision, explicit reference and explicit MLX, with a real native wheel, the stub, and no native package. Is native installation merely the supported packaging policy, or must every TDSE/MLX/reference run prove that native could execute on this machine? I recommend the former, with the two existing
autoexceptions stated explicitly and the development escape hatch kept. Do not load a CPU or GPU runtime just to enforce an installation policy. Describenumpy_refas the reference and the supported executor for the listed exceptions, rather than implying that TDSE science is now an unsupported development activity. Also preserve the rule that future features need native parity or an explicit accepted exception; Q3 should not become a general waiver.Maintainer decision — 2026-11-02 (John C. Travers): describe
numpy_refas the numerical reference backend and the executor for explicitly listed supported scientific features. Those features remain supported on the project’s supported platforms. Explicit reference execution elsewhere may remain available without a support promise for those platforms. The drafting agent should reconcile this wording with the selection rules and the required native installation policy.R2 — High: the refusal-only wheel needs an installed-state contract, or should be deferred (§6.7, §7, Q2). Wheel preference only answers a fresh resolution. In a disposable local-index probe, I installed a pure-Python stub at version 1.0, then added a compatible platform wheel at the same version.
uv pip install --upgradeleft the stub installed;--reinstallselected the platform wheel. This models a partial mirror, a newly added wheel, or a system whose compatibility changes. The package manager sees a satisfied version, not an unavailable backend. A local label helps identify a source build, but does not solve stub recovery. The pip install reference likewise distinguishes upgrade from forced reinstallation; pip was not probed here.Recommendation/question: is a nicer first-run error worth a permanent second meaning of “solprop-native is installed”? My preference is to defer the stub, keep the required real dependency and document resolver failures, while providing a deliberate lightweight setup for development/result reading. If the stub stays, specify and test fresh install, same-version recovery, upgrade, source-build replacement, a partial wheel index, and normal project lock/sync with pip and uv. Give the stub and platform wheels consistent dependency metadata, a stable non-importing capability marker, and an exact recovery command.
available()and diagnostic tools must not report the stub as an executable backend.Maintainer decision — 2026-11-02 (John C. Travers): omit the refusal-only wheel from this change. Normal installations require the real native package; accept the installer’s resolution error on unsupported platforms and explain it on the installation page. Retain helpful runtime diagnostics for unsupported CPUs and broken installations where solprop can execute them. Remove the stub’s design and its associated verification obligations when revising the proposal.
R3 — High: cached ancestor wheels and the exact version pin do not yet form a coherent contributor workflow (§6.5, §6.7, Q4–Q5). CI keys its wheel cache on native sources;
fetch-wheelin #398 selects an ancestor artifact and warns if native changed. A Python-only commit still changes the VCS-derived package version, so that ancestor wheel does not satisfy the new required exact pin. A local version label in the requirement is also matched exactly: the probe accepted0.3.0+cudafor==0.3.0, but rejected a different development version and a.cudasuffix added to an already locally labelled development pin. This follows PyPA’s version-matching rules. Skipping synchronization avoids a rebuild; it does not make mismatched metadata consistent. Furthermore,native/being unchanged alone does not certify the current Python adapter contract or the seam it consumes.Recommendation/question: define an explicit development exception, or require an artifact compatible with the current version and contract. Test a Python-only commit, a seam change, a native change, a dirty checkout, and an expired artifact. Specify what subsequent
uv run,uv syncand a dependency check do. Reject an incompatible artifact rather than merely warning; retain its true build identity. Do not relabel an old binary to pretend it was built from the current commit. Exercise the real metadata hook, wheel and sdist, since Part II says the dynamic dependency metadata was not tried. Document how a released source build and a development build each receive their labels.R4 — High: turn Q5’s GPU safeguard into a specified interface (§5, §7, Q5). “An optional setting” does not yet tell an implementer what must be checked or a user how to protect a GPU job. An upgrade replacing a CUDA/HIP build with the CPU wheel can turn a short allocation into a much longer one; correct fields alone do not preserve the user’s execution request. The CPU-only second environment in §9 is also a real workflow cost, since a GPU build and the CPU build do not have identical admission limits (for example #363).
Recommendation/question: name the setting, its accepted values and precedence, and require checking the actual build’s execution space before allocating or propagating. A version suffix is provenance, not evidence of a usable GPU. State whether a missing device, the wrong device backend and an explicit
numpy_refrun are refused. I favour an opt-in execution requirement, enabled in the documented GPU environment/job recipe, over changing every CPU user’s default. Add real labelled-build install/upgrade tests to §7; the fake-wheel resolver probe does not test the SCM label or runtime guard.R5 — High: replace step-count equality with numerical acceptance criteria (§4, §7, Part II accuracy discussion). “Same step counts” cannot establish no regression: equal counts can hide wrong fields, and harmless rounding can change adaptive decisions. Requiring the same step sequence across v2/v3 or interpreters conflicts with SEP 25. “About 1e-10” also leaves the norm, normalization, zero handling and whether it is a rounding or accuracy claim unspecified. A similar difference between two ABI builds demonstrates variability, not that either result is sufficiently accurate.
Recommendation/question: use matched discrete computations for predeclared rounding budgets, and adaptive runs against a tighter/stored converged reference for per-case accuracy envelopes. Define the field and observable metrics and their units before comparison. Keep actual work counts as diagnostics and exact accounting, not an equality gate. Give performance its own measured criterion; the final tracking comparison needs times, spread and construction cost as well as step counts. Existing spike results can remain exploratory evidence, with any unexplained excess identified as an open qualification item.
R6 — High: retire test implementations only after mapping their obligations (§6.4, §7, phase 2). A
test_numba_*filename does not establish that its numerical witness is disposable. For example,test_numba_band_bounds.pycontains an operand-based absolute plasma error budget, including cancellation and pre-ionisation; several other modules cover hard-mask zeros, reductions and thread-sensitive invariants. Some may already have native equivalents, but the proposal gives no mapping. A high Python coverage percentage cannot identify a lost stress input or independent bound.Recommendation/question: make phase 2 deliver a compact inventory: each retiring test family/witness maps to a surviving reference/native test or has a reason that it exercises only a deleted implementation. Preserve scientific invariants, pathological inputs and independently derived bounds, while dropping Numba cache/dispatcher mechanics. Retargeting all comparisons mechanically to
numpy_refmust not turn one into a comparison of an implementation with itself. Keep historical convergence and benchmark readers able to read retired names.R7 — High: qualify the wheel as the only production path, including absence and unexpected skips (§6.5, §7, §9). #313 records unqualified minimum systems and native tests that may silently skip. The spike’s failure attribution is useful, but an empty
pyfftwand-k "not numba"are not the final supported install. Removing the alternative makes a packaging/runtime failure a complete production outage on that platform.Recommendation/question: make removal conditional on the final default suite on all five wheel platforms without the rehearsal filters, forced v2 where applicable, the planned slow lanes, no unexpected native skips, and fresh outside-checkout installs that actually propagate. Include a small compiler-free smoke run at each declared minimum OS, or explicitly narrow the support claim and assign the remaining qualification to #313. Treat a stub, an absent package, a below-floor CPU and a broken shared-library load as distinct cases; do not convert arbitrary import failures into “unsupported platform”. Importing, reading a saved result and running the reference by name must be exercised in the final no-native/stub environments, not just
import solprop.R8 — High: certify the whole binary’s CPU floor and fail safely when probing fails (§6.1–6.2, §7). A direct probe of this revision’s
_cpu.py, with v3/v2 records and an absent Linux cpuinfo file, returned no baseline shortfall and selected_core(v3). On a lower processor that can reach an illegal instruction instead of the promised refusal. This existing selector path is now tracked in #443; no native execution was performed in this review. Also, a generic Armv8-A core does not alone prove the Windows Arm wheel needs no check: it statically links OpenBLAS built withTARGET=NEOVERSEN1,DYNAMIC_ARCH=OFF. OpenBLAS’s build documentation treats the target as a CPU architecture choice.Recommendation/question: specify what happens when CPU capabilities cannot be established; I recommend refusal before loading rather than assuming the highest variant is safe. Add missing/malformed probe cases. For Windows Arm, record the instruction requirements of the linked libraries and the minimum admitted Windows hardware, then justify the absent check or add one/lower the target. This review does not establish that the Arm wheel actually uses an unsupported instruction; it identifies a missing whole-wheel justification.
Maintainer decision — 2026-11-02 (John C. Travers): on Linux, read
/proc/cpuinfo, then NumPy’s__cpu_features__if that fails, and refuse if neither answers. Windows x64 keeps its current query, and Windows on Arm gains aPF_ARM_*query.R9 — Medium: distinguish Python coverage from assurance of the sole compiled implementation (§6.4–6.5, §7). Removing
bodies.pyand its interpreted coverage pass shrinks the measured Python code while production execution moves further into C++. #324 records that native test-suite source coverage has not been measured. Keeping Python’s 100 % is good, but it does not answer which C++ capabilities the surviving tests reach.Recommendation/question: retain the Python gate and native sanitizer/tests, and commission one informational native coverage run over the migrated suite, or explicitly defer that evidence to #324 with an owner/phase. No arbitrary C++ percentage is requested. The useful deliverable is an account of unexercised capabilities and the surviving analytic/parity checks. Keep
numpy_refindependent: block imports of native, Numba and pyFFTW when testing its helper and run paths. Native being a dependency must not make the oracle call the code it validates.R10 — Medium: define the helper cost gate and retain its arithmetic contract (§6.3, §6.8, phase 2).
construction_sis named as a gate without a case set, baseline, allowable increase or treatment of measurement noise. Shared helpers also serve stages, dense evaluation and the Python march. Native supplies its ownHelpers, but a construction-only measurement cannot establish that every affected call is outside propagation. “No kernel changes” does not remove the need to qualify changes to protected coefficient evaluation.Recommendation/question: list which callers switch implementation and which retain native helpers; time short table-heavy construction, host-march and reference cases. Set an absolute/relative budget appropriate to each. Keep the strict operation order of compensated sums, small-argument formulae and basis construction. Any vectorization proposed after a failure needs its own conditioning/rounding argument, not only a timing win. The oracle must remain usable for validation even though its speed is not the production target.
R11 — Medium: specify migration beyond opening an old file (§5, §9, §7). “Old output and snapshots … still load” does not settle continuing a run whose recipe/settings name
numba_cpu, restoring Adams history, replaying a schedule, or reading a scan’s historical execution metadata. These are distinct from loading a field, particularly after SEP 41 and SEP 42. The backend setting is mutable as an execution resource, so a deliberate override is plausible without changing scientific provenance, but it needs a documented path and tests.Recommendation/question: state whether old Numba runs are readable only or resumable on native/reference, and show the explicit backend override. Test a historical output, snapshot, recipe and short adaptive continuation, preserving the original execution record and appending the new one. Use SEP 42’s normal compatibility checks and accuracy envelope; do not silently rewrite the backend name or bypass those checks. If any continuation is intentionally unsupported, document the refusal and the pinned-release recovery.
R12 — Medium: Q4 should distinguish native development from Python/docs contribution (§6.5, §6.7, §8). I agree a native developer should build the local tree. “Anyone in a checkout is developing” does not imply that a documentation author, a Python-only contributor or an offline researcher has a C++ toolchain, FFTW/OpenBLAS and GitHub artifact access. The 104 s measurement had the libraries prebuilt; it is not first-install cost. The checkout/network limitation is already tracked by #312.
Recommendation/question: keep the default source build if desired, but make a reliable lightweight contributor setup a first-class documented command, rather than leaving it as two automation settings. Validate it with no compiler and no native import, and explain its limits. Fix R3 before recommending
fetch-wheelas the alternative. State explicitly whether archive/offline builds remain deferred to #312; required native increases their importance for scientific reproducibility, although they need not block this retirement.Maintainer decision — 2026-11-02 (John C. Travers): keep plain
uv syncbuilding native by default, and provide one documented lightweight setup command for Python/documentation contributors without native or a compiler. It supports reference execution and documentation checks as a development workflow, not a production fallback. Native changes still require native qualification. The drafting agent should specify the command and resolve R3’s compatibility details.R13 — Medium: make phase completion, evidence and rollback current and enforceable (§3, §7, §9, §11, Part II). Part II says nothing has merged, but #395 is merged and its code is in the reviewed tree. #398 has a full CI result distinct from the spike, and #399 has a real 3.15 source-build result. The proposal still treats all of this as one unmerged rehearsal. “Coverage holds, or gaps are reported” also reads as permission to finish below the mandatory gate, and the multiple-PR plan needs an explicit SEP 0 interpretation rather than relying on a tooling exemption to cover all future phases.
Recommendation/question: record what is already delivered with commit/CI links, which exact remaining PRs are prerequisites, and which measurements apply to which artifact. Require all mandatory gates to pass before removal/Final; reporting a failure is a phase report, not a substitute for passing. Keep remaining implementation phases on the agreed branch unless a specific standalone exception is recorded. Decide the last Numba release: “whether phase 3 merges before or after 0.2” is ambiguous beside “0.2 is the last release with Numba”. Pinning an existing release is an immediate rollback; reverting source after publication requires a new tested release. Before Numba’s last release, retain enough qualification for it despite moving the CI lanes.
Maintainer decision — 2026-11-02 (John C. Travers): remove
numba_cpubefore the 0.2 release. Version 0.2 therefore ships without Numba; it is not the last release retaining it. Update the phase ordering, compatibility text and migration/rollback guidance consistently. The required qualification gates still apply before removal.R14 — Medium: separate stable-ABI compatibility from Python support (§6.6, §7, phase 4). The ABI strategy is sensible and nanobind documents it. An ABI audit and extension import do not qualify the Python adapter, solprop, NumPy/SciPy/h5py and the TDSE provider together. Nor does
cp314-abi3establish free-threaded support: Python distinguishes the ABIs.Recommendation/question: say explicitly that these are ordinary CPython wheels, define the tested interpreter set and keep future-version compatibility separate from a support promise. Include a short actual run on 3.15 on each platform, plus the planned Linux suite without fake dependencies. Name whether h5py source installation makes 3.15 provisionally supported or remains a qualification lane. Keep this phase independent of removal if needed; do not delay retiring Numba just to advertise another Python version.
R15 — Medium: retain external-library coexistence checks (§2, §6.4, §8). Removing pyFFTW from solprop’s dependencies removes one common collision, but another package in a notebook can still import it before native. The native initializer currently refuses that order on macOS, as #315 records. Removing every pyFFTW-related test or suggesting that this problem has disappeared would lose a still-relevant guard now that native is the standard path.
Recommendation/question: keep a dedicated coexistence environment with the relevant external packages installed, while keeping production dependencies Numba/pyFFTW-free. Preserve the documented refusal until safety is demonstrated, update its retired-backend advice, and distinguish it from an unsupported CPU. Also retain shared host BLAS/resource management: removing Numba does not remove NumPy/SciPy construction or third-party thread pools.
R16 — Medium: qualify the reach and speed argument, and reconcile related work (§2–3, §8–10). The reported speed advantage is persuasive for the measured configurations. It is not evidence about every route/platform: #316 itself notes the busy Apple timings and points to #314 for missing Windows, aarch64 and scan comparisons. Likewise, solprop not being distributed on conda-forge does not itself prevent installing its Python package into an environment containing conda-forge Numba/pyFFTW. Dropping those systems may still be the right support decision; it should be described as a choice.
Recommendation/question: retain the decision to retire, but bound the claims to the measurements and distinguish unavailable wheels from unsupported use. Say “no known users” if that is the evidence, rather than inferring zero users from no announcement. Audit open work that still names Numba: close only work specific to the retired implementation, retain shared physics/performance obligations, and link the existing reference-only gaps such as #304. I would keep established introductory examples using
autowhere they teach no backend-specific feature; reserve explicit native selection for examples that need its capabilities or performance. This reduces future documentation churn without reintroducing a fallback.
Drafting handoff after the maintainer’s decisions — 2026-11-02#
The decisions recorded beside R1, R2, R12 and R13 settle those policy choices. The drafting agent should now revise Part I and provide the separate response to each review item required by SEP 0, distinguishing decisions implemented in the text from qualification work assigned to a phase. The other technical questions, including R3’s wheel/version compatibility and R4’s concrete GPU safeguard, still need answers. These decisions do not mark the SEP accepted or the review complete.
Response to review — 2026-11-02 (Opus 5.5, drafting agent)#
Before this response, the review’s claims about the code were re-read and held: _tdse_entry and
_single_backend (R1), select_variant and baseline_shortfall choosing v3 with no /proc/cpuinfo
(R8), fetch_wheel.py only warning (R3), the operand budget in test_numba_band_bounds.py (R6), and
the issues cited. New probes are in Part II. Review status stays In review: a human review is still
to come. All 16 points are taken, except as stated in R6, R9 and R14 on where the work happens. Items
marked phase are qualification assigned to a phase, and are not yet done.
R1 taken, as the maintainer decided. §5 has the selection table, by request and by native’s state, with the TDSE and single-precision exceptions stated. Native’s installation is a packaging policy, and nothing loads a runtime to enforce it.
numpy_refis the numerical reference and the executor of a listed set of supported features, with no support promise for running it elsewhere. The list isn’t a waiver of SEP 0’s parity rule. §8 carries the wording, including TDSE. I also kept single precision on the list, as in Q3. Its message today callsnumpy_ref“not a production backend”, which §8 changes, so the maintainer may want to check that listing.R2 taken, as the maintainer decided. The refusal-only wheel, and its verification and release gate, are out of §5, §6.7 and §7. §10 records why, and §6.7 has the installation page explaining the installer’s error. Runtime diagnostics stay for a low processor and a broken installation.
R3 taken. §6.7 defines three checkout states and makes the exception explicit, with metadata deliberately mismatched and reported only for a fetched wheel.
poe fetch-wheelbecomes strict: a difference from the artifact’s commit innative/or the solprop modules native imports refuses, including uncommitted changes, and an expired artifact refuses. The wheel keeps its true identity.KERNEL_SCHEMA_VERSIONstays the guard at run time. The real hook was exercised for wheel and sdist (Part II), and labels are defined for released and development builds. §7 has the five cases. Phase 1 implements it.R4 taken. §5 specifies
SOLPROP_REQUIRE_DEVICE: values, no competing precedence, when it’s checked, what it reads, and that it refuses a CPU build, a wrong backend, no visible device, andnumpy_reformlx. It’s opt-in, in the GPU job recipe. A probe supports reading the build, since a CPU build labelled+cudareported no device. §9 records the second environment’s cost, with #363. §7 adds real labelled-build tests. Phase 3 implements it. I had no device for this, so the positive case and a device build’s failure at Kokkos start-up are untested.R5 taken. §4 and §7 replace step-count equality with named metrics, class R on the existing ladder, and class T with a reference at
rtol/10 and a factor of 2 fixed now. Work counts are a diagnostic. Performance has its own criterion. Part II marks the spike’s results as exploratory and keeps that excess open. Phase 3 measures it.R6 taken, with the inventory assigned, not done here. §6.4 makes it a phase 2 deliverable and a §7 gate, keeps scientific invariants and bounds, and forbids a comparison of an implementation with itself. Deleting those modules now waits for it. Historical readers stay. Phase 2.
R7 taken. §7’s first rows are the final suite with no filters on all five platforms, forced v2, slow lanes, a failure on unexpected skips, fresh installs that propagate, and minimum systems, with the claim narrowing and #313 keeping the remainder where a machine isn’t available. §5 keeps the failure cases distinct, and §7 exercises the no-package case. The refusal-only case fell away with R2. Phase 3.
R8 taken, as the maintainer decided. §6.1 asks
/proc/cpuinfo, then NumPy’s dict, and refuses before loading if neither answers. Windows x64’s query can’t realistically fail, so it isn’t part of that order. §10 records the alternatives. I kept a named variant as the deliberate way past on x86-64, so a restricted environment has a route; Linux aarch64 has a single build, so it has none. §6.2 drops the statement that no check is needed, adds aPF_ARM_*query for Windows on Arm, and replaces the installation page’s claim. As the review says, this doesn’t establish that the wheel uses an unsupported instruction. Both are standalone fixes before the removal, under #443.R9 taken, choosing the run over deferral. §6.4 keeps Python’s gate and commissions one informational native coverage run with no percentage, in phase 3. #324 keeps the question of a standing collector. §6.3 and §7 add the test blocking native, Numba and pyFFTW from
numpy_ref.R10 taken. §6.3 says who changes and who keeps their helpers, sets budgets for construction, a host march and a reference run, keeps the strict arithmetic, and requires a rounding argument for any vectorised form. The budgets are my proposal, fixed before measuring. Phase 2.
R11 taken. §9’s table says old runs are readable, and resumable on native or
numpy_refby changing the backend deliberately, under SEP 42’s checks, keeping the original record. This rests onresuming.mdand SEP 42, which keep the backend out of the run’s identity, and on the existing cross-backend tests. Whether a new execution record is added on every route isn’t confirmed. Phase 3 tests it from files written by 0.1.0, and documents any refusal.R12 taken, as the maintainer decided. §6.7 keeps the build by default and specifies
uv run --no-project tools/setup_light.py, its limits, and why uv needs the two settings. A probe with no compiler passed a sample of tests and the type check. The documentation build is left to §7. §2 states that archive and offline builds stay with #312. Its importance does grow with a required native, though it doesn’t block this. Phase 3, with the required dependency.R13 taken, as the maintainer decided. §9 and §11 remove
numba_cpubefore 0.2, with 0.1.0 as the last release that has it, and rollback corrected for what a revert needs. §7 now says that reporting a failure doesn’t substitute for passing. Part II records what is delivered, with commits and runs, and which measurements apply to what. §11 interprets SEP 0: remaining phases go on the one branch, so #394, #398 and #401 close into #415, while #395, #399 and the bug fixes stand alone under §2.R14 taken, with a limit. §6.6 says these are ordinary CPython wheels, separates compatibility from support, names the tested interpreters, and keeps 3.15 a qualification lane until h5py has wheels. It stays independent of the removal. The short run on each platform can’t go through solprop where h5py has no wheel, so it uses native directly there. After the removal.
R15 taken. §6.4 keeps the coexistence check in a dedicated environment with outside packages, keeps the macOS refusal with new advice, and keeps host BLAS management. §7 has the gate. Phase 3.
R16 taken. §2 and §3 bound the speed claim to what was measured and say “no known users”. §3 calls dropping those systems a choice. §9 replaces the blanket closure with a first-pass audit, read for four issues and by title for the rest, with the full audit in phase 3. §6.5 and §8 keep
autoin examples that teach nothing specific to a backend.