tests/determinism.rs runs the same deterministic 200-event history
at thread counts {1, 2, 4, 8} via rayon::ThreadPoolBuilder::install
and asserts every (time, posterior) pair has bit-identical mu and
sigma across all configurations.
Cfg-gated to the rayon feature; no-op under --features approx alone.
Verifies the T3 determinism invariant that the ordered-reduce
strategy (per-slice parallel, sequential sum) produces thread-count-
independent results.
Part of T3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.3 KiB
Rust
101 lines
3.3 KiB
Rust
//! Determinism tests: identical posteriors across RAYON_NUM_THREADS
|
|
//! values. Only compiled with the `rayon` feature.
|
|
|
|
#![cfg(feature = "rayon")]
|
|
|
|
use smallvec::smallvec;
|
|
use trueskill_tt::{ConstantDrift, ConvergenceOptions, Event, History, Member, Outcome, Team};
|
|
|
|
/// Build a deterministic workload using a simple LCG (no external rand crate).
|
|
fn build_and_converge(seed: u64) -> Vec<(i64, trueskill_tt::Gaussian)> {
|
|
let mut h = History::<i64, _, _, String>::builder_with_key()
|
|
.mu(25.0)
|
|
.sigma(25.0 / 3.0)
|
|
.beta(25.0 / 6.0)
|
|
.drift(ConstantDrift(25.0 / 300.0))
|
|
.convergence(ConvergenceOptions {
|
|
max_iter: 30,
|
|
epsilon: 1e-6,
|
|
})
|
|
.build();
|
|
|
|
// LCG for deterministic pseudo-random ints.
|
|
let mut rng = seed;
|
|
let mut next = || {
|
|
rng = rng
|
|
.wrapping_mul(6364136223846793005)
|
|
.wrapping_add(1442695040888963407);
|
|
rng
|
|
};
|
|
|
|
let mut events: Vec<Event<i64, String>> = Vec::with_capacity(200);
|
|
for ev_i in 0..200 {
|
|
let a = (next() % 40) as usize;
|
|
let mut b = (next() % 40) as usize;
|
|
while b == a {
|
|
b = (next() % 40) as usize;
|
|
}
|
|
// ~10 events per slice so color groups have material parallelism.
|
|
events.push(Event {
|
|
time: (ev_i as i64 / 10) + 1,
|
|
teams: smallvec![
|
|
Team::with_members([Member::new(format!("p{a}"))]),
|
|
Team::with_members([Member::new(format!("p{b}"))]),
|
|
],
|
|
outcome: Outcome::winner((next() % 2) as u32, 2),
|
|
});
|
|
}
|
|
h.add_events(events).unwrap();
|
|
h.converge().unwrap();
|
|
// Sample one competitor's curve for the comparison.
|
|
h.learning_curve("p0")
|
|
}
|
|
|
|
#[test]
|
|
fn posteriors_identical_across_thread_counts() {
|
|
let sizes = [1usize, 2, 4, 8];
|
|
let mut results: Vec<Vec<(i64, trueskill_tt::Gaussian)>> = Vec::new();
|
|
for &n in &sizes {
|
|
let pool = rayon::ThreadPoolBuilder::new()
|
|
.num_threads(n)
|
|
.build()
|
|
.expect("rayon pool build");
|
|
let curve = pool.install(|| build_and_converge(42));
|
|
results.push(curve);
|
|
}
|
|
|
|
let reference = &results[0];
|
|
for (i, curve) in results.iter().enumerate().skip(1) {
|
|
assert_eq!(
|
|
curve.len(),
|
|
reference.len(),
|
|
"curve length differs at {n} threads",
|
|
n = sizes[i],
|
|
);
|
|
for (j, (&(t_ref, g_ref), &(t, g))) in reference.iter().zip(curve.iter()).enumerate() {
|
|
assert_eq!(
|
|
t_ref,
|
|
t,
|
|
"time point {j} differs at {n} threads: ref={t_ref} vs got={t}",
|
|
n = sizes[i],
|
|
);
|
|
assert_eq!(
|
|
g_ref.mu().to_bits(),
|
|
g.mu().to_bits(),
|
|
"mu bits differ at {n} threads, time {t}: ref={ref_mu} got={got_mu}",
|
|
n = sizes[i],
|
|
ref_mu = g_ref.mu(),
|
|
got_mu = g.mu(),
|
|
);
|
|
assert_eq!(
|
|
g_ref.sigma().to_bits(),
|
|
g.sigma().to_bits(),
|
|
"sigma bits differ at {n} threads, time {t}: ref={ref_sigma} got={got_sigma}",
|
|
n = sizes[i],
|
|
ref_sigma = g_ref.sigma(),
|
|
got_sigma = g.sigma(),
|
|
);
|
|
}
|
|
}
|
|
}
|