From 642c7c390b364fa0dbb6727ad56b2378dbeeecd0 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Tue, 12 Nov 2024 22:04:10 +0700 Subject: [PATCH 1/3] i think i unsafed too hard --- Cargo.toml | 5 +- .../games/sandy_release_wins_with_no_tt.pgn | 31 +++ src/benches/ngm.rs | 1 + src/engine/debug.rs | 106 +-------- src/engine/lib.rs | 16 +- src/engine/search/main_search.rs | 12 +- src/engine/search/mod.rs | 20 +- src/engine/search/negamax.rs | 103 ++++++++- src/engine/transposition_table/bounds.rs | 21 ++ src/engine/transposition_table/mod.rs | 209 +++++++++++++++++- src/engine/transposition_table/tests/mod.rs | 86 +++++++ src/engine/uci.rs | 12 +- src/sandy/main.rs | 29 ++- src/sandy/uci/mod.rs | 10 + tests/d6.rs | 1 + tests/mate.rs | 12 +- 16 files changed, 540 insertions(+), 134 deletions(-) create mode 100644 research/games/sandy_release_wins_with_no_tt.pgn create mode 100644 src/engine/transposition_table/bounds.rs create mode 100644 src/engine/transposition_table/tests/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 9bed3b56..7f17fe2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ smallvec = "1.13.2" crossbeam-deque = "0.8.5" lockfree = "0.5.1" once_cell = "1.20.2" +colored = "2.1.0" #jja = { version = "0.9.1", features = [] } [dev-dependencies] @@ -46,7 +47,7 @@ path = "src/benches/d6.rs" harness = false [[bench]] -name = "qs" +name = "qs" # quick search benches, fewer samples path = "src/benches/quick_search.rs" harness = false @@ -70,4 +71,4 @@ path = "tests/d6.rs" [[test]] name = "mate" -path = "tests/mate.rs" \ No newline at end of file +path = "tests/mate.rs" diff --git a/research/games/sandy_release_wins_with_no_tt.pgn b/research/games/sandy_release_wins_with_no_tt.pgn new file mode 100644 index 00000000..d82dea19 --- /dev/null +++ b/research/games/sandy_release_wins_with_no_tt.pgn @@ -0,0 +1,31 @@ +[Event "?"] +[Site "?"] +[Date "2024.11.12"] +[Round "?"] +[White "sandy"] +[Black "sandy_release"] +[Result "0-1"] +[ECO "A40"] +[GameDuration "00:02:56"] +[GameEndTime "2024-11-12T16:03:44.673 GMT+7"] +[GameStartTime "2024-11-12T16:00:48.226 GMT+7"] +[Opening "Queen's pawn"] +[PlyCount "44"] +[TimeControl "40/120+1"] + +1. d4 {+0.23/4 4.0s} e6 {+0.50/5 4.0s} 2. Bf4 {-0.47/4 4.0s} Nc6 {+0.86/5 4.0s} +3. Be3 {-0.42/4 4.0s} Nf6 {+1.18/5 4.0s} 4. Nc3 {-1.31/4 4.0s} +Ng4 {+0.04/6 4.0s} 5. Bf4 {+0.11/5 4.0s} Nxf2 {+1.49/5 4.0s} +6. Kxf2 {+1.21/5 4.0s} Qh4+ {+0.21/5 4.0s} 7. g3 {+1.66/5 4.0s} +Qf6 {-0.63/6 4.0s} 8. Nb5 {+2.82/5 4.0s} e5 {-1.11/6 4.0s} +9. Nxc7+ {+1.03/6 4.0s} Kd8 {-0.03/7 4.0s} 10. Nxa8 {+2.37/5 4.0s} +exf4 {-0.16/6 4.0s} 11. Nf3 {+2.58/5 4.0s} Nxd4 {-0.05/6 4.0s} +12. Nxd4 {+0.66/6 4.0s} fxg3+ {+0.70/7 4.0s} 13. Kxg3 {+3.42/5 4.0s} +Qe5+ {+0.31/6 4.0s} 14. Kf3 {+0.55/5 4.0s} Qh5+ {+0.12/6 4.0s} +15. Kf2 {+3.76/5 4.0s} Qh4+ {+0.43/7 4.0s} 16. Ke3 {+0.63/6 4.0s} +Bc5 {+0.90/7 4.0s} 17. Kf3 {+2.76/5 4.0s} Bxd4 {+1.18/7 4.0s} +18. Nc7 {+0.12/5 4.0s} Qf2+ {+1.18/7 4.0s} 19. Ke4 {-0.30/5 4.0s} +Kxc7 {+1.26/7 4.0s} 20. Rb1 {-0.69/5 4.0s} d5+ {0.00/8 4.0s} +21. Kxd5 {-320.00/5 4.0s} Be6+ {0.00/8 4.0s} 22. Ke4 {-320.00/5 4.0s} +Qe3# {0.00/7 4.0s, Black mates} 0-1 + diff --git a/src/benches/ngm.rs b/src/benches/ngm.rs index 5b6a0d0f..0d98d0a0 100644 --- a/src/benches/ngm.rs +++ b/src/benches/ngm.rs @@ -2,6 +2,7 @@ use criterion::black_box; use criterion::criterion_group; use criterion::criterion_main; use criterion::Criterion; +<<<<<<< HEAD use sandy_engine::opts::Opts; use sandy_engine::search::negamax::ng_test; use sandy_engine::setup::depth::Depth; diff --git a/src/engine/debug.rs b/src/engine/debug.rs index 1a79be1a..72dc01fa 100644 --- a/src/engine/debug.rs +++ b/src/engine/debug.rs @@ -47,112 +47,10 @@ macro_rules! optlog { } } } - } - }; + } } - -// /// Querying the options, shorthand for discreet use in code. don't worry -// about /// this too much, kinda garbage code xd -// impl Opts { -// /// Search [`DebugLevel`] is TRACE -// pub fn st(&self) -> bool { -// self.search == DebugLevel::trace -// } -// -// /// Search [`DebugLevel`] is DEBUG or higher (TRACE) -// pub fn sd(&self) -> bool { -// self.search == DebugLevel::debug || self.st() -// } -// -// /// Search [`DebugLevel`] is INFO or higher (DEBUG, TRACE) -// pub fn si(&self) -> bool { -// self.search == DebugLevel::info || self.sd() -// } -// -// /// Search [`DebugLevel`] is WARN or higher (INFO, DEBUG, TRACE) -// pub fn sw(&self) -> bool { -// self.search == DebugLevel::warn || self.si() -// } -// -// /// Search [`DebugLevel`] is ERROR or higher (WARN, INFO, DEBUG, TRACE) -// pub fn se(&self) -> bool { -// self.search == DebugLevel::error || self.sw() -// } -// -// /// Eval [`DebugLevel`] is TRACE -// pub fn et(&self) -> bool { -// self.eval == DebugLevel::trace -// } -// -// /// Eval [`DebugLevel`] is DEBUG or higher (TRACE) -// pub fn ed(&self) -> bool { -// self.eval == DebugLevel::debug || self.et() -// } -// -// /// Eval [`DebugLevel`] is INFO or higher (DEBUG, TRACE) -// pub fn ei(&self) -> bool { -// self.eval == DebugLevel::info || self.ed() -// } -// -// /// Eval [`DebugLevel`] is WARN or higher (INFO, DEBUG, TRACE) -// pub fn ew(&self) -> bool { -// self.eval == DebugLevel::warn || self.ei() -// } -// -// /// Eval [`DebugLevel`] is ERROR or higher (WARN, INFO, DEBUG, TRACE) -// pub fn ee(&self) -> bool { -// self.eval == DebugLevel::error || self.ew() -// } -// -// /// Comm [`DebugLevel`] is TRACE -// pub fn ct(&self) -> bool { -// self.comm == DebugLevel::trace -// } -// -// /// Comm [`DebugLevel`] is DEBUG or higher (TRACE) -// pub fn cd(&self) -> bool { -// self.comm == DebugLevel::debug || self.ct() -// } -// -// /// Comm [`DebugLevel`] is INFO or higher (DEBUG, TRACE) -// pub fn ci(&self) -> bool { -// self.comm == DebugLevel::info || self.cd() -// } -// -// /// Comm [`DebugLevel`] is WARN or higher (INFO, DEBUG, TRACE) -// pub fn cw(&self) -> bool { -// self.comm == DebugLevel::warn || self.ci() -// } -// -// /// Comm [`DebugLevel`] is ERROR or higher (WARN, INFO, DEBUG, TRACE) -// pub fn ce(&self) -> bool { -// self.comm == DebugLevel::error || self.cw() -// } -// -// /// if debug level, print the message -// pub fn edp(&self, msg: &str) { -// if self.ed() { -// println!("{}", msg); -// } -// } -// -// /// if trace level, print the message -// pub fn stp(&self, msg: &str) { -// if self.st() { -// println!("{}", msg); -// } -// } -// -// pub fn ut(&self) -> bool { -// self.uci == DebugLevel::trace -// } -// -// pub fn ud(&self) -> bool { -// self.uci == DebugLevel::debug || self.ut() -// } -// } - + impl DebugLevel { /// This [`DebugLevel`] is TRACE #[inline(always)] diff --git a/src/engine/lib.rs b/src/engine/lib.rs index cc84e7e8..f0697422 100644 --- a/src/engine/lib.rs +++ b/src/engine/lib.rs @@ -14,6 +14,7 @@ pub mod uci; pub mod util; use std::sync::atomic::Ordering; +use std::sync::RwLock; use std::thread; use std::time::Duration; use std::time::Instant; @@ -32,18 +33,24 @@ use crate::search::SEARCHING; use crate::search::SEARCH_TO; use crate::search::SEARCH_UNTIL; use crate::setup::depth::Depth; +use crate::transposition_table::{TranspositionTable, TT_INITIALISED}; +use crate::transposition_table::TT; #[derive(Debug)] pub struct Engine { pub board: Board, + pub created_at: Instant, } impl Engine { pub fn new() -> Result { info!("creating engine at version {}", env!("CARGO_PKG_VERSION")); - Ok(Self { + let mut s = Self { board: Board::default(), - }) + created_at: Instant::now(), + }; + s.setup()?; + Ok(s) } pub fn set_search(&self, x: bool) { @@ -77,6 +84,11 @@ impl Engine { /// 4. ... pub fn setup(&mut self) -> Result<()> { // ... + unsafe { + TT.write(RwLock::new(TranspositionTable::new()?)); + } + TT_INITIALISED.store(true, Ordering::Relaxed); + Ok(()) } diff --git a/src/engine/search/main_search.rs b/src/engine/search/main_search.rs index e7805a80..80851a50 100644 --- a/src/engine/search/main_search.rs +++ b/src/engine/search/main_search.rs @@ -47,6 +47,7 @@ impl Engine { let mut target_depth = Depth(0); let mut total_nodes = 0; + let mut table_hits = 0; let start_time = Instant::now(); while !exit_condition() && target_depth < search_to() { @@ -89,6 +90,9 @@ impl Engine { // add up all the recursively searched nodes, and the one the search begun from total_nodes += search_result.nodes_searched + 1; + // add up all the transposition table hits + table_hits += search_result.tt_hits; + // we found a better match, update: // * best available value for a next position // * best move to get to that position @@ -131,6 +135,7 @@ impl Engine { target_depth, best_value, total_nodes, + table_hits, start_time.elapsed(), &root.pv, ); @@ -139,7 +144,7 @@ impl Engine { send(&mut publisher, Message::BestMove(MV(mv, best_value))); } if let Some(ponder) = root.pv.get(1) { - send(&mut publisher, Message::Ponder(ponder.clone())); + send(&mut publisher, Message::Ponder(*ponder)); } } @@ -149,10 +154,13 @@ impl Engine { if let Some(mv) = best_move { send(&mut publisher, Message::BestMove(MV(mv, best_value))) } - + + // looks sketchy, but it's to prevent dropping the sender before the receiver + // has gotten the best move. thread::sleep(Duration::from_millis( (SEARCH_THREADS * 2 * UCI_LISTENING_FREQUENCY) as u64, )); + }); Ok(receiver) diff --git a/src/engine/search/mod.rs b/src/engine/search/mod.rs index d7a79242..e0d64953 100644 --- a/src/engine/search/mod.rs +++ b/src/engine/search/mod.rs @@ -27,7 +27,7 @@ pub static SEARCH_TO: AtomicU16 = AtomicU16::new(0); pub static SEARCHING: AtomicBool = AtomicBool::new(false); pub static EXIT: AtomicBool = AtomicBool::new(false); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct MV(pub ChessMove, pub Value); pub struct RootNode { @@ -37,10 +37,12 @@ pub struct RootNode { pub previous_eval: Value, } +#[derive(Debug, Clone)] pub struct SearchResult { pub pv: Vec, pub next_position_value: Value, - pub nodes_searched: usize, + pub nodes_searched: u32, + pub tt_hits: u32, } #[derive(Debug)] @@ -55,9 +57,10 @@ pub enum Message { pub struct SearchInfo { pub depth: Depth, pub score: Value, - pub nodes: usize, + pub nodes: u32, pub time: Duration, pub pv: Vec, + pub tt_hits: u32, } pub fn exit_condition() -> bool { @@ -79,7 +82,8 @@ fn info( publisher: &mut Sender, target_depth: Depth, best_value: Value, - total_nodes: usize, + total_nodes: u32, + tt_hits: u32, el: Duration, pv: &[MV], ) { @@ -89,6 +93,7 @@ fn info( nodes: total_nodes, time: el, pv: pv.to_vec(), + tt_hits, })) { debug!("error sending info message: {:?}", e); } @@ -104,10 +109,13 @@ impl SearchResult { pub fn new_eval(&mut self, ev: Value) { self.next_position_value = ev; } - pub fn add_nodes(&mut self, nodes: usize) { + pub fn add_nodes(&mut self, nodes: u32) { self.nodes_searched += nodes; } - pub fn set_nodes(&mut self, nodes: usize) { + pub fn add_tt_hits(&mut self, nodes: u32) { + self.tt_hits += nodes; + } + pub fn set_nodes(&mut self, nodes: u32) { self.nodes_searched = nodes; } } diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index 853d35b4..ae5748a5 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -1,7 +1,9 @@ use std::sync::atomic::Ordering; +use std::sync::RwLock; use anyhow::Result; use chess::Board; +use log::error; use crate::evaluation::evaluate; use crate::optlog; @@ -15,12 +17,19 @@ use crate::search::SEARCHING; use crate::search::SEARCH_TO; use crate::setup::depth::Depth; use crate::setup::values::Value; +use crate::transposition_table::bounds::EvalBound; +use crate::transposition_table::{table_mut_ref, TT_INITIALISED}; +use crate::transposition_table::table_ref; +use crate::transposition_table::TableEntry; +use crate::transposition_table::TranspositionTable; +use crate::transposition_table::TT; #[inline(always)] pub fn searching() -> bool { SEARCHING.load(Ordering::Relaxed) } + #[inline(always)] pub fn search_to() -> Depth { Depth(SEARCH_TO.load(Ordering::Relaxed)) @@ -40,19 +49,73 @@ pub fn ng_test( Ok(negamax(pos, to_depth, alpha, beta)) } -pub fn negamax(pos: Board, to_depth: Depth, mut alpha: Value, beta: Value) -> SearchResult { +pub fn negamax(pos: Board, to_depth: Depth, mut alpha: Value, mut beta: Value) -> SearchResult { + let original_alpha = alpha; + let original_beta = beta; + let mut tt_hits = 0; + + if opts().unwrap().use_tt { + { + match table_ref().try_read() { + Ok(lock) => { + if let Some(hit) = lock.lookup(pos.get_hash()) { + if hit.depth() >= to_depth { + tt_hits += 1; + match hit.bound() { + EvalBound::Exact => { + return { + SearchResult { + pv: if hit.is_pv() { + vec![hit.mv_struct()] + } else { + vec![] + }, + next_position_value: hit.eval(), + nodes_searched: 1, + tt_hits, + } + } + } + EvalBound::LowerBound => alpha = alpha.max(hit.eval()), + EvalBound::UpperBound => beta = beta.min(hit.eval()), + } + if alpha >= beta { + return SearchResult { + pv: if hit.is_pv() { + vec![hit.mv_struct()] + } else { + vec![] + }, + next_position_value: hit.eval(), + nodes_searched: 1, + tt_hits, + }; + } + } + } + } + Err(e) => { + error!("tt lock poisoned: {e}"); + } + } + } + } + let moves = ordered_moves(&pos); optlog!(search;trace;"ng: {pos}, td: {to_depth:?}, a: {alpha:?}, b: {beta:?}"); optlog!(search;trace;"moves: {}", moves); + if to_depth == Depth::ZERO || moves.is_empty() { let ev = evaluate(&pos, &moves); optlog!(search;trace;"return eval: {:?}", ev); + return SearchResult { pv: vec![], next_position_value: ev, nodes_searched: 1, + tt_hits, }; } @@ -63,6 +126,7 @@ pub fn negamax(pos: Board, to_depth: Depth, mut alpha: Value, beta: Value) -> Se for mv in moves.0.iter() { let mut deeper = -negamax(pos.make_move_new(*mv), to_depth - 1, -beta, -alpha); total_nodes += deeper.nodes_searched + 1; + tt_hits += deeper.tt_hits; if !searching() { optlog!(search;trace;"searching() == false, breaking early"); @@ -90,11 +154,46 @@ pub fn negamax(pos: Board, to_depth: Depth, mut alpha: Value, beta: Value) -> Se optlog!(search;trace;"return max_val: {:?}", best); - SearchResult { + let this_search_result = SearchResult { pv, next_position_value: best.as_ref().map_or(Value::MIN, |b| b.1), nodes_searched: total_nodes, + tt_hits, + }; + + if opts().unwrap().use_tt { + if let Some(b) = best { + let bound = if this_search_result.next_position_value <= original_alpha { + EvalBound::UpperBound + } else if this_search_result.next_position_value >= original_beta { + EvalBound::LowerBound + } else { + EvalBound::Exact + }; + + { + match table_mut_ref().try_write() { + Ok(mut lock) => lock.insert( + pos.get_hash(), + TableEntry::pack( + pos.get_hash(), + this_search_result.next_position_value, + to_depth, + b.0, + bound, + false, + ), + ), + Err(e) => { + error!("tt write lock poisoned: {e}"); + } + } + } + } } + + opts().stp(&format!("return max_val: {:?}", best)); + this_search_result } #[cfg(test)] diff --git a/src/engine/transposition_table/bounds.rs b/src/engine/transposition_table/bounds.rs new file mode 100644 index 00000000..2675f5cc --- /dev/null +++ b/src/engine/transposition_table/bounds.rs @@ -0,0 +1,21 @@ +#![allow(dead_code)] +use crate::setup::values::Value; + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum EvalBound { + Exact, + LowerBound, + UpperBound, +} + +impl EvalBound { + pub(crate) fn from_bounds(p0: Value, p1: Value, p2: Value) -> Self { + if p0 == p1 { + Self::Exact + } else if p0 == p2 { + Self::LowerBound + } else { + Self::UpperBound + } + } +} diff --git a/src/engine/transposition_table/mod.rs b/src/engine/transposition_table/mod.rs index 1c344bb9..fff01b95 100644 --- a/src/engine/transposition_table/mod.rs +++ b/src/engine/transposition_table/mod.rs @@ -1,19 +1,204 @@ +pub mod bounds; + +use std::mem::MaybeUninit; +use std::sync::atomic::{AtomicBool, AtomicU64}; +use std::sync::atomic::Ordering; +use std::sync::RwLock; + +use chess::ChessMove; +use chess::Piece; +use chess::Square; +use crate::opts; + +use crate::search::MV; use crate::setup::depth::Depth; +use crate::setup::values::Value; +use crate::transposition_table::bounds::EvalBound; +use anyhow::{anyhow, Result}; -pub type BoardHash = u64; +/// # A single transposition table entry. +/// [`TranspositionTable`] +/// ## Memory Layout +/// * 2 bytes for the key +/// +/// **[`AtomicU64`]:** +/// * evaluation value: 2 bytes +/// * depth: 2 bytes +/// * source square: 1 byte +/// * destination square: 1 byte +/// * promotion: 3 bits +/// * bound: 2 bits +/// * is_pv: 1 bit +/// * is_valid_entry: 1 bit +/// * 1 parity bit for checking +#[derive(Debug)] +pub struct TableEntry { + /// only store 2 bytes of the key to verify if a collision occurred + pub key: u16, + pub value: AtomicU64, +} -pub enum EvalBound { - Exact, - LowerBound, - UpperBound, +pub struct TranspositionTable { + size: usize, + used: usize, + table: Vec, } -pub struct TableEntry { - // pub best_move: Option, - pub evaluation: i32, - pub computed_from_depth: Depth, - pub computed_for_depth: Depth, - pub bound: EvalBound, +pub static TT_INITIALISED: AtomicBool = AtomicBool::new(false); +pub static mut TT: MaybeUninit> = MaybeUninit::uninit(); + +pub fn table_mut_ref() -> &'static mut RwLock { + if !TT_INITIALISED.load(Ordering::Relaxed) { + panic!("transposition table not initialised, asked for mut ref"); + } + unsafe { TT.assume_init_mut() } +} + +pub fn table_ref() -> &'static RwLock { + if !TT_INITIALISED.load(Ordering::Relaxed) { + panic!("transposition table not initialised, asked for mut ref"); + } + unsafe { TT.assume_init_ref() } +} + +impl TranspositionTable { + pub fn new() -> Result { + let size = (opts().hash_size * 1024 / size_of::()).checked_next_power_of_two().ok_or(anyhow!("invalid hash map size (overflowed)"))?; + let mut table = Vec::with_capacity(size); + for _ in 0..size { + table.push(TableEntry { + key: 0, + value: AtomicU64::new(0), + }); + } + Ok(Self { + table, + size, + used: 0, + }) + } + + pub fn index(&self, key: u64) -> usize { + (key as usize) % self.size + } + + pub fn lookup(&self, key: u64) -> Option<&TableEntry> { + let index = self.index(key); + let slot = &self.table[index]; + + if slot.key == (key & 0xFFFF) as u16 { + Some(slot) + } else { + None + } + } + + pub(crate) fn insert(&mut self, key: u64, new_entry: TableEntry) { + let index = self.index(key); + let slot = &mut self.table[index]; + + let existing_depth = slot.depth(); + + let new_depth = new_entry.depth(); + + if new_depth >= existing_depth { + if !slot.is_valid_entry() { + self.used += 1; + } + slot.key = (key & 0xFFFF) as u16; + slot.value + .store(new_entry.value.load(Ordering::Relaxed), Ordering::Relaxed); + } + } + + pub fn hashfull(&self) -> usize { + self.used * 1000 / self.size + } +} + +impl TableEntry { + const PROMOTION_BITS: [Option; 8] = [ + None, + Some(Piece::Knight), + Some(Piece::Bishop), + Some(Piece::Rook), + Some(Piece::Queen), + None, + None, + None, + ]; + + pub fn pack( + key: u64, + eval: Value, + depth: Depth, + mv: ChessMove, + bound: EvalBound, + is_pv: bool, + ) -> Self { + let mut value = 0u64; + value |= (eval.0 as u64) << 48; + value |= (depth.0 as u64) << 32; + value |= (mv.get_source().to_int() as u64) << 24; + value |= (mv.get_dest().to_int() as u64) << 16; + value |= (Self::PROMOTION_BITS + .iter() + .position(|x| x.eq(&mv.get_promotion())) + .unwrap_or(0) as u64) + << 13; + value |= (bound as u64) << 11; + value |= (is_pv as u64) << 10; + // since we just created this entry, it is valid (unlike the empty ones + // initially in the table) + value |= 0b10; + // parity bit, set to 1 if the number of bits set in the value is even + // ensures the full value is always odd parity + value |= 1 ^ (value.count_ones() & 1) as u64; + Self { + key: key as u16, + value: AtomicU64::new(value), + } + } + + pub fn depth(&self) -> Depth { + Depth((self.value.load(Ordering::Relaxed) >> 32) as u16) + } + + pub fn eval(&self) -> Value { + Value((self.value.load(Ordering::Relaxed) >> 48) as i16) + } + + pub fn mv(&self) -> ChessMove { + let value = self.value.load(Ordering::Relaxed); + let src = (value >> 24) as u8; + let dest = (value >> 16) as u8; + let promotion = Self::PROMOTION_BITS[(0b111 & (value >> 13)) as usize]; + unsafe { ChessMove::new(Square::new(src), Square::new(dest), promotion) } + } + + pub fn mv_struct(&self) -> MV { + MV(self.mv(), self.eval()) + } + + pub fn bound(&self) -> EvalBound { + match 0b11 & (self.value.load(Ordering::Relaxed) >> 11) as u8 { + 0 => EvalBound::Exact, + 1 => EvalBound::LowerBound, + 2 => EvalBound::UpperBound, + _ => unreachable!("bound value out of range"), + } + } + + pub fn is_pv(&self) -> bool { + (self.value.load(Ordering::Relaxed) >> 10) & 1 == 1 + } + + pub fn is_valid_entry(&self) -> bool { + let val = self.value.load(Ordering::Relaxed); + (val & 0b10) == 0b10 && val.count_ones() & 1 == 1 + } } -pub type TranspositionTable = lockfree::map::Map; +#[cfg(test)] +#[path = "tests/mod.rs"] +mod tests; diff --git a/src/engine/transposition_table/tests/mod.rs b/src/engine/transposition_table/tests/mod.rs new file mode 100644 index 00000000..bab93084 --- /dev/null +++ b/src/engine/transposition_table/tests/mod.rs @@ -0,0 +1,86 @@ +use std::str::FromStr; + +use chess::ChessMove; +use chess::Piece; +use chess::Square; + +use crate::setup::depth::Depth; +use crate::setup::values::Value; +use crate::transposition_table::bounds::EvalBound; +use crate::transposition_table::TableEntry; + +#[test] +fn test_pack_then_unpack() { + let key = 0x1234567890abcdef; + let eval = Value(0x1234); + let depth = Depth(0x1234); + let mv = ChessMove::new( + Square::from_str("a7").unwrap(), + Square::from_str("a8").unwrap(), + Some(Piece::Queen), + ); + let bound = EvalBound::Exact; + let entry = TableEntry::pack(key, eval, depth, mv, bound, true); + assert_eq!(eval, entry.eval()); + assert_eq!(depth, entry.depth()); + assert_eq!(mv, entry.mv()); + assert_eq!(bound, entry.bound()); + assert_eq!(true, entry.is_pv()); +} + +#[test] +fn test_pack_then_unpack_with_promotion() { + let key = 0x1234567890abcdef; + let eval = Value(0x1234); + let depth = Depth(0x1234); + let mv = ChessMove::new( + Square::from_str("a7").unwrap(), + Square::from_str("a8").unwrap(), + Some(Piece::Queen), + ); + let bound = EvalBound::Exact; + let entry = TableEntry::pack(key, eval, depth, mv, bound, true); + assert_eq!(eval, entry.eval()); + assert_eq!(depth, entry.depth()); + assert_eq!(mv, entry.mv()); + assert_eq!(bound, entry.bound()); + assert_eq!(true, entry.is_pv()); +} + +#[test] +fn test_pack_then_unpack_with_no_promotion() { + let key = 0x1234567890abcdef; + let eval = Value(0x1234); + let depth = Depth(0x1234); + let mv = ChessMove::new( + Square::from_str("a7").unwrap(), + Square::from_str("a8").unwrap(), + None, + ); + let bound = EvalBound::Exact; + let entry = TableEntry::pack(key, eval, depth, mv, bound, false); + assert_eq!(eval, entry.eval()); + assert_eq!(depth, entry.depth()); + assert_eq!(mv, entry.mv()); + assert_eq!(bound, entry.bound()); + assert_eq!(false, entry.is_pv()); +} + +#[test] +fn test_pack_then_unpack_edge_case_values() { + let key = 0x1234567890abcdef; + let eval = Value(0x7fff); + let depth = Depth(0xffff); + let mv = ChessMove::new( + Square::from_str("h8").unwrap(), + Square::from_str("h1").unwrap(), + Some(Piece::Queen), + ); + let bound = EvalBound::UpperBound; + let entry = TableEntry::pack(key, eval, depth, mv, bound, true); + assert_eq!(eval, entry.eval()); + assert_eq!(depth, entry.depth()); + assert_eq!(mv, entry.mv()); + assert_eq!(bound, entry.bound()); + assert_eq!(true, entry.is_pv()); +} diff --git a/src/engine/uci.rs b/src/engine/uci.rs index f23f14ba..fa17a4e2 100644 --- a/src/engine/uci.rs +++ b/src/engine/uci.rs @@ -7,9 +7,13 @@ use anyhow::Result; use lockfree::channel::RecvErr; use crate::optlog; +use log::{debug, error}; +use log::trace; + use crate::search::exit_condition; use crate::search::Message; use crate::search::SearchInfo; +use crate::transposition_table::table_ref; use crate::Engine; /// How often to check for new uci messages from the search threads, in *ms* @@ -55,14 +59,20 @@ impl Engine { score, time, pv, + tt_hits, }) => { println!( - "info depth {} score {} nodes {} nps {} time {} pv {}", + "info depth {} score {} nodes {} nps {} time {} hashfull {} tt_hits {} pv {}", depth.0, score, nodes, (nodes as f64 / time.as_secs_f64()) as usize, time.as_millis(), + table_ref().try_read().map_or_else(|e| { + error!("tt lock poisoned when reading hashfull: {e}"); + -1 + }, |l| l.hashfull() as i64), + tt_hits, pv.iter().fold(String::new(), |mut acc, m| { write!(acc, "{} ", m.0).expect("strings shouldn't fail"); acc diff --git a/src/sandy/main.rs b/src/sandy/main.rs index 4cd1e3f4..62efe6bc 100644 --- a/src/sandy/main.rs +++ b/src/sandy/main.rs @@ -9,15 +9,18 @@ use std::io::stdin; use std::str::FromStr; +use std::time::Duration; use anyhow::anyhow; use anyhow::Result; use chess::Board; use log::info; use log::warn; +use sandy_engine::setup::depth::Depth; use sandy_engine::util::fen_to_str; use sandy_engine::util::Print; use sandy_engine::Engine; +use sandy_engine::Opts; use crate::player::terminal_loop; use crate::uci::uci_loop; @@ -28,11 +31,29 @@ mod uci; fn main() -> Result<()> { println!("Sandy Chess Engine v0.0.0"); + #[cfg(debug_assertions)] colog::basic_builder() .filter(None, log::LevelFilter::Trace) .init(); + #[cfg(not(debug_assertions))] + colog::basic_builder() + .filter(None, log::LevelFilter::Info) + .init(); + + debug_assert!( + size_of::() <= 16, + "does Opts really need to be {} bytes?", + size_of::() + ); - let engine = Engine::new()?; + let mut engine = Engine::new()?; + + // println!( + // "depth 4 best move: {}", + // engine.best_move(Depth(4), Duration::from_secs(600))? + // ); + // + // return Ok(()); let mut read_line = String::new(); loop { @@ -76,6 +97,12 @@ fn main() -> Result<()> { ("other", _) => { // used for testing/prototyping snippets } + ("debug", _) => { + println!( + "depth 6 best move: {}", + engine.best_move(Depth(6), Duration::from_secs(60))? + ); + } _ => { warn!("unrecognised command {command:?}"); } diff --git a/src/sandy/uci/mod.rs b/src/sandy/uci/mod.rs index 4c850d6e..21580fe0 100644 --- a/src/sandy/uci/mod.rs +++ b/src/sandy/uci/mod.rs @@ -9,15 +9,21 @@ use anyhow::Result; use chess::Board; use log::info; use log::warn; +<<<<<<< HEAD use sandy_engine::debug::DebugLevel::debug; use sandy_engine::debug::DebugLevel::info; use sandy_engine::optlog; use sandy_engine::opts::opts; use sandy_engine::opts::setopts; use sandy_engine::opts::Opts; +======= +use sandy_engine::opts; +>>>>>>> e981bb0 (i think i unsafed too hard) use sandy_engine::setup::depth::Depth; use sandy_engine::util::Print; use sandy_engine::Engine; +use sandy_engine::Opts; +use sandy_engine::OPTS; use vampirc_uci::parse_one; use vampirc_uci::Serializable; use vampirc_uci::UciMessage; @@ -119,6 +125,10 @@ pub fn uci_loop(mut engine: Engine) -> Result<()> { UciMessage::Quit => { // clean up and EXIT optlog!(uci;info;"quitting"); + println!( + "quitting (total runtime: {:.2}s)", + engine.created_at.elapsed().as_secs_f32() + ); break; } UciMessage::Id { .. } => {} diff --git a/tests/d6.rs b/tests/d6.rs index d5df7d21..2ce43184 100644 --- a/tests/d6.rs +++ b/tests/d6.rs @@ -1,3 +1,4 @@ + use std::io::BufRead; use std::io::Write; use std::path::PathBuf; diff --git a/tests/mate.rs b/tests/mate.rs index e312d44a..386ca850 100644 --- a/tests/mate.rs +++ b/tests/mate.rs @@ -1,3 +1,10 @@ +<<<<<<< HEAD +======= +// 1r2k3/8/K3p3/4p3/4q3/8/5bpr/6q1 b - - 0 44 + +//! How deep can we reach in 10 seconds? + +>>>>>>> 5a70444 (i think i unsafed too hard) use std::io::BufRead; use std::io::Write; use std::path::PathBuf; @@ -16,7 +23,7 @@ fn main() { let startpos = "1r2k3/8/K3p3/4p3/4q3/8/5bpr/6q1 b - - 0 44"; let valid_best_moves = ["b8a8", "e4a4", "e4a8", "g1a1"]; test_mating(startpos, &valid_best_moves); - + let mate_in_2 = "8/1k6/8/8/7n/4Nn2/8/1rq2R1K b - - 0 1"; let valid_best_moves = ["c1f1"]; test_mating(mate_in_2, &valid_best_moves); @@ -86,6 +93,7 @@ fn test_mating(startpos: &str, valid_mates: &[&str]) { ) .black() .on_cyan() + ); } else if parts.iter().any(|x| x.eq_ignore_ascii_case("quitting")) { break; @@ -99,6 +107,6 @@ fn test_mating(startpos: &str, valid_mates: &[&str]) { valid_mates, best_move ); - + child.kill().unwrap(); } From 6bebf2df8632a3c567440e13c31adeb6cae10829 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Wed, 13 Nov 2024 11:20:55 +0700 Subject: [PATCH 2/3] fewer static muts --- src/engine/lib.rs | 3 ++- src/engine/search/negamax.rs | 4 ++-- src/engine/transposition_table/mod.rs | 12 ++++++++---- src/engine/uci.rs | 2 ++ tests/d6.rs | 1 - tests/mate.rs | 12 ++---------- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/engine/lib.rs b/src/engine/lib.rs index f0697422..a61918f7 100644 --- a/src/engine/lib.rs +++ b/src/engine/lib.rs @@ -33,8 +33,9 @@ use crate::search::SEARCHING; use crate::search::SEARCH_TO; use crate::search::SEARCH_UNTIL; use crate::setup::depth::Depth; -use crate::transposition_table::{TranspositionTable, TT_INITIALISED}; +use crate::transposition_table::TranspositionTable; use crate::transposition_table::TT; +use crate::transposition_table::TT_INITIALISED; #[derive(Debug)] pub struct Engine { diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index ae5748a5..ef1f93ff 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -18,18 +18,18 @@ use crate::search::SEARCH_TO; use crate::setup::depth::Depth; use crate::setup::values::Value; use crate::transposition_table::bounds::EvalBound; -use crate::transposition_table::{table_mut_ref, TT_INITIALISED}; +use crate::transposition_table::table_mut_ref; use crate::transposition_table::table_ref; use crate::transposition_table::TableEntry; use crate::transposition_table::TranspositionTable; use crate::transposition_table::TT; +use crate::transposition_table::TT_INITIALISED; #[inline(always)] pub fn searching() -> bool { SEARCHING.load(Ordering::Relaxed) } - #[inline(always)] pub fn search_to() -> Depth { Depth(SEARCH_TO.load(Ordering::Relaxed)) diff --git a/src/engine/transposition_table/mod.rs b/src/engine/transposition_table/mod.rs index fff01b95..909de0c5 100644 --- a/src/engine/transposition_table/mod.rs +++ b/src/engine/transposition_table/mod.rs @@ -1,20 +1,22 @@ pub mod bounds; use std::mem::MaybeUninit; -use std::sync::atomic::{AtomicBool, AtomicU64}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::RwLock; +use anyhow::anyhow; +use anyhow::Result; use chess::ChessMove; use chess::Piece; use chess::Square; -use crate::opts; +use crate::opts; use crate::search::MV; use crate::setup::depth::Depth; use crate::setup::values::Value; use crate::transposition_table::bounds::EvalBound; -use anyhow::{anyhow, Result}; /// # A single transposition table entry. /// [`TranspositionTable`] @@ -63,7 +65,9 @@ pub fn table_ref() -> &'static RwLock { impl TranspositionTable { pub fn new() -> Result { - let size = (opts().hash_size * 1024 / size_of::()).checked_next_power_of_two().ok_or(anyhow!("invalid hash map size (overflowed)"))?; + let size = (opts().hash_size * 1024 / size_of::()) + .checked_next_power_of_two() + .ok_or(anyhow!("invalid hash map size (overflowed)"))?; let mut table = Vec::with_capacity(size); for _ in 0..size { table.push(TableEntry { diff --git a/src/engine/uci.rs b/src/engine/uci.rs index fa17a4e2..1c6f41e7 100644 --- a/src/engine/uci.rs +++ b/src/engine/uci.rs @@ -8,6 +8,8 @@ use lockfree::channel::RecvErr; use crate::optlog; use log::{debug, error}; +use log::debug; +use log::error; use log::trace; use crate::search::exit_condition; diff --git a/tests/d6.rs b/tests/d6.rs index 2ce43184..d5df7d21 100644 --- a/tests/d6.rs +++ b/tests/d6.rs @@ -1,4 +1,3 @@ - use std::io::BufRead; use std::io::Write; use std::path::PathBuf; diff --git a/tests/mate.rs b/tests/mate.rs index 386ca850..e312d44a 100644 --- a/tests/mate.rs +++ b/tests/mate.rs @@ -1,10 +1,3 @@ -<<<<<<< HEAD -======= -// 1r2k3/8/K3p3/4p3/4q3/8/5bpr/6q1 b - - 0 44 - -//! How deep can we reach in 10 seconds? - ->>>>>>> 5a70444 (i think i unsafed too hard) use std::io::BufRead; use std::io::Write; use std::path::PathBuf; @@ -23,7 +16,7 @@ fn main() { let startpos = "1r2k3/8/K3p3/4p3/4q3/8/5bpr/6q1 b - - 0 44"; let valid_best_moves = ["b8a8", "e4a4", "e4a8", "g1a1"]; test_mating(startpos, &valid_best_moves); - + let mate_in_2 = "8/1k6/8/8/7n/4Nn2/8/1rq2R1K b - - 0 1"; let valid_best_moves = ["c1f1"]; test_mating(mate_in_2, &valid_best_moves); @@ -93,7 +86,6 @@ fn test_mating(startpos: &str, valid_mates: &[&str]) { ) .black() .on_cyan() - ); } else if parts.iter().any(|x| x.eq_ignore_ascii_case("quitting")) { break; @@ -107,6 +99,6 @@ fn test_mating(startpos: &str, valid_mates: &[&str]) { valid_mates, best_move ); - + child.kill().unwrap(); } From 838fdcafe1c53036ceed7763bbd8911a680fad86 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Wed, 13 Nov 2024 15:03:33 +0700 Subject: [PATCH 3/3] sigsegv these tt's --- src/benches/ngm.rs | 1 - src/engine/debug.rs | 2 +- src/engine/search/main_search.rs | 3 +-- src/engine/search/negamax.rs | 8 ++++++-- src/engine/transposition_table/mod.rs | 4 ++-- src/engine/transposition_table/tests/mod.rs | 8 ++++---- src/engine/uci.rs | 6 +----- src/sandy/main.rs | 11 ++--------- src/sandy/uci/mod.rs | 6 ------ 9 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/benches/ngm.rs b/src/benches/ngm.rs index 0d98d0a0..5b6a0d0f 100644 --- a/src/benches/ngm.rs +++ b/src/benches/ngm.rs @@ -2,7 +2,6 @@ use criterion::black_box; use criterion::criterion_group; use criterion::criterion_main; use criterion::Criterion; -<<<<<<< HEAD use sandy_engine::opts::Opts; use sandy_engine::search::negamax::ng_test; use sandy_engine::setup::depth::Depth; diff --git a/src/engine/debug.rs b/src/engine/debug.rs index 72dc01fa..fe50b9f9 100644 --- a/src/engine/debug.rs +++ b/src/engine/debug.rs @@ -50,7 +50,7 @@ macro_rules! optlog { } } } - + impl DebugLevel { /// This [`DebugLevel`] is TRACE #[inline(always)] diff --git a/src/engine/search/main_search.rs b/src/engine/search/main_search.rs index 80851a50..8fa50728 100644 --- a/src/engine/search/main_search.rs +++ b/src/engine/search/main_search.rs @@ -154,13 +154,12 @@ impl Engine { if let Some(mv) = best_move { send(&mut publisher, Message::BestMove(MV(mv, best_value))) } - + // looks sketchy, but it's to prevent dropping the sender before the receiver // has gotten the best move. thread::sleep(Duration::from_millis( (SEARCH_THREADS * 2 * UCI_LISTENING_FREQUENCY) as u64, )); - }); Ok(receiver) diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index ef1f93ff..457f60a8 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -46,6 +46,11 @@ pub fn ng_test( { setopts(opts)?; } + unsafe { + TT.write(RwLock::new(TranspositionTable::new().unwrap())); + } + TT_INITIALISED.store(true, Ordering::Relaxed); + Ok(negamax(pos, to_depth, alpha, beta)) } @@ -106,7 +111,6 @@ pub fn negamax(pos: Board, to_depth: Depth, mut alpha: Value, mut beta: Value) - optlog!(search;trace;"ng: {pos}, td: {to_depth:?}, a: {alpha:?}, b: {beta:?}"); optlog!(search;trace;"moves: {}", moves); - if to_depth == Depth::ZERO || moves.is_empty() { let ev = evaluate(&pos, &moves); optlog!(search;trace;"return eval: {:?}", ev); @@ -192,7 +196,7 @@ pub fn negamax(pos: Board, to_depth: Depth, mut alpha: Value, mut beta: Value) - } } - opts().stp(&format!("return max_val: {:?}", best)); + optlog!(search;trace;"return max_val: {:?}", best); this_search_result } diff --git a/src/engine/transposition_table/mod.rs b/src/engine/transposition_table/mod.rs index 909de0c5..86a84011 100644 --- a/src/engine/transposition_table/mod.rs +++ b/src/engine/transposition_table/mod.rs @@ -12,7 +12,7 @@ use chess::ChessMove; use chess::Piece; use chess::Square; -use crate::opts; +use crate::opts::opts; use crate::search::MV; use crate::setup::depth::Depth; use crate::setup::values::Value; @@ -65,7 +65,7 @@ pub fn table_ref() -> &'static RwLock { impl TranspositionTable { pub fn new() -> Result { - let size = (opts().hash_size * 1024 / size_of::()) + let size = (opts()?.hash_size * 1024 / size_of::()) .checked_next_power_of_two() .ok_or(anyhow!("invalid hash map size (overflowed)"))?; let mut table = Vec::with_capacity(size); diff --git a/src/engine/transposition_table/tests/mod.rs b/src/engine/transposition_table/tests/mod.rs index bab93084..ed63c0e2 100644 --- a/src/engine/transposition_table/tests/mod.rs +++ b/src/engine/transposition_table/tests/mod.rs @@ -25,7 +25,7 @@ fn test_pack_then_unpack() { assert_eq!(depth, entry.depth()); assert_eq!(mv, entry.mv()); assert_eq!(bound, entry.bound()); - assert_eq!(true, entry.is_pv()); + assert!(entry.is_pv()); } #[test] @@ -44,7 +44,7 @@ fn test_pack_then_unpack_with_promotion() { assert_eq!(depth, entry.depth()); assert_eq!(mv, entry.mv()); assert_eq!(bound, entry.bound()); - assert_eq!(true, entry.is_pv()); + assert!(entry.is_pv()); } #[test] @@ -63,7 +63,7 @@ fn test_pack_then_unpack_with_no_promotion() { assert_eq!(depth, entry.depth()); assert_eq!(mv, entry.mv()); assert_eq!(bound, entry.bound()); - assert_eq!(false, entry.is_pv()); + assert!(!entry.is_pv()); } #[test] @@ -82,5 +82,5 @@ fn test_pack_then_unpack_edge_case_values() { assert_eq!(depth, entry.depth()); assert_eq!(mv, entry.mv()); assert_eq!(bound, entry.bound()); - assert_eq!(true, entry.is_pv()); + assert!(entry.is_pv()); } diff --git a/src/engine/uci.rs b/src/engine/uci.rs index 1c6f41e7..9753c272 100644 --- a/src/engine/uci.rs +++ b/src/engine/uci.rs @@ -5,13 +5,9 @@ use std::time::Instant; use anyhow::Result; use lockfree::channel::RecvErr; - -use crate::optlog; -use log::{debug, error}; -use log::debug; use log::error; -use log::trace; +use crate::optlog; use crate::search::exit_condition; use crate::search::Message; use crate::search::SearchInfo; diff --git a/src/sandy/main.rs b/src/sandy/main.rs index 62efe6bc..11befe6e 100644 --- a/src/sandy/main.rs +++ b/src/sandy/main.rs @@ -16,11 +16,11 @@ use anyhow::Result; use chess::Board; use log::info; use log::warn; +use sandy_engine::opts::Opts; use sandy_engine::setup::depth::Depth; use sandy_engine::util::fen_to_str; use sandy_engine::util::Print; use sandy_engine::Engine; -use sandy_engine::Opts; use crate::player::terminal_loop; use crate::uci::uci_loop; @@ -41,20 +41,13 @@ fn main() -> Result<()> { .init(); debug_assert!( - size_of::() <= 16, + size_of::() <= 24, "does Opts really need to be {} bytes?", size_of::() ); let mut engine = Engine::new()?; - // println!( - // "depth 4 best move: {}", - // engine.best_move(Depth(4), Duration::from_secs(600))? - // ); - // - // return Ok(()); - let mut read_line = String::new(); loop { read_line.clear(); diff --git a/src/sandy/uci/mod.rs b/src/sandy/uci/mod.rs index 21580fe0..f2df9687 100644 --- a/src/sandy/uci/mod.rs +++ b/src/sandy/uci/mod.rs @@ -9,21 +9,15 @@ use anyhow::Result; use chess::Board; use log::info; use log::warn; -<<<<<<< HEAD use sandy_engine::debug::DebugLevel::debug; use sandy_engine::debug::DebugLevel::info; use sandy_engine::optlog; use sandy_engine::opts::opts; use sandy_engine::opts::setopts; use sandy_engine::opts::Opts; -======= -use sandy_engine::opts; ->>>>>>> e981bb0 (i think i unsafed too hard) use sandy_engine::setup::depth::Depth; use sandy_engine::util::Print; use sandy_engine::Engine; -use sandy_engine::Opts; -use sandy_engine::OPTS; use vampirc_uci::parse_one; use vampirc_uci::Serializable; use vampirc_uci::UciMessage;