T3: rayon-backed concurrency (opt-in) #2
100
tests/determinism.rs
Normal file
100
tests/determinism.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//! 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user