From 90ccd51b37ff5fb8c2a7ea6ef48b687f7517d20b Mon Sep 17 00:00:00 2001 From: Muzhen Gaming <61100393+XInTheDark@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:16:40 +0800 Subject: [PATCH] Redefine Nodes Count (#39) Previously, a node was one complete playout from the root position. Now, we redefine nodes count to the number of times `perform_one_iteration()` is called, i.e. for every position we consider. The previous definition has been renamed to "iters" as a more unambiguous term. The benefit of this is a much more consistent NPS value, as opposed to the previous version where NPS would greatly fluctuate between different patches. This patch also includes all search threads in the total nodes count calculation and output, not just the main thread. Bench depth was also decreased from 7 to 6 to reduce the total time a bench takes. Passed non-regression STC: LLR: 3.05 (-2.94,2.94) <-3.50,0.50> Total: 77896 W: 18486 L: 18528 D: 40882 Ptnml(0-2): 1173, 8945, 18710, 8991, 1129 https://montychess.org/tests/view/66ba5f9d3ae9310e136de28e Passed non-regression STC SMP: LLR: 2.97 (-2.94,2.94) <-3.50,0.50> Total: 30404 W: 7246 L: 7172 D: 15986 Ptnml(0-2): 434, 3388, 7475, 3480, 425 https://montychess.org/tests/view/66ba6ff93ae9310e136de30e Bench: 2093550 --- src/chess.rs | 2 +- src/mcts.rs | 93 ++++++++++++++++++++++++++++++---------------------- src/uci.rs | 26 +++++++++++++-- 3 files changed, 78 insertions(+), 43 deletions(-) diff --git a/src/chess.rs b/src/chess.rs index 2eaab574..d355307a 100644 --- a/src/chess.rs +++ b/src/chess.rs @@ -78,7 +78,7 @@ impl Default for ChessState { impl ChessState { pub const STARTPOS: &'static str = STARTPOS; - pub const BENCH_DEPTH: usize = 7; + pub const BENCH_DEPTH: usize = 6; pub fn bbs(&self) -> [u64; 8] { self.board.bbs() diff --git a/src/mcts.rs b/src/mcts.rs index 56832a03..87600c6c 100644 --- a/src/mcts.rs +++ b/src/mcts.rs @@ -11,7 +11,7 @@ use crate::{ }; use std::{ - sync::atomic::{AtomicBool, Ordering}, + sync::atomic::{AtomicBool, AtomicUsize, Ordering}, thread, time::Instant, }; @@ -24,6 +24,14 @@ pub struct Limits { pub max_nodes: usize, } +#[derive(Default)] +pub struct SearchStats { + pub total_nodes: AtomicUsize, + pub total_iters: AtomicUsize, + pub main_iters: AtomicUsize, + pub avg_depth: AtomicUsize, +} + pub struct Searcher<'a> { root_position: ChessState, tree: &'a Tree, @@ -57,24 +65,20 @@ impl<'a> Searcher<'a> { &self, limits: &Limits, timer: &Instant, - nodes: &mut usize, - depth: &mut usize, - cumulative_depth: &mut usize, + search_stats: &SearchStats, best_move: &mut Move, best_move_changes: &mut i32, previous_score: &mut f32, #[cfg(not(feature = "uci-minimal"))] uci_output: bool, ) { - if self.playout_until_full_internal(nodes, cumulative_depth, |n, cd| { + if self.playout_until_full_internal(search_stats, true, || { self.check_limits( limits, timer, - n, + search_stats, best_move, best_move_changes, previous_score, - depth, - cd, #[cfg(not(feature = "uci-minimal"))] uci_output, ) @@ -83,18 +87,18 @@ impl<'a> Searcher<'a> { } } - fn playout_until_full_worker(&self, nodes: &mut usize, cumulative_depth: &mut usize) { - let _ = self.playout_until_full_internal(nodes, cumulative_depth, |_, _| false); + fn playout_until_full_worker(&self, search_stats: &SearchStats) { + let _ = self.playout_until_full_internal(search_stats, false, || false); } fn playout_until_full_internal( &self, - nodes: &mut usize, - cumulative_depth: &mut usize, + search_stats: &SearchStats, + main_thread: bool, mut stop: F, ) -> bool where - F: FnMut(usize, usize) -> bool, + F: FnMut() -> bool, { loop { let mut pos = self.root_position.clone(); @@ -111,8 +115,13 @@ impl<'a> Searcher<'a> { return false; } - *cumulative_depth += this_depth - 1; - *nodes += 1; + search_stats.total_iters.fetch_add(1, Ordering::Relaxed); + search_stats + .total_nodes + .fetch_add(this_depth, Ordering::Relaxed); + if main_thread { + search_stats.main_iters.fetch_add(1, Ordering::Relaxed); + } // proven checkmate if self.tree[self.tree.root_node()].is_terminal() { @@ -124,7 +133,7 @@ impl<'a> Searcher<'a> { return true; } - if stop(*nodes, *cumulative_depth) { + if stop() { return true; } } @@ -135,19 +144,19 @@ impl<'a> Searcher<'a> { &self, limits: &Limits, timer: &Instant, - nodes: usize, + search_stats: &SearchStats, best_move: &mut Move, best_move_changes: &mut i32, previous_score: &mut f32, - depth: &mut usize, - cumulative_depth: usize, #[cfg(not(feature = "uci-minimal"))] uci_output: bool, ) -> bool { - if nodes >= limits.max_nodes { + let iters = search_stats.main_iters.load(Ordering::Relaxed); + + if search_stats.total_iters.load(Ordering::Relaxed) >= limits.max_nodes { return true; } - if nodes % 128 == 0 { + if iters % 128 == 0 { if let Some(time) = limits.max_time { if timer.elapsed().as_millis() >= time { return true; @@ -161,7 +170,7 @@ impl<'a> Searcher<'a> { } } - if nodes % 4096 == 0 { + if iters % 4096 == 0 { // Time management if let Some(time) = limits.opt_time { let (should_stop, score) = SearchHelpers::soft_time_cutoff( @@ -169,7 +178,7 @@ impl<'a> Searcher<'a> { timer, *previous_score, *best_move_changes, - nodes, + iters, time, ); @@ -177,7 +186,7 @@ impl<'a> Searcher<'a> { return true; } - if nodes % 16384 == 0 { + if iters % 16384 == 0 { *best_move_changes = 0; } @@ -190,16 +199,22 @@ impl<'a> Searcher<'a> { } // define "depth" as the average depth of selection - let avg_depth = cumulative_depth / nodes; - if avg_depth > *depth { - *depth = avg_depth; - if *depth >= limits.max_depth { + let total_depth = search_stats.total_nodes.load(Ordering::Relaxed) + - search_stats.total_iters.load(Ordering::Relaxed); + let new_depth = total_depth / search_stats.total_iters.load(Ordering::Relaxed); + if new_depth > search_stats.avg_depth.load(Ordering::Relaxed) { + search_stats.avg_depth.store(new_depth, Ordering::Relaxed); + if new_depth >= limits.max_depth { return true; } #[cfg(not(feature = "uci-minimal"))] if uci_output { - self.search_report(*depth, timer, nodes); + self.search_report( + new_depth, + timer, + search_stats.total_nodes.load(Ordering::Relaxed), + ); } } @@ -211,7 +226,7 @@ impl<'a> Searcher<'a> { threads: usize, limits: Limits, uci_output: bool, - total_nodes: &mut usize, + update_nodes: &mut usize, ) -> (Move, f32) { let timer = Instant::now(); @@ -225,9 +240,7 @@ impl<'a> Searcher<'a> { self.tree[node].expand::(&self.root_position, self.params, self.policy); } - let mut nodes = 0; - let mut depth = 0; - let mut cumulative_depth = 0; + let search_stats = SearchStats::default(); let mut best_move = Move::NULL; let mut best_move_changes = 0; @@ -240,9 +253,7 @@ impl<'a> Searcher<'a> { self.playout_until_full_main( &limits, &timer, - &mut nodes, - &mut depth, - &mut cumulative_depth, + &search_stats, &mut best_move, &mut best_move_changes, &mut previous_score, @@ -252,7 +263,7 @@ impl<'a> Searcher<'a> { }); for _ in 0..threads - 1 { - s.spawn(|| self.playout_until_full_worker(&mut 0, &mut 0)); + s.spawn(|| self.playout_until_full_worker(&search_stats)); } }); @@ -261,10 +272,14 @@ impl<'a> Searcher<'a> { } } - *total_nodes += nodes; + *update_nodes += search_stats.total_nodes.load(Ordering::Relaxed); if uci_output { - self.search_report(depth.max(1), &timer, nodes); + self.search_report( + search_stats.avg_depth.load(Ordering::Relaxed).max(1), + &timer, + search_stats.total_nodes.load(Ordering::Relaxed), + ); } let best_action = self.get_best_action(); diff --git a/src/uci.rs b/src/uci.rs index 77fec142..94a99c01 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -27,6 +27,7 @@ impl Uci { let mut tree = Tree::new_mb(64, 1); let mut report_moves = false; let mut threads = 1; + let mut move_overhead = 40; let mut stored_message: Option = None; @@ -58,6 +59,7 @@ impl Uci { &mut report_moves, &mut tree, &mut threads, + &mut move_overhead, ), "position" => position(commands, &mut pos), "go" => { @@ -75,11 +77,21 @@ impl Uci { policy, value, threads, + move_overhead, &mut stored_message, ); prev = Some(pos.clone()); } + "bench" => { + let depth = if let Some(d) = commands.get(1) { + d.parse().unwrap_or(ChessState::BENCH_DEPTH) + } else { + ChessState::BENCH_DEPTH + }; + + Uci::bench(depth, policy, value, ¶ms); + } "perft" => run_perft(&commands, &pos), "quit" => std::process::exit(0), "eval" => { @@ -169,6 +181,7 @@ fn preamble() { println!("id author Jamie Whiting"); println!("option name Hash type spin default 64 min 1 max 8192"); println!("option name Threads type spin default 1 min 1 max 512"); + println!("option name MoveOverhead type spin default 40 min 0 max 5000"); println!("option name report_moves type button"); Uci::options(); @@ -184,6 +197,7 @@ fn setoption( report_moves: &mut bool, tree: &mut Tree, threads: &mut usize, + move_overhead: &mut usize, ) { if let ["setoption", "name", "report_moves"] = commands { *report_moves = !*report_moves; @@ -200,6 +214,11 @@ fn setoption( return; } + if *x == "MoveOverhead" { + *move_overhead = y.parse().unwrap(); + return; + } + (*x, y.parse::().unwrap_or(0)) } else { return; @@ -259,6 +278,7 @@ fn go( policy: &PolicyNetwork, value: &ValueNetwork, threads: usize, + move_overhead: usize, stored_message: &mut Option, ) { let mut max_nodes = i32::MAX as usize; @@ -313,12 +333,12 @@ fn go( max_time = Some(max_time.unwrap_or(u128::MAX).min(max)); } - // 20ms move overhead + // apply move overhead if let Some(t) = opt_time.as_mut() { - *t = t.saturating_sub(20); + *t = t.saturating_sub(move_overhead as u128); } if let Some(t) = max_time.as_mut() { - *t = t.saturating_sub(20); + *t = t.saturating_sub(move_overhead as u128); } let abort = AtomicBool::new(false);