pub const ODE_ENSEMBLE_MIN_LANES: usize = 16;Expand description
Lane count below which a ComputeBackend::CpuMulti request runs
integrate_ensemble on the calling thread instead.
§Measured crossover
Methodology. Measured 2026-08-13 on this workspace’s development machine,
std::thread::available_parallelism() = 4, release build, --features parallel, rayon’s global pool, machine otherwise idle (see the
Contention note below, which is not a footnote — it changes the answer).
Ensembles of n independent one-equation decay problems dy/dx = -k_i y,
y(0) = 1, integrated from x = 0 to x = 1 by
Rkf45 with abs_tol = 1e-10, rel_tol = 1e-8,
initial step 0.1. Half the lanes are given k_i in [0.5, 1.5) and half
in [50, 70), so the ensemble is deliberately imbalanced: on the 4 096-lane
case the accepted-step count averages 67.9 per lane against a
maximum of 126. Each lane costs about 8.6 us of serial work. Best of 7
samples per point, wall clock for one whole ensemble. Produced by the
#[ignore]d ensemble_crossover_benchmark test in parallel/tests.rs and
transcribed from its printed output. Three independent runs are carried
side by side rather than averaged, because the parallel column is far
noisier than the serial one and the spread is the finding.
| Lanes | serial (run A) | speed-up A | speed-up B | speed-up C |
|---|---|---|---|---|
| 8 | 66.85 us | 2.66x | 1.26x | 0.89x |
| 16 | 132.56 us | 2.99x | 2.10x | 3.12x |
| 32 | 267.99 us | 1.23x | 1.53x | 2.51x |
| 64 | 545.25 us | 3.07x | 1.94x | 2.37x |
| 128 | 1092.75 us | 3.27x | 1.96x | 3.78x |
| 256 | 2212.57 us | 3.67x | 2.41x | 2.99x |
| 1 024 | 8814.43 us | 3.55x | 2.94x | 3.72x |
| 4 096 | 35205.42 us | 3.67x | 3.87x | 3.60x |
| 16 384 | 141229.69 us | 3.86x | 3.89x | 3.74x |
Result. 16 is the smallest size at which the parallel path won in all three runs and kept winning at every larger size in all three, and it is the value this constant takes. At 8 lanes it lost run C (0.89x).
How firm is that. Not very, and the table says so honestly. Between 16 and 128 lanes the sign of the effect is consistent — the parallel path never loses — but the magnitude is not resolved: 32 lanes gave 1.23x and 2.51x in two runs of the same code on the same data. Anywhere in 16–128 would be defensible on this evidence. What the table does establish firmly is the plateau: from 1 024 lanes upward the speed-up sits at 3.5–3.9x on 4 logical cores, run to run, which is close to the ideal and is the expected signature of a compute-bound kernel with no cross-lane traffic.
Interpretation. This is the lowest crossover measured anywhere in the crate
— 16x below crate::math::parallel::ROOT_BATCH_MIN_PROBLEMS (256), 256x
below the crate-wide placeholder
crate::compute::CPU_MULTI_MIN_WORK_ITEMS (4 096) and 8 192x below
crate::fields::parallel::FIELD_PARALLEL_CROSSOVER (131 072). The reason
is structural: one lane here is 68 adaptive steps, each of them six
derivative evaluations, against a state vector of one f64. At about 8.6 us
per lane it is by a wide margin the most compute-dense per work item of the
crate’s measured kernels, so rayon’s dispatch cost is amortised almost
immediately. Five kernel families have now been measured in this crate and
they want 16, 256, 4 096, 131 072 and 262 144 — a spread of 16 384x,
which is the strongest evidence yet that no single crate-wide threshold can
be right.
The corollary is the one the root finder also found, sharpened here:
the crossover is set by the caller’s problem, not by this module. A lane
integrating a short interval, or a two-equation system to a loose tolerance,
is an order of magnitude cheaper and crosses over correspondingly later. A
caller whose lanes are very cheap should pass ComputeBackend::Serial
explicitly rather than trust this number.
§Contention — this threshold assumes the cores are actually free
Three earlier runs of the same benchmark were taken while an unrelated
rustc was using ~234% CPU on the same 4-core machine (load average 5.08),
and they are not the table above. Under that load the parallel path lost
at 8 lanes in every run and lost once at 64 lanes, and the plateau speed-up
fell from 3.5–3.9x to 2.9–3.9x. The lesson generalises beyond this
measurement: a threshold measured on an idle machine is optimistic for a
process sharing cores with anything else — an MPI rank per core, a coupled
solver threading elsewhere, or a CI box running several jobs. In those
settings prefer an explicit ComputeBackend.
§Limitations
One machine, four logical cores, one system family (scalar linear decay),
one stepper (Rkf45). Not measured on Android/Termux hardware, not on a
many-core server, and not with Rosenbrock23,
whose per-lane cost includes an LU factorisation and is therefore higher
still — meaning this floor is conservative for stiff ensembles rather than
wrong for them.
§Units
A count of independent initial-value problems, dimensionless.