Skip to main content

ODE_ENSEMBLE_MIN_LANES

Constant ODE_ENSEMBLE_MIN_LANES 

Source
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.

Lanesserial (run A)speed-up Aspeed-up Bspeed-up C
866.85 us2.66x1.26x0.89x
16132.56 us2.99x2.10x3.12x
32267.99 us1.23x1.53x2.51x
64545.25 us3.07x1.94x2.37x
1281092.75 us3.27x1.96x3.78x
2562212.57 us3.67x2.41x2.99x
1 0248814.43 us3.55x2.94x3.72x
4 09635205.42 us3.67x3.87x3.60x
16 384141229.69 us3.86x3.89x3.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.