CPU improvements after 0.1.0¶
The October 4 CPU comparison measures batched cluster solves, spherical fields, periodic arrays and internal slab illumination against separately rebuilt treams-rs 0.1.0 sources. On one Ryzen 9 9950X host, the largest measured gain in the four-thread algorithm comparison is 12.34× for eight requested illuminations of 512 dipole spheres, and 11.04× for their complete physical gradient. This is a comparison between treams-rs builds, not against Python treams.
The qualification record and raw evidence archive retain the measurements, numerical checks, source identities and exceptions. These measurements precede the separate work on scheduling small dense products; the scheduling qualification below measures that later change separately.
Measured workloads¶
Four threads, complex128/f64, identical inputs and accuracy settings. A gradient time includes its forward calculation and analytic pullback. Speedup is the median of the paired baseline/candidate ratios; it can differ slightly from the ratio of the two displayed time medians.
| Workload | 0.1.0 | Measured candidate | Speedup |
|---|---|---|---|
| 512 dipole spheres, one illumination | 157.63 ms | 59.71 ms | 2.64× |
| 512 dipole spheres, eight illuminations | 1235.19 ms | 100.25 ms | 12.34× |
| Same eight illuminations, complete gradient | 2567.39 ms | 232.64 ms | 11.04× |
| 32 stronger-scattering spheres, degree 3, eight illuminations | 212.09 ms | 62.89 ms | 3.37× |
| Prepared 32-dipole field, 4096 points | 12.19 ms | 5.22 ms | 2.34× |
| Same prepared field, forward and field pullback | 48.44 ms | 26.52 ms | 1.83× |
| Degree-12 sphere near field, 4096 points | 13.45 ms | 6.32 ms | 2.13× |
| Same near field, forward and pullback | 67.82 ms | 39.42 ms | 1.72× |
| Four-sphere periodic array, degree 6, size parameter near one | 57.74 ms | 26.72 ms | 2.16× |
| Same periodic array, complete gradient | 238.11 ms | 111.58 ms | 2.13× |
| Internal illumination, 1024 slab modes | 31.65 ms | 15.53 ms | 2.03× |
| Same internal illumination, complete gradient | 63.30 ms | 47.87 ms | 1.33× |
Cluster times include local coefficients, operator construction and the requested
solve, with incident-wave construction outside timing. Field maps start with
prepared scattering coefficients: the dipole map does not include the preceding
cluster solve or its derivative. Slab cases use prepared matrices. The archive's
compare_builds-final.py snapshot specifies every case.
The changes share translation work across independently converging right-hand sides, reuse spherical angular and radial calculations, share Ewald terms, and apply thin internal-illumination operators without first forming their dense product. They retain analytic derivatives and the original accuracy gates.
These methods have limits. Dipole order is a model, not a general multipole
convergence claim. The recorded degree-12 near field differs from degree 14 by
3.18e-9 in relative norm; the degree-6 periodic case differs from degree 8 by
9.68e-9. Those are cutoff checks for the stated inputs. In a separate 32-sphere
strong-scattering diagnostic, selected dense LU remains about five times faster
than the improved iterative solver for a fresh solve. That diagnostic tightens
the iterative residual to 1e-12 after a strict componentwise comparison failed
at 1e-10; both the failure and the successful diagnostic are archived.
Memory and retained failures¶
The speedups use additional temporary storage in some cases. For the 512-dipole, eight-illumination gradient, whole-process peak RSS changes from 57.97 to 60.69 MiB; growth above the post-setup high-water mark changes from 4.75 to 9.30 MiB. Eight independent Krylov spaces account for additional storage.
Four periodic degree-6 cases exceed the unchanged 5% call-growth memory limit: both forward and gradient cases at the two measured size parameters. Gradient growth increases by 4.68–6.34 MiB, while whole-process peak increases by approximately 2–4 MiB. The size-parameter-near-one gradient changes from 86.57 to 88.57 MiB peak RSS. These memory failures are retained; faster execution is not a claim of universally lower memory. Source-based scratch bounds are recorded separately from measured process peaks.
All 36 targeted four-thread timing comparisons pass, with maximum output
difference 1.30e-14 under the 1e-12 comparison gate. The broader 92-case screen
has four original timing failures. Separate same-build controls and longer
seven-round rechecks pass all four without changing the limits. The small
64-mode stack still shows about 60 microseconds of overhead in that recheck,
within the existing 10% limit for submillisecond calls. The original failures
remain in the archive; the rechecks do not establish zero regressions.
The one- and sixteen-thread screens pass all 17 cases. At 32 threads, all 16 optimization targets pass, while the additional small-stack timing fails its initial screen. Its same-build control and longer paired recheck pass. The pre-scheduling implementation is inefficient for that small workload at large thread budgets, and this record makes no claim about the subsequent scheduling change.
Measurement and provenance¶
The host was a Ryzen 9 9950X with 16 physical cores, Linux, Python 3.13.1,
NumPy 2.5.3 and Rust 1.94.0. Both extensions used the repository release profile.
The primary comparison pinned four workers to CPUs 8–11, with seven paired
rounds and three samples of at least 60 ms each. A fresh process runs each tree
and case, sequentially in alternating order, so inactive numerical thread pools
cannot compete. Imports and input setup are excluded. Linux memory measurements
use the worker's VmHWM, with three fresh-process repetitions per tree and case.
The baseline source is 8bbcb87aaeaccd9293fc8c09eecb8727c37c4970; its numerical
and Python sources match tag v0.1.0. The measured candidate was an uncommitted
patch on that source, identified by patch SHA-256
72039ec484ab21902cc034cb12835841eed69360e9de60f385075d1d432f0950 and native-library
SHA-256 68919ce62085ca79a34719aa03a1171c2da530cab33da4559c3d9b5a1148e4f4.
The archive includes that patch and the exact comparison-harness snapshot.
Later commits or releases are not the measured revision.
Local just ci passed 262 Rust tests and 6625 Python tests, with two ignored Rust
tests and 22 skipped Python tests. Additional physical, reference, gradient and
cutoff diagnostics are included. The full historical 1300-case benchmark grid
was not replayed; no macOS, Windows or GPU performance is established here.
Earlier exploratory runs exposed two harness errors: inherited ru_maxrss
invalidated their memory results, and concurrent idle BLAS pools distorted some
timings. The final measurements use corrected isolation, actual minimum-duration
batches and worker-local memory accounting. The archive identifies the rejected
measurements and retains the diagnostic evidence. Privacy-only distribution
changes are recorded in privacy provenance;
numerical values, timing samples and outcomes are unchanged.
Reproduce¶
Extract the archive from the repository root. Reconstruct the measured candidate
from its stated base and stage6-final.patch, and use compare_builds-final.py
as the scripts/compare_builds.py snapshot. Build baseline and candidate package
trees separately with just build-ext-release; a rebuilt binary receives its
own identity. On an otherwise idle Linux host, run:
taskset -c 8-11 uv run --no-sync python scripts/compare_builds.py \
--baseline /path/to/built-baseline --candidate /path/to/built-candidate \
--match '^(iterative-|mie-nearfield|prepared-dipole|periodic-array|slab-internal)' \
--threads 4 --rounds 7 --samples 3 --min-sample 0.06 --memory \
--output benchmarks/results/local/cpu-comparison.json
The archive manifest gives each original and distributed file's digest. Its
README distinguishes replay inputs, final results, exploratory diagnostics and
superseded measurements. New runs belong under benchmarks/results/local/.
Small-product scheduling¶
A separate follow-up limits the workers used for a dense matrix product to
min(thread_budget, floor(m*n*k / 65536)), retaining the existing serial cutoff
and vector-product rules. Small products previously requested the entire thread
budget, so scheduling overhead could dominate at 32 threads. A 64-by-64 product
now requests four workers even when the application allows 32.
The scheduling qualification and separate raw archive record the selected build against the original 0.1.0 baseline. At 32 threads, all 12 cases pass the unchanged timing and numerical gates across seven paired rounds, using three samples of at least 80 ms per round:
| Workload, 32-thread budget | 0.1.0 | Selected candidate | Paired speedup |
|---|---|---|---|
| 64-mode stack power | 17.376 ms | 1.143 ms | 15.35× |
| Four-sphere dense cluster solve | 6.854 ms | 0.700 ms | 10.00× |
| 16-sphere dense cluster solve | 36.090 ms | 14.082 ms | 2.59× |
These gains remove excessive scheduling overhead at a large thread budget; they do not imply that 32 threads is the fastest setting. The selected 64-mode stack takes 0.765 ms with four threads. In seven-round comparisons at four threads, the selected stack and 16-sphere solve have paired time ratios of 1.017 and 1.020 against 0.1.0, both within their existing limits.
One intermediate regression remains: the selected 16-sphere solve is 29.8% slower than the preceding stage6 build at four threads, with medians of 5.623 and 7.159 ms. Against the released-source baseline, its medians are 7.248 and 7.314 ms. A temporary exception preserving the old policy at budgets of four or fewer did not fix the intermediate regression; that rejected variant was 32.9% slower than stage6. The archive retains all five failed intermediate timing gates, the null control, rechecks and rejected implementation. The cause of the intermediate advantage is unresolved.
A final three-round four-thread spot check retains the main algorithm gains: 512 dipoles with eight illuminations take 1.252 s versus 0.103 s for the selected build (12.21×), and the complete gradient takes 2.563 s versus 0.235 s (10.90×). The selected build includes the preceding CPU changes and translation-plan allocation pruning, so these comparisons measure the combined build. They do not assign every gain to scheduling.
Six selected-build forward/gradient workflows produce identical output bits across native budgets of 1, 4, 16 and 32 with BLAS held to one thread. Final comparative timings cover four and 32 threads; the one- and sixteen-thread speed screens used an earlier implementation of the same cap. No additional memory measurement was made for scheduling, and the preceding periodic-memory exceptions remain. These results still cover one Linux host and a selected workload set, not the full historical grid.
The selected native-library SHA-256 is
41748dd504e744b58c14842390c6aa5015591328c3bfe7f3de5e5f4998a3c6dd.
Its source inventory is reconstructed from the immediately following rejected
variant, restoring the selected scheduling implementation; it was not captured
at the selected build's creation. The archive's selected-reconstructed.patch
applies to the same 8bbcb87 base and reproduces all 180 recorded native,
Python and Cargo-file hashes. Two supporting build inputs have separately
recorded publication-time hashes. This does not reconstruct the entire as-run
documentation and test tree.
For replay, apply that patch, build separate baseline and candidate package
trees, and restore the archived compare_builds-as-run.py as
scripts/compare_builds.py, with _harness.py beside it. The harness was
simplified after timing, so use the snapshot
whose SHA-256 is
925aeb12247fc09ae0d7d14e3b9fee26f601b4273e10ce2b5696c321b7c38361.
The 32-thread qualification uses:
taskset -c 0-31 uv run --no-sync python scripts/compare_builds.py \
--baseline /path/to/built-baseline --candidate /path/to/built-candidate \
--match '^(cluster-solve-n(2|4|16)|stack-power-h(2|16|64|256)|periodic-to-smatrix-l3|periodic-array-n4-l6-size1-(forward|gradient)|slab-internal-h1024-(forward|gradient))$' \
--threads 32 --rounds 7 --samples 3 --min-sample 0.08 \
--output benchmarks/results/local/cpu-scheduling-replay.json