From e368f463f9ffb3735ea620f2e94798a0495f1a20 Mon Sep 17 00:00:00 2001 From: crippa1337 Date: Tue, 28 Feb 2023 20:15:17 +0100 Subject: [PATCH 1/6] aging and smaller entries --- src/engine/search.rs | 20 +++++++++--------- src/engine/tt.rs | 49 +++++++++++++++++++++++++++++--------------- src/uci.rs | 2 +- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/engine/search.rs b/src/engine/search.rs index 6914e6f..3f7e7e7 100644 --- a/src/engine/search.rs +++ b/src/engine/search.rs @@ -110,7 +110,7 @@ impl Search { ///////////////////////////////// let tt_entry = self.tt.probe(hash_key); - let tt_hit = tt_entry.key == hash_key; + let tt_hit = tt_entry.key == hash_key as u16; let mut tt_move: Option = None; if tt_hit { let tt_score = self.tt.score_from_tt(tt_entry.score, ply); @@ -119,11 +119,11 @@ impl Search { eval = tt_score; if !is_pv && tt_entry.depth >= depth { - assert!(tt_score != NONE && tt_entry.flags != TTFlag::None); + assert!(tt_score != NONE && tt_entry.flag != TTFlag::None); - if (tt_entry.flags == TTFlag::Exact) - || (tt_entry.flags == TTFlag::LowerBound && tt_score >= beta) - || (tt_entry.flags == TTFlag::UpperBound && tt_score <= alpha) + if (tt_entry.flag == TTFlag::Exact) + || (tt_entry.flag == TTFlag::LowerBound && tt_score >= beta) + || (tt_entry.flag == TTFlag::UpperBound && tt_score <= alpha) { return tt_score; } @@ -309,17 +309,17 @@ impl Search { let hash_key = board.hash(); let tt_entry = self.tt.probe(hash_key); - let tt_hit = tt_entry.key == hash_key; + let tt_hit = tt_entry.key == hash_key as u16; let mut tt_move: Option = None; if tt_hit && !is_pv { let tt_score = self.tt.score_from_tt(tt_entry.score, ply); tt_move = tt_entry.mv; - assert!(tt_score != NONE && tt_entry.flags != TTFlag::None); + assert!(tt_score != NONE && tt_entry.flag != TTFlag::None); - if (tt_entry.flags == TTFlag::Exact) - || (tt_entry.flags == TTFlag::LowerBound && tt_score >= beta) - || (tt_entry.flags == TTFlag::UpperBound && tt_score <= alpha) + if (tt_entry.flag == TTFlag::Exact) + || (tt_entry.flag == TTFlag::LowerBound && tt_score >= beta) + || (tt_entry.flag == TTFlag::UpperBound && tt_score <= alpha) { return tt_score; } diff --git a/src/engine/tt.rs b/src/engine/tt.rs index 7115670..bf7988f 100644 --- a/src/engine/tt.rs +++ b/src/engine/tt.rs @@ -11,16 +11,18 @@ pub enum TTFlag { #[derive(Clone, Copy, Debug)] pub struct TTEntry { - pub key: u64, // 8 bytes + pub key: u16, // 2 bytes + pub epoch: u16, // 2 bytes pub mv: Option, // 4 bytes pub score: i16, // 2 bytes pub depth: u8, // 1 byte - pub flags: TTFlag, // 1 byte + pub flag: TTFlag, // 1 byte } #[derive(Clone)] pub struct TT { pub entries: Vec, + pub epoch: u16, } impl TT { @@ -33,21 +35,32 @@ impl TT { key: 0, mv: None, score: 0, + epoch: 0, depth: 0, - flags: TTFlag::None, + flag: TTFlag::None, }); } - Self { entries } + Self { entries, epoch: 0 } } pub fn index(&self, key: u64) -> usize { - key as usize % self.entries.capacity() + // Cool hack Cosmo taught me + let key = key as u128; + let len = self.entries.len() as u128; + ((key * len) >> 64) as usize } pub fn probe(&self, key: u64) -> TTEntry { - let index = self.index(key); - self.entries[index] + self.entries[self.index(key)] + } + + pub fn age(&mut self) { + self.epoch += 1; + } + + pub fn quality(&self, entry: TTEntry) -> u16 { + entry.epoch + entry.depth as u16 / 3 } pub fn store( @@ -56,19 +69,24 @@ impl TT { mv: Option, score: i16, depth: u8, - flags: TTFlag, + flag: TTFlag, ply: u8, ) { - let index = self.index(key); - - // Always replace scheme - self.entries[index] = TTEntry { - key, + let target_index = self.index(key); + let target = self.entries[target_index]; + let entry = TTEntry { + key: key as u16, mv, score: self.score_to_tt(score, ply), + epoch: self.epoch, depth, - flags, + flag, }; + + // Only replace entries of similar or higher quality + if self.quality(entry) >= self.quality(target) { + self.entries[target_index] = entry; + } } pub fn score_to_tt(&self, score: i16, ply: u8) -> i16 { @@ -92,5 +110,4 @@ impl TT { } } -#[allow(dead_code)] -pub const TT_TEST: () = assert!(std::mem::size_of::() == 16, "TT IS NOT 16 BYTES"); +const _TT_TEST: () = assert!(std::mem::size_of::() == 12); diff --git a/src/uci.rs b/src/uci.rs index 41fb6d1..2178f17 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -307,7 +307,6 @@ fn time_for_move(time: u64, increment: Option, moves_to_go: Option) -> } fn reset_search(search: &mut Search) { - // Reset everything except the transposition table search.stop = false; search.search_type = SearchType::Depth(0); search.timer = None; @@ -317,4 +316,5 @@ fn reset_search(search: &mut Search) { search.seldepth = 0; search.killers = [[None; 2]; MAX_PLY as usize]; search.history.age_table(); + search.tt.age(); } From 6a8e85246b69abae700717577cfbd0f16d2f5551 Mon Sep 17 00:00:00 2001 From: crippa1337 Date: Wed, 1 Mar 2023 00:17:26 +0100 Subject: [PATCH 2/6] fix assertion error --- src/engine/search.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/search.rs b/src/engine/search.rs index 3f7e7e7..6b9ba2c 100644 --- a/src/engine/search.rs +++ b/src/engine/search.rs @@ -311,11 +311,11 @@ impl Search { let tt_entry = self.tt.probe(hash_key); let tt_hit = tt_entry.key == hash_key as u16; let mut tt_move: Option = None; - if tt_hit && !is_pv { + if tt_hit && !is_pv && tt_entry.flag != TTFlag::None { let tt_score = self.tt.score_from_tt(tt_entry.score, ply); tt_move = tt_entry.mv; - assert!(tt_score != NONE && tt_entry.flag != TTFlag::None); + assert!(tt_score != NONE); if (tt_entry.flag == TTFlag::Exact) || (tt_entry.flag == TTFlag::LowerBound && tt_score >= beta) From eee81b1bd72997bea1a3b7f0edcb3f9d91e8f69c Mon Sep 17 00:00:00 2001 From: crippa1337 Date: Wed, 1 Mar 2023 02:07:15 +0100 Subject: [PATCH 3/6] Non-bugged pv table, const generics --- src/engine/pv_table.rs | 36 ++++++++----------- src/engine/search.rs | 81 +++++++++++++++++++++++++++++++----------- src/uci.rs | 1 - 3 files changed, 74 insertions(+), 44 deletions(-) diff --git a/src/engine/pv_table.rs b/src/engine/pv_table.rs index b69db5c..ad51cfa 100644 --- a/src/engine/pv_table.rs +++ b/src/engine/pv_table.rs @@ -3,42 +3,34 @@ use crate::uci::reverse_castling_move; use cozy_chess::{Board, Move}; pub struct PVTable { - pub length: [u8; MAX_PLY as usize], - pub table: [[Option; MAX_PLY as usize]; MAX_PLY as usize], + pub length: usize, + pub table: [Option; MAX_PLY as usize], } impl PVTable { pub fn new() -> Self { PVTable { - length: [0; MAX_PLY as usize], - table: [[None; MAX_PLY as usize]; MAX_PLY as usize], + length: 0, + table: [None; MAX_PLY as usize], } } - pub fn store(&mut self, board: &Board, ply: u8, mut mv: Move) { - // Write to PV table - let uply = ply as usize; - mv = reverse_castling_move(board, mv); - self.table[uply][uply] = Some(mv); - - // Loop over the next ply - for i in (uply + 1)..self.length[uply + 1] as usize { - // Copy move from deeper ply into current line - self.table[uply][i] = self.table[uply + 1][i]; - } + pub fn store(&mut self, board: &Board, mv: Move, old: &Self) { + let mv = reverse_castling_move(board, mv); + self.table[0] = Some(mv); + self.table[1..=old.length].copy_from_slice(&old.table[..old.length]); + self.length = old.length + 1; + } - // Update PV length - self.length[uply] = self.length[uply + 1]; + pub fn moves(&self) -> &[Option] { + &self.table[..self.length] } pub fn pv_string(&self) -> String { let mut pv = String::new(); - for i in 0..self.length[0] { - if self.table[0][i as usize].is_none() { - break; - } + for &mv in self.moves() { pv.push(' '); - pv.push_str(&self.table[0][i as usize].unwrap().to_string()); + pv.push_str(mv.unwrap().to_string().as_str()); } pv diff --git a/src/engine/search.rs b/src/engine/search.rs index 6b9ba2c..97b13a1 100644 --- a/src/engine/search.rs +++ b/src/engine/search.rs @@ -22,7 +22,6 @@ pub struct Search { pub search_type: SearchType, pub timer: Option, pub goal_time: Option, - pub pv_table: PVTable, pub nodes: u32, pub seldepth: u8, pub tt: TT, @@ -38,7 +37,6 @@ impl Search { search_type: SearchType::Depth(0), timer: None, goal_time: None, - pv_table: PVTable::new(), nodes: 0, seldepth: 0, tt, @@ -48,14 +46,26 @@ impl Search { } } - pub fn pvsearch( + fn zw_search( &mut self, board: &Board, + pv: &mut PVTable, + alpha: i16, + beta: i16, + depth: u8, + ply: u8, + ) -> i16 { + self.pvsearch::(board, pv, alpha, beta, depth, ply) + } + + pub fn pvsearch( + &mut self, + board: &Board, + pv: &mut PVTable, mut alpha: i16, beta: i16, depth: u8, ply: u8, - is_pv: bool, ) -> i16 { // Every 1024 nodes, check if it's time to stop if let (Some(timer), Some(goal)) = (self.timer, self.goal_time) { @@ -74,7 +84,8 @@ impl Search { } self.seldepth = max(self.seldepth, ply); - self.pv_table.length[ply as usize] = ply; + pv.length = 0; + let mut old_pv = PVTable::new(); match board.status() { GameStatus::Won => return ply as i16 - MATE, @@ -118,7 +129,7 @@ impl Search { // Use the TT score if available since eval is expensive eval = tt_score; - if !is_pv && tt_entry.depth >= depth { + if !PV && tt_entry.depth >= depth { assert!(tt_score != NONE && tt_entry.flag != TTFlag::None); if (tt_entry.flag == TTFlag::Exact) @@ -138,7 +149,7 @@ impl Search { // Pre-search pruning techniques // /////////////////////////////////// - if !is_pv { + if !PV { // Null Move Pruning (NMP) // If we can give the opponent a free move and still cause a beta cutoff, // we can safely prune this node. This does not work in zugzwang positions @@ -154,7 +165,7 @@ impl Search { let d = depth.saturating_sub(r); let new_board = board.null_move().unwrap(); - let score = -self.pvsearch(&new_board, -beta, -beta + 1, d, ply + 1, false); + let score = -self.zw_search(&new_board, &mut old_pv, -beta, -beta + 1, d, ply + 1); if score >= beta { if score >= TB_WIN_IN_PLY { @@ -184,7 +195,7 @@ impl Search { let mut best_move: Option = None; let mut move_list = movegen::all_moves(self, board, tt_move, ply); let mut quiet_moves = StaticVec::, MAX_MOVES_POSITION>::new(None); - let lmr_depth = if is_pv { 4 } else { 2 }; + let lmr_depth = if PV { 4 } else { 2 }; for i in 0..move_list.len() { let mv = movegen::pick_move(&mut move_list, i); @@ -194,7 +205,7 @@ impl Search { } let mut new_board = board.clone(); - new_board.play(mv); + new_board.play_unchecked(mv); self.game_history.push(new_board.hash()); // Repetition detection self.nodes += 1; @@ -203,7 +214,14 @@ impl Search { // Principal Variation Search let mut score: i16; if i == 0 { - score = -self.pvsearch(&new_board, -beta, -alpha, depth - 1, ply + 1, is_pv); + score = -self.pvsearch::( + &new_board, + &mut old_pv, + -beta, + -alpha, + depth - 1, + ply + 1, + ); } else { // Late Move Reduction (LMR) // Assuming our move ordering is good, later moves will be worse @@ -214,7 +232,7 @@ impl Search { let mut r = LMR.reduction(depth, i) as u8; // Bonus for non PV nodes - r += u8::from(!is_pv); + r += u8::from(!PV); // Malus for capture moves and checks r -= u8::from(capture_move(board, mv)); @@ -228,9 +246,23 @@ impl Search { }; // Zero window search - score = -self.pvsearch(&new_board, -alpha - 1, -alpha, depth - r, ply + 1, false); + score = -self.zw_search( + &new_board, + &mut old_pv, + -alpha - 1, + -alpha, + depth - r, + ply + 1, + ); if alpha < score && score < beta { - score = -self.pvsearch(&new_board, -beta, -alpha, depth - 1, ply + 1, true); + score = -self.pvsearch::( + &new_board, + &mut old_pv, + -beta, + -alpha, + depth - 1, + ply + 1, + ); } } @@ -242,7 +274,7 @@ impl Search { if score > alpha { alpha = score; best_move = Some(mv); - self.pv_table.store(board, ply, mv); + pv.store(board, mv, &old_pv); if score >= beta { if quiet_move(board, mv) { @@ -332,7 +364,7 @@ impl Search { for i in 0..captures.len() { let mv = movegen::pick_move(&mut captures, i); let mut new_board = board.clone(); - new_board.play(mv); + new_board.play_unchecked(mv); self.nodes += 1; @@ -383,17 +415,18 @@ impl Search { let mut best_move: Option = None; let mut score: i16 = 0; + let mut pv = PVTable::new(); for d in 1..depth + 1 { self.seldepth = 0; - score = self.aspiration_window(board, score, d); + score = self.aspiration_window(board, &mut pv, score, d); // Search wasn't complete, do not update best move with garbage if self.stop && d > 1 { break; } - best_move = self.pv_table.table[0][0]; + best_move = pv.table[0]; println!( "info depth {} seldepth {} score {} nodes {} time {} pv{}", @@ -402,14 +435,20 @@ impl Search { self.format_score(score), self.nodes, info_timer.elapsed().as_millis(), - self.pv_table.pv_string() + pv.pv_string() ); } println!("bestmove {}", best_move.unwrap()); } - fn aspiration_window(&mut self, board: &Board, prev_eval: i16, depth: u8) -> i16 { + fn aspiration_window( + &mut self, + board: &Board, + pv: &mut PVTable, + prev_eval: i16, + depth: u8, + ) -> i16 { let mut score: i16; // Window size @@ -425,7 +464,7 @@ impl Search { } loop { - score = self.pvsearch(board, alpha, beta, depth, 0, true); + score = self.pvsearch::(board, pv, alpha, beta, depth, 0); // This result won't be used if self.stop { diff --git a/src/uci.rs b/src/uci.rs index 2178f17..5203ce9 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -311,7 +311,6 @@ fn reset_search(search: &mut Search) { search.search_type = SearchType::Depth(0); search.timer = None; search.goal_time = None; - search.pv_table = crate::engine::pv_table::PVTable::new(); search.nodes = 0; search.seldepth = 0; search.killers = [[None; 2]; MAX_PLY as usize]; From be3086fd306b17a0f27e62f868ce9be59466d4d8 Mon Sep 17 00:00:00 2001 From: crippa1337 Date: Wed, 1 Mar 2023 02:22:03 +0100 Subject: [PATCH 4/6] Change PVS PV call --- src/engine/search.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/engine/search.rs b/src/engine/search.rs index 97b13a1..14cbfb7 100644 --- a/src/engine/search.rs +++ b/src/engine/search.rs @@ -46,6 +46,9 @@ impl Search { } } + // Zero Window Search - A way to reduce the search space in alpha-beta like search algorithms, + // to perform a boolean test, whether a move produces a worse or better score than a passed value. + // (https://www.chessprogramming.org/Null_Window) fn zw_search( &mut self, board: &Board, @@ -245,7 +248,6 @@ impl Search { 1 }; - // Zero window search score = -self.zw_search( &new_board, &mut old_pv, @@ -255,7 +257,7 @@ impl Search { ply + 1, ); if alpha < score && score < beta { - score = -self.pvsearch::( + score = -self.pvsearch::( &new_board, &mut old_pv, -beta, From b154863f67c66c6bddbc2d8c582d356346c26c56 Mon Sep 17 00:00:00 2001 From: crippa1337 Date: Wed, 1 Mar 2023 02:54:10 +0100 Subject: [PATCH 5/6] Refactor quality function --- src/engine/tt.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/engine/tt.rs b/src/engine/tt.rs index bf7988f..32ca0bf 100644 --- a/src/engine/tt.rs +++ b/src/engine/tt.rs @@ -19,6 +19,12 @@ pub struct TTEntry { pub flag: TTFlag, // 1 byte } +impl TTEntry { + fn quality(&self) -> u16 { + self.epoch + self.depth as u16 + } +} + #[derive(Clone)] pub struct TT { pub entries: Vec, @@ -59,10 +65,6 @@ impl TT { self.epoch += 1; } - pub fn quality(&self, entry: TTEntry) -> u16 { - entry.epoch + entry.depth as u16 / 3 - } - pub fn store( &mut self, key: u64, @@ -84,7 +86,7 @@ impl TT { }; // Only replace entries of similar or higher quality - if self.quality(entry) >= self.quality(target) { + if entry.quality() >= target.quality() { self.entries[target_index] = entry; } } From fde274018f538fca164b23cf51d2cdc337be8ffb Mon Sep 17 00:00:00 2001 From: crippa1337 Date: Wed, 1 Mar 2023 03:51:09 +0100 Subject: [PATCH 6/6] New aging formula Score of svart-dev vs svart-master: 171 - 105 - 194 [0.570] 470 ... svart-dev playing White: 84 - 52 - 99 [0.568] 235 ... svart-dev playing Black: 87 - 53 - 95 [0.572] 235 ... White vs Black: 137 - 139 - 194 [0.498] 470 Elo difference: 49.1 +/- 24.1, LOS: 100.0 %, DrawRatio: 41.3 % SPRT: llr 2.96 (100.4%), lbound -2.94, ubound 2.94 - H1 was accepted --- src/engine/tt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/tt.rs b/src/engine/tt.rs index 32ca0bf..a6384da 100644 --- a/src/engine/tt.rs +++ b/src/engine/tt.rs @@ -21,7 +21,7 @@ pub struct TTEntry { impl TTEntry { fn quality(&self) -> u16 { - self.epoch + self.depth as u16 + self.epoch * 2 + self.depth as u16 } }