From 638950fe33c952b33b7c2f6fa41f488471ea25af Mon Sep 17 00:00:00 2001 From: Bruno Dutra Date: Sat, 9 Sep 2023 23:00:44 +0200 Subject: [PATCH] make parallel PVS more deterministic --- bin/engine.rs | 14 ++---- lib/search/depth.rs | 6 +++ lib/search/ply.rs | 6 +++ lib/search/pv.rs | 79 +++------------------------------ lib/search/pvs.rs | 104 +++++++++++++++++--------------------------- 5 files changed, 62 insertions(+), 147 deletions(-) diff --git a/bin/engine.rs b/bin/engine.rs index 085e20d2..0c6a38f8 100644 --- a/bin/engine.rs +++ b/bin/engine.rs @@ -17,7 +17,7 @@ trait Searcher { impl MockSearcher { fn search(&mut self, pos: &Position, limits: Limits) -> Pv { let pv = Searcher::search(self, pos, limits); - Pv::new(pv.score(), pv.depth(), pv.ply(), pv) + Pv::new(pv.score(), pv.depth(), pv) } fn with_options(_: Options) -> Self { @@ -93,7 +93,7 @@ impl Ai for Engine { mod tests { use super::*; use futures_util::StreamExt; - use lib::search::{Ply, Score}; + use lib::search::Score; use mockall::predicate::eq; use proptest::sample::size_range; use std::time::Duration; @@ -110,15 +110,9 @@ mod tests { #[proptest(async = "tokio")] #[should_panic] - async fn play_panics_if_there_are_no_legal_moves( - l: Limits, - pos: Position, - s: Score, - d: Depth, - p: Ply, - ) { + async fn play_panics_if_there_are_no_legal_moves(l: Limits, pos: Position, s: Score, d: Depth) { let mut strategy = Strategy::new(); - strategy.expect_search().return_const(Pv::new(s, d, p, [])); + strategy.expect_search().return_const(Pv::new(s, d, [])); let mut engine = Engine { strategy }; engine.play(&pos, l).await; diff --git a/lib/search/depth.rs b/lib/search/depth.rs index 3181cff5..5ae21f04 100644 --- a/lib/search/depth.rs +++ b/lib/search/depth.rs @@ -5,8 +5,14 @@ pub struct DepthBounds; impl Bounds for DepthBounds { type Integer = u8; + const LOWER: Self::Integer = 0; + + #[cfg(not(test))] const UPPER: Self::Integer = 31; + + #[cfg(test)] + const UPPER: Self::Integer = 3; } /// The search depth. diff --git a/lib/search/ply.rs b/lib/search/ply.rs index b73d61ab..45222444 100644 --- a/lib/search/ply.rs +++ b/lib/search/ply.rs @@ -4,8 +4,14 @@ pub struct PlyBounds; impl Bounds for PlyBounds { type Integer = i8; + const LOWER: Self::Integer = -Self::UPPER; + + #[cfg(not(test))] const UPPER: Self::Integer = 127; + + #[cfg(test)] + const UPPER: Self::Integer = 3; } /// The number of half-moves played. diff --git a/lib/search/pv.rs b/lib/search/pv.rs index ffe1b7da..12e28f49 100644 --- a/lib/search/pv.rs +++ b/lib/search/pv.rs @@ -1,4 +1,4 @@ -use crate::search::{Depth, DepthBounds, Line, Ply, Score}; +use crate::search::{Depth, DepthBounds, Line, Score}; use crate::{chess::Move, util::Bounds}; use derive_more::{Deref, IntoIterator}; use std::{cmp::Ordering, iter::once, mem, ops::Neg}; @@ -11,8 +11,6 @@ use test_strategy::Arbitrary; pub struct Pv { score: Score, depth: Depth, - #[filter(#ply >= 0)] - ply: Ply, #[deref] #[into_iterator(owned, ref, ref_mut)] line: Line, @@ -20,33 +18,14 @@ pub struct Pv { impl Pv { /// Constructs a pv. - pub fn new(score: Score, depth: Depth, ply: Ply, line: I) -> Self - where - I: IntoIterator, - { + pub fn new>(score: Score, depth: Depth, line: I) -> Self { Pv { score, depth, - ply, - line: line.into_iter().collect(), + line: Line::from_iter(line), } } - /// Constructs a pv leaf. - pub fn leaf(score: Score, depth: Depth, ply: Ply) -> Self { - Self::new(score, depth, ply, []) - } - - /// Constructs a drawn pv leaf. - pub fn drawn(depth: Depth, ply: Ply) -> Self { - Self::leaf(Score::new(0), depth, ply) - } - - /// Constructs a lost pv leaf. - pub fn lost(depth: Depth, ply: Ply) -> Self { - Self::leaf(Score::LOWER.normalize(ply), depth, ply) - } - /// The score from the point of view of the side to move. pub fn score(&self) -> Score { self.score @@ -57,24 +36,6 @@ impl Pv { self.depth } - /// The ply reached. - pub fn ply(&self) -> Ply { - if self.ply < 0 { - -self.ply - } else { - self.ply - } - } - - /// The tempo bonus from the point of view of the side to move. - pub fn tempo(&self) -> Ply { - if self.ply < 0 { - -(self.ply + self.depth) - } else { - -(self.ply - self.depth) - } - } - /// The strongest [`Line`]. pub fn line(&self) -> &Line { &self.line @@ -90,7 +51,7 @@ impl Pv { impl Ord for Pv { fn cmp(&self, other: &Self) -> Ordering { - (self.score(), self.tempo()).cmp(&(other.score(), other.tempo())) + self.score.cmp(&other.score) } } @@ -122,7 +83,7 @@ impl Neg for Pv { type Output = Self; fn neg(self) -> Self::Output { - Pv::new(-self.score, self.depth, -self.ply, self.line) + Pv::new(-self.score, self.depth, self.line) } } @@ -141,11 +102,6 @@ mod tests { assert_eq!(pv.depth(), pv.depth); } - #[proptest] - fn ply_returns_ply(pv: Pv<3>) { - assert_eq!(pv.ply().get(), pv.ply.get().abs()); - } - #[proptest] fn line_returns_line(pv: Pv<3>) { assert_eq!(pv.line(), &pv.line); @@ -156,21 +112,11 @@ mod tests { assert_eq!(pv.clone().neg().score(), -pv.score()); } - #[proptest] - fn negation_changes_tempo(#[filter(#pv.ply() > 0)] pv: Pv<3>) { - assert_eq!(pv.clone().neg().tempo(), -pv.tempo()); - } - #[proptest] fn negation_preserves_depth(pv: Pv<3>) { assert_eq!(pv.clone().neg().depth(), pv.depth()); } - #[proptest] - fn negation_preserves_ply(pv: Pv<3>) { - assert_eq!(pv.clone().neg().ply(), pv.ply()); - } - #[proptest] fn negation_preserves_line(pv: Pv<3>) { assert_eq!(pv.clone().neg().line(), pv.line()); @@ -190,19 +136,4 @@ mod tests { fn pv_with_larger_score_is_larger(p: Pv<3>, #[filter(#p.score() != #q.score())] q: Pv<3>) { assert_eq!(p < q, p.score() < q.score()); } - - #[proptest] - fn pvs_with_same_score_are_compared_by_tempo( - s: Score, - dp: Depth, - dq: Depth, - pp: Ply, - pq: Ply, - lp: Line<3>, - lq: Line<3>, - ) { - let p = Pv::<3>::new(s, dp, pp, lp); - let q = Pv::<3>::new(s, dq, pq, lq); - assert_eq!(p < q, p.tempo() < q.tempo()); - } } diff --git a/lib/search/pvs.rs b/lib/search/pvs.rs index 4ce0ae89..511d06fc 100644 --- a/lib/search/pvs.rs +++ b/lib/search/pvs.rs @@ -27,12 +27,6 @@ impl Default for Searcher { } impl Searcher { - #[cfg(not(test))] - const MAX_PLY: i8 = Ply::UPPER.get() / 2; - - #[cfg(test)] - const MAX_PLY: i8 = 3; - /// Constructs [`Searcher`] with the default [`Options`]. pub fn new() -> Self { Self::with_options(Options::default()) @@ -160,8 +154,8 @@ impl Searcher { timer.elapsed()?; let in_check = pos.is_check(); let zobrist = match pos.outcome() { - Some(o) if o.is_draw() => return Ok(Pv::drawn(depth, ply)), - Some(_) => return Ok(Pv::lost(depth, ply)), + Some(o) if o.is_draw() => return Ok(Pv::new(Score::new(0), depth, [])), + Some(_) => return Ok(Pv::new(Score::LOWER.normalize(ply), depth, [])), None => pos.zobrist(), }; @@ -169,8 +163,8 @@ impl Searcher { if alpha >= beta { return match tpos { - Some(t) => Ok(Pv::new(t.score().normalize(ply), depth, ply, [t.best()])), - None => Ok(Pv::leaf(alpha, depth, ply)), + Some(t) => Ok(Pv::new(t.score().normalize(ply), depth, [t.best()])), + None => Ok(Pv::new(alpha, depth, [])), }; } @@ -180,8 +174,8 @@ impl Searcher { None => pos.value().cast(), }; - if ply >= Searcher::MAX_PLY { - return Ok(Pv::leaf(score, depth, ply)); + if ply >= Ply::UPPER { + return Ok(Pv::new(score, depth, [])); } else if !in_check { if let Some(d) = self.nmp(pos, score, beta, depth) { let mut next = pos.clone(); @@ -189,7 +183,7 @@ impl Searcher { if d <= ply || -self.nw::<1>(&next, -beta + 1, d, ply + 1, timer)? >= beta { #[cfg(not(test))] // The null move pruning heuristic is not exact. - return Ok(Pv::leaf(score, depth, ply)); + return Ok(Pv::new(score, depth, [])); } } } @@ -215,8 +209,8 @@ impl Searcher { } }); - let pv = match moves.pop() { - None => return Ok(Pv::leaf(score, depth, ply)), + let best = match moves.pop() { + None => return Ok(Pv::new(score, depth, [])), Some((m, _)) => { let mut next = pos.clone(); next.play(m).expect("expected legal move"); @@ -231,12 +225,14 @@ impl Searcher { } }; - let cutoff = AtomicI16::new(pv.score().max(alpha).get()); + let cutoff = AtomicI16::new(best.score().max(alpha).get()); - let pv = moves + let (best, _) = moves .into_par_iter() + .copied() + .enumerate() .rev() - .map(|&(m, guess)| { + .map(|(n, (m, guess))| { let alpha = Score::new(cutoff.load(Ordering::Relaxed)); if alpha >= beta { @@ -257,20 +253,20 @@ impl Searcher { } let pv = match -self.nw(&next, -alpha, depth, ply + 1, timer)? { - pv if pv < alpha => return Ok(Some(pv.shift(m))), + pv if pv < alpha => return Ok(Some((pv.shift(m), n))), _ => -self.pvs(&next, -beta..-alpha, depth, ply + 1, timer)?, }; cutoff.fetch_max(pv.score().get(), Ordering::Relaxed); - Ok(Some(pv.shift(m))) + Ok(Some((pv.shift(m), n))) }) - .chain([Ok(Some(pv))]) + .chain([Ok(Some((best, usize::MAX)))]) .try_reduce(|| None, |a, b| Ok(max(a, b)))? .expect("expected at least one principal variation"); - self.record(zobrist, bounds, depth, ply, pv.score(), pv[0]); + self.record(zobrist, bounds, depth, ply, best.score(), best[0]); - Ok(pv) + Ok(best) } /// An implementation of [aspiration windows] with [iterative deepening]. @@ -278,7 +274,7 @@ impl Searcher { /// [aspiration windows]: https://www.chessprogramming.org/Aspiration_Windows /// [iterative deepening]: https://www.chessprogramming.org/Iterative_Deepening fn aw(&self, pos: &Position, depth: Depth, timer: Timer) -> Pv { - let mut best = Pv::drawn(Depth::new(0), Ply::new(0)); + let mut best = Pv::new(Score::new(0), Depth::new(0), []); 'id: for d in 0..=depth.get() { let mut w: i16 = 32; @@ -352,7 +348,7 @@ mod tests { let kind = if ply < depth { MoveKind::ANY - } else if ply < Searcher::MAX_PLY { + } else if ply < Ply::UPPER { MoveKind::CAPTURE | MoveKind::PROMOTION } else { return score; @@ -419,7 +415,11 @@ mod tests { ) { let pos = Evaluator::borrow(&pos); let timer = Timer::start(Duration::MAX); - assert_eq!(s.pvs(&pos, b, d, p, timer), Ok(Pv::<1>::drawn(d, p))); + + assert_eq!( + s.pvs(&pos, b, d, p, timer), + Ok(Pv::<1>::new(Score::new(0), d, [])) + ); } #[proptest] @@ -432,7 +432,11 @@ mod tests { ) { let pos = Evaluator::borrow(&pos); let timer = Timer::start(Duration::MAX); - assert_eq!(s.pvs(&pos, b, d, p, timer), Ok(Pv::<1>::lost(d, p))); + + assert_eq!( + s.pvs(&pos, b, d, p, timer), + Ok(Pv::<1>::new(Score::LOWER.normalize(p), d, [])) + ); } #[proptest] @@ -452,14 +456,14 @@ mod tests { let pos = Evaluator::borrow(&pos); let timer = Timer::start(Duration::MAX); - assert_eq!(s.pvs(&pos, b, d, p, timer), Ok(Pv::<1>::new(sc, d, p, [m]))); + assert_eq!(s.pvs(&pos, b, d, p, timer), Ok(Pv::<1>::new(sc, d, [m]))); } #[proptest] fn pvs_returns_pv_of_the_given_depth( s: Searcher, pos: Position, - #[filter((1..=3).contains(&#d.get()))] d: Depth, + d: Depth, #[filter(#p >= 0)] p: Ply, ) { let pos = Evaluator::borrow(&pos); @@ -470,26 +474,7 @@ mod tests { } #[proptest] - fn pvs_returns_pv_of_greater_ply( - s: Searcher, - pos: Position, - #[filter((1..=3).contains(&#d.get()))] d: Depth, - #[filter(#p >= 0)] p: Ply, - ) { - let pos = Evaluator::borrow(&pos); - let timer = Timer::start(Duration::MAX); - let bounds = Score::LOWER..Score::UPPER; - - assert!(s.pvs::<1>(&pos, bounds, d, p, timer)?.ply() >= p); - } - - #[proptest] - fn pvs_finds_best_score( - s: Searcher, - pos: Position, - #[filter((1..=3).contains(&#d.get()))] d: Depth, - #[filter(#p >= 0)] p: Ply, - ) { + fn pvs_finds_best_score(s: Searcher, pos: Position, d: Depth, #[filter(#p >= 0)] p: Ply) { let pos = Evaluator::borrow(&pos); let timer = Timer::start(Duration::MAX); let bounds = Score::LOWER..Score::UPPER; @@ -498,31 +483,28 @@ mod tests { } #[proptest] - fn pvs_does_not_depend_on_table_size( + fn pvs_does_not_depend_on_configuration( x: Options, y: Options, pos: Position, - #[filter((1..=3).contains(&#d.get()))] d: Depth, + d: Depth, #[filter(#p >= 0)] p: Ply, ) { let x = Searcher::with_options(x); let y = Searcher::with_options(y); let pos = Evaluator::borrow(&pos); + let bounds = Score::LOWER..Score::UPPER; let timer = Timer::start(Duration::MAX); assert_eq!( - x.pvs::<1>(&pos, Score::LOWER..Score::UPPER, d, p, timer), - y.pvs::<1>(&pos, Score::LOWER..Score::UPPER, d, p, timer) + x.pvs::<1>(&pos, bounds.clone(), d, p, timer)?.score(), + y.pvs::<1>(&pos, bounds, d, p, timer)?.score() ); } #[proptest] - fn search_finds_the_principal_variation( - mut s: Searcher, - pos: Position, - #[filter((1..=3).contains(&#d.get()))] d: Depth, - ) { + fn search_finds_the_principal_variation(mut s: Searcher, pos: Position, d: Depth) { let pos = Evaluator::borrow(&pos); let timer = Timer::start(Duration::MAX); let bounds = Score::LOWER..Score::UPPER; @@ -534,11 +516,7 @@ mod tests { } #[proptest] - fn search_is_stable( - mut s: Searcher, - pos: Position, - #[filter((1..=3).contains(&#d.get()))] d: Depth, - ) { + fn search_is_stable(mut s: Searcher, pos: Position, d: Depth) { assert_eq!( s.search::<3>(&pos, Limits::Depth(d)).score(), s.search::<3>(&pos, Limits::Depth(d)).score()