diff --git a/Cargo.lock b/Cargo.lock index 22f0b1c..2bfb579 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -325,6 +325,7 @@ dependencies = [ "memmap", "nohash-hasher", "once_cell", + "scoped_threadpool", ] [[package]] @@ -402,6 +403,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 07cf604..67ef2fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ goober = { git = "https://github.com/jw1912/goober/", rev = "32b9b52" } nohash-hasher = "=0.2.0" memmap = "=0.7.0" once_cell = "=1.19.0" +scoped_threadpool = "=0.1.9" [features] no-policy-net = [] diff --git a/bin/Dockerfile.fastchess b/bin/Dockerfile.fastchess index 4da03dc..d8473ee 100644 --- a/bin/Dockerfile.fastchess +++ b/bin/Dockerfile.fastchess @@ -3,8 +3,8 @@ FROM debian:bullseye RUN apt-get update \ && apt-get install -y curl git build-essential unzip -RUN git clone https://github.com/Disservin/fast-chess.git \ - && cd fast-chess \ +RUN git clone https://github.com/Disservin/fastchess.git \ + && cd fastchess \ && make RUN mkdir /books @@ -15,4 +15,4 @@ RUN curl -ssL https://github.com/AndyGrant/openbench-books/raw/master/8moves_v3. RUN unzip /books/4moves_noob.epd.zip -d /books RUN unzip /books/8moves_v3.epd.zip -d /books -ENTRYPOINT ["fast-chess/fast-chess"] +ENTRYPOINT ["fastchess/fastchess"] diff --git a/docker-compose.yml b/docker-compose.yml index 477dffb..2fb5247 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: -each proto=uci tc=8+0.08 option.SyzygyPath=/syzygy option.Hash=128 option.Threads=1 - -randomseed -openings file=/books/8moves_v3.epd format=epd order=random + -openings file=/books/8moves_v3.epd format=epd order=random -games 2 -repeat -rounds 500 -recover -ratinginterval 10 -concurrency 6 volumes: @@ -29,7 +29,7 @@ services: -each proto=uci tc=inf nodes=1 option.SyzygyPath=/syzygy option.Hash=128 option.Threads=1 - -randomseed -openings file=/books/8moves_v3.epd format=epd order=random + -openings file=/books/8moves_v3.epd format=epd order=random -games 2 -repeat -rounds 2500 -recover -ratinginterval 10 -concurrency 6 volumes: @@ -48,7 +48,7 @@ services: -each proto=uci tc=8+0.08 option.SyzygyPath=/syzygy option.Hash=128 option.Threads=1 -sprt elo0=0 elo1=5 alpha=0.05 beta=0.1 - -randomseed -openings file=/books/4moves_noob.epd format=epd order=random + -openings file=/books/4moves_noob.epd format=epd order=random -games 2 -repeat -rounds 7500 -recover -ratinginterval 10 -concurrency 6 volumes: @@ -67,7 +67,7 @@ services: -each proto=uci tc=inf nodes=5000 option.SyzygyPath=/syzygy option.Hash=128 option.Threads=1 -sprt elo0=0 elo1=5 alpha=0.05 beta=0.1 - -randomseed -openings file=/books/4moves_noob.epd format=epd order=random + -openings file=/books/4moves_noob.epd format=epd order=random -games 2 -repeat -rounds 2500 -recover -ratinginterval 10 -concurrency 6 volumes: @@ -86,7 +86,7 @@ services: -each proto=uci tc=40+0.4 option.SyzygyPath=/syzygy option.Hash=128 option.Threads=1 -sprt elo0=0 elo1=5 alpha=0.1 beta=0.2 - -randomseed -openings file=/books/4moves_noob.epd format=epd order=random + -openings file=/books/4moves_noob.epd format=epd order=random -games 2 -repeat -rounds 7500 -recover -ratinginterval 10 -concurrency 6 -resign movecount=3 score=500 twosided=true @@ -107,7 +107,7 @@ services: -each proto=uci tc=8+0.08 option.SyzygyPath=/syzygy option.Hash=128 option.Threads=1 -sprt elo0=-5 elo1=0 alpha=0.05 beta=0.1 - -randomseed -openings file=/books/4moves_noob.epd format=epd order=random + -openings file=/books/4moves_noob.epd format=epd order=random -games 2 -repeat -rounds 7500 -recover -ratinginterval 10 -concurrency 6 volumes: @@ -125,7 +125,7 @@ services: -each proto=uci tc=inf nodes=5000 option.Hash=128 option.Threads=1 - -randomseed -openings file=/books/8moves_v3.epd format=epd order=random + -openings file=/books/8moves_v3.epd format=epd order=random -rounds 100000 -recover -ratinginterval 100 -concurrency 6 -pgnout /pgn/self_play-0.15.0.pgn min fi @@ -143,7 +143,7 @@ services: -each proto=uci tc=8+0.08 option.SyzygyPath=/syzygy option.Hash=128 option.Threads=1 - -randomseed -openings file=/books/8moves_v3.epd format=epd order=random + -openings file=/books/8moves_v3.epd format=epd order=random -debug -rounds 1 -pgnout /pgn/debug.pgn volumes: @@ -163,7 +163,7 @@ services: -each proto=uci tc=inf nodes=5000 option.SyzygyPath=/syzygy option.Hash=128 option.Threads=1 -tournament gauntlet - -randomseed -openings file=/books/4moves_noob.epd format=epd order=random + -openings file=/books/4moves_noob.epd format=epd order=random -games 2 -repeat -rounds 250 -recover -ratinginterval 100 -concurrency 6 volumes: diff --git a/src/options.rs b/src/options.rs index f0dc74c..6d67b59 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,9 +1,9 @@ use once_cell::sync::Lazy; use std::cmp::max; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use std::sync::RwLock; -static NUM_THREADS: AtomicUsize = AtomicUsize::new(1); +static NUM_THREADS: AtomicU32 = AtomicU32::new(1); static HASH_SIZE_MB: AtomicUsize = AtomicUsize::new(16); static MULTI_PV: AtomicUsize = AtomicUsize::new(1); @@ -16,11 +16,11 @@ static POLICY_TEMPERATURE_ROOT: Lazy> = Lazy::new(|| RwLock::new(14. static CHESS960: AtomicBool = AtomicBool::new(false); static POLICY_ONLY: AtomicBool = AtomicBool::new(false); -pub fn set_num_threads(threads: usize) { +pub fn set_num_threads(threads: u32) { NUM_THREADS.store(threads, Ordering::Relaxed); } -pub fn get_num_threads() -> usize { +pub fn get_num_threads() -> u32 { max(1, NUM_THREADS.load(Ordering::Relaxed)) } diff --git a/src/search.rs b/src/search.rs index 12b6fcc..aead89e 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,9 +1,10 @@ +use scoped_threadpool::Pool; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; use crate::chess::{Color, Move}; use crate::evaluation; -use crate::options::{get_num_threads, get_policy_temperature, is_policy_only}; +use crate::options::{get_policy_temperature, is_policy_only}; use crate::search_tree::{MoveEdge, PositionNode, SearchTree}; use crate::state::State; use crate::tablebase; @@ -128,7 +129,7 @@ impl Search { .map(Duration::from_millis) } - pub fn go(&self, mut tokens: Tokens, next_line: &mut Option) { + pub fn go(&self, threads: &mut Pool, mut tokens: Tokens, next_line: &mut Option) { let state = self.search_tree.root_state(); let stm = state.side_to_move(); @@ -227,32 +228,33 @@ impl Search { think_time.set_node_limit(node_limit); - self.playout_parallel(get_num_threads(), think_time, next_line); + self.playout_parallel(threads, think_time, next_line); } fn playout_parallel( &self, - num_threads: usize, + threads: &mut Pool, time_management: TimeManagement, next_line: &mut Option, ) { let stop_signal = AtomicBool::new(false); + let thread_count = threads.thread_count(); let run_search_thread = |tm: &TimeManagement| { let mut tld = ThreadData::create(&self.search_tree); while self.search_tree.playout(&mut tld, tm, &stop_signal) {} }; - std::thread::scope(|s| { - s.spawn(|| { + threads.scoped(|s| { + s.execute(|| { run_search_thread(&time_management); self.search_tree.print_info(&time_management); stop_signal.store(true, Ordering::Relaxed); println!("bestmove {}", self.best_move().to_uci()); }); - for _ in 0..(num_threads - 1) { - s.spawn(|| { + for _ in 0..(thread_count - 1) { + s.execute(|| { run_search_thread(&TimeManagement::infinite()); }); } @@ -307,13 +309,14 @@ impl Search { let reward = mov.reward(); println!( - "info string {:7} M: {:5} P: {:>6} V: {:7} E: {:>6} ({:>8})", - format!("{}", mov.get_move().to_uci()), - format!("{:3.2}", e * 100.), - format!("{:3.2}", f32::from(mov.policy()) / SCALE * 100.), + "info string {:7} M: {:>5.2} P: {:>5.2} V: {:7} ({:>5.2}%) E: {:>7.2} ({:>8})", + mov.get_move().to_uci(), + e * 100., + f32::from(mov.policy()) / SCALE * 100., mov.visits(), - format!("{:3.2}", reward.average as f32 / (SCALE / 100.)), - format!("{:3.2}", eval_in_cp(reward.average as f32 / SCALE)) + mov.visits() as f32 / root_node.visits() as f32 * 100., + reward.average as f32 / (SCALE / 100.), + eval_in_cp(reward.average as f32 / SCALE) ); } } diff --git a/src/search_tree.rs b/src/search_tree.rs index a9c56df..e3c0519 100644 --- a/src/search_tree.rs +++ b/src/search_tree.rs @@ -102,6 +102,10 @@ impl PositionNode { unsafe { &*(self.hots) } } + pub fn visits(&self) -> u64 { + self.hots().iter().map(|x| u64::from(x.visits())).sum() + } + pub fn clear_children_links(&self) { let hots = unsafe { &*(self.hots.cast_mut()) }; diff --git a/src/uci.rs b/src/uci.rs index f35a613..1644226 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -1,9 +1,11 @@ +use scoped_threadpool::Pool; use std::io::stdin; use std::str::{FromStr, SplitWhitespace}; use crate::options::{ - set_chess960, set_cpuct, set_cpuct_tau, set_cvisits_selection, set_hash_size_mb, set_multi_pv, - set_num_threads, set_policy_only, set_policy_temperature, set_policy_temperature_root, + get_num_threads, set_chess960, set_cpuct, set_cpuct_tau, set_cvisits_selection, + set_hash_size_mb, set_multi_pv, set_num_threads, set_policy_only, set_policy_temperature, + set_policy_temperature_root, }; use crate::search::Search; use crate::search_tree::print_size_list; @@ -19,6 +21,7 @@ const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); pub fn main() { let mut search = Search::new(State::default(), LRTable::empty()); + let mut search_threads = Pool::new(1); let mut next_line: Option = None; @@ -53,7 +56,12 @@ pub fn main() { } } "quit" => return, - "go" => search.go(tokens, &mut next_line), + "go" => { + if search_threads.thread_count() != get_num_threads() { + search_threads = Pool::new(get_num_threads()); + } + search.go(&mut search_threads, tokens, &mut next_line); + } "movelist" => search.print_move_list(), "sizelist" => print_size_list(), "eval" => search.print_eval(),