Expand description
Many independent samples, run in parallel, with an answer that does not depend on how many threads did the work.
The other axis of parallelism in this workspace. TreeNBody::with_threads splits one
evaluation across cores; this splits many evaluations, which is the shape a Monte Carlo
study has and the shape a parameter sweep has. A hundred thousand detector realisations, a
thousand trajectories from perturbed initial conditions, a grid of designs each run to a
steady state — all the same problem: independent work items and a reduction at the end.
§Why this can be parallel and still bit-for-bit
Two decisions that were already made, meeting.
Rng::for_index is stateless and addressed by index, so sample i draws the same numbers
whether it ran first, last, or on another core. Nothing is consumed from a shared stream and
there is no order to depend on.
And results land in a slot chosen by index, never appended. Each worker owns a disjoint run
of the output and writes nothing else, exactly as TreeNBody does — so the vector that comes
back is a function of (seed, count) alone. Reduce it however you like; a fold over that
vector is in index order by construction.
The failure this avoids is worth naming, because it is the usual one: a Monte Carlo that draws from a shared generator gives a different answer on eight cores than on one, and the difference looks like statistical noise. It is not noise, it is the result depending on the scheduler, and no amount of averaging removes it.
use dualis_core::{Ensemble, Rng};
// A hundred thousand throws of a loaded die, in parallel.
let hits = Ensemble::new(20, 100_000)
.with_threads(8)
.run(|_, mut rng| u64::from(rng.unit() < 0.25));
let heads: u64 = hits.iter().sum();
// Same seed, same count, same answer — on one thread or on eight.
assert_eq!(heads, Ensemble::new(20, 100_000).run(|_, mut rng| u64::from(rng.unit() < 0.25))
.iter().sum::<u64>());