diff --git a/src/cluster.cpp b/src/cluster.cpp index 72392c372..dc117359b 100644 --- a/src/cluster.cpp +++ b/src/cluster.cpp @@ -35,7 +35,7 @@ #include "search.h" namespace Stockfish { -namespace Cluster { +namespace Distributed { // Total number of ranks and rank within the communicator static int world_rank = MPI_PROC_NULL; @@ -297,7 +297,7 @@ void cluster_info(const ThreadPool& threads, Depth depth, TimePoint elapsed) { void save(TranspositionTable& TT, ThreadPool& threads, Search::Worker* thread, - TTEntry* tte, + TTWriter ttWriter, Key k, Value v, bool PvHit, @@ -308,7 +308,7 @@ void save(TranspositionTable& TT, uint8_t generation8) { // Standard save to the TT - tte->save(k, v, PvHit, b, d, m, ev, generation8); + ttWriter.write(k, v, PvHit, b, d, m, ev, generation8); // If the entry is of sufficient depth to be worth communicating, take action. if (d > 3) @@ -321,7 +321,7 @@ void save(TranspositionTable& TT, // prepares the send buffer. { std::lock_guard lk(thread->ttCache.mutex); - thread->ttCache.buffer.replace(KeyedTTEntry(k, *tte)); + thread->ttCache.buffer.replace(KeyedTTEntry(k, TTData(m, v, ev, d, b, PvHit))); ++TTCacheCounter; } @@ -364,13 +364,11 @@ void save(TranspositionTable& TT, for (size_t i = irank * recvBuffPerRankSize; i < (irank + 1) * recvBuffPerRankSize; ++i) { - auto&& e = TTSendRecvBuffs[sendRecvPosted % 2][i]; - bool found; - TTEntry* replace_tte; - replace_tte = TT.probe(e.first, found); - replace_tte->save(e.first, e.second.value(), e.second.is_pv(), - e.second.bound(), e.second.depth(), e.second.move(), - e.second.eval(), TT.generation()); + auto&& e = TTSendRecvBuffs[sendRecvPosted % 2][i]; + auto [ttHit, ttData, ttWriterForRecvd] = TT.probe(e.first); + ttWriterForRecvd.write(e.first, e.second.value, e.second.is_pv, + e.second.bound, e.second.depth, e.second.move, + e.second.eval, TT.generation()); } } @@ -478,7 +476,7 @@ uint64_t TT_saves(const ThreadPool& threads) { return TTsavesOthers + threads.TT #include "thread.h" namespace Stockfish { -namespace Cluster { +namespace Distributed { uint64_t nodes_searched(const ThreadPool& threads) { return threads.nodes_searched(); } diff --git a/src/cluster.h b/src/cluster.h index 8098ee5bc..2763969fb 100644 --- a/src/cluster.h +++ b/src/cluster.h @@ -24,6 +24,7 @@ #include #include +#include "misc.h" #include "tt.h" namespace Stockfish { @@ -34,7 +35,7 @@ namespace Search { class Worker; } -/// The Cluster namespace contains functionality required to run on distributed +/// The Distributed namespace contains functionality required to run on distributed /// memory architectures using MPI as the message passing interface. On a high level, /// a 'lazy SMP'-like scheme is implemented where TT saves of sufficient depth are /// collected on each rank and distributed to, and used by, all other ranks, @@ -44,7 +45,7 @@ class Worker; /// quantities per MPI rank. It is recommended to have one rank (MPI process) per node. /// For the non-MPI case, wrappers that will be compiler-optimized away are provided. -namespace Cluster { +namespace Distributed { /// Basic info to find the cluster-wide bestMove struct MoveInfo { @@ -57,8 +58,8 @@ struct MoveInfo { #ifdef USE_MPI -// store the TTEntry with its full key, so it can be saved on the receiver side -using KeyedTTEntry = std::pair; +// store the TTData with its (full) key, so it can be saved on the receiver side +using KeyedTTEntry = std::pair; constexpr std::size_t TTCacheSize = 16; // Threads locally cache their high-depth TT entries till a batch can be send by MPI @@ -67,7 +68,7 @@ class TTCache: public std::array { struct Compare { inline bool operator()(const KeyedTTEntry& lhs, const KeyedTTEntry& rhs) { - return lhs.second.depth() > rhs.second.depth(); + return lhs.second.depth > rhs.second.depth; } }; Compare compare; @@ -96,7 +97,7 @@ inline bool is_root() { return rank() == 0; } void save(TranspositionTable&, ThreadPool&, Search::Worker* thread, - TTEntry* tte, + TTWriter ttWriter, Key k, Value v, bool PvHit, @@ -128,7 +129,7 @@ constexpr bool is_root() { return true; } inline void save(TranspositionTable&, ThreadPool&, Search::Worker*, - TTEntry* tte, + TTWriter ttWriter, Key k, Value v, bool PvHit, @@ -137,7 +138,7 @@ inline void save(TranspositionTable&, Move m, Value ev, uint8_t generation8) { - tte->save(k, v, PvHit, b, d, m, ev, generation8); + ttWriter.write(k, v, PvHit, b, d, m, ev, generation8); } inline void pick_moves(MoveInfo&, std::vector>&) {} inline void ttSendRecvBuff_resize(size_t) {} diff --git a/src/engine.cpp b/src/engine.cpp index 6a9a16713..317768625 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -164,7 +164,7 @@ void Engine::set_tt_size(size_t mb) { tt.resize(mb, threads); // Adjust cluster buffers - Cluster::ttSendRecvBuff_resize(threads.num_threads()); + Distributed::ttSendRecvBuff_resize(threads.num_threads()); } void Engine::set_ponderhit(bool b) { threads.main_manager()->ponder = b; } diff --git a/src/main.cpp b/src/main.cpp index 944e15d1e..724f779a2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,8 +29,8 @@ using namespace Stockfish; int main(int argc, char* argv[]) { - Cluster::init(); - if (Cluster::is_root()) + Distributed::init(); + if (Distributed::is_root()) std::cout << engine_info() << std::endl; Bitboards::init(); @@ -42,7 +42,7 @@ int main(int argc, char* argv[]) { uci.loop(); - Cluster::finalize(); + Distributed::finalize(); return 0; } diff --git a/src/nnue/network.cpp b/src/nnue/network.cpp index 3962d337f..4e9aa03ac 100644 --- a/src/nnue/network.cpp +++ b/src/nnue/network.cpp @@ -260,7 +260,7 @@ void Network::verify(std::string evalfilePath) const { } size_t size = sizeof(*featureTransformer) + sizeof(Arch) * LayerStacks; - if (Cluster::is_root()) + if (Distributed::is_root()) sync_cout << "info string NNUE evaluation using " << evalfilePath << " (" << size / (1024 * 1024) << "MiB, (" << featureTransformer->InputDimensions << ", " << network[0].TransformedFeatureDimensions << ", " << network[0].FC_0_OUTPUTS diff --git a/src/perft.h b/src/perft.h index 0bbca819d..fa21c67dc 100644 --- a/src/perft.h +++ b/src/perft.h @@ -51,7 +51,7 @@ uint64_t perft(Position& pos, Depth depth) { nodes += cnt; pos.undo_move(m); } - if (Root && Cluster::is_root()) + if (Root && Distributed::is_root()) sync_cout << UCIEngine::move(m, pos.is_chess960()) << ": " << cnt << sync_endl; } return nodes; diff --git a/src/search.cpp b/src/search.cpp index 7256b81f2..2ae6313a5 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -168,7 +168,7 @@ void Search::Worker::start_searching() { if (rootMoves.empty()) { rootMoves.emplace_back(Move::none()); - if (Cluster::is_root()) + if (Distributed::is_root()) main_manager()->updates.onUpdateNoMoves( {0, {rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos}}); } @@ -185,7 +185,7 @@ void Search::Worker::start_searching() { // until the GUI sends one of those commands. while (!threads.stop && (main_manager()->ponder || limits.infinite)) { - Cluster::signals_poll(threads); + Distributed::signals_poll(threads); } // Busy wait for a stop or a ponder reset // Stop the threads if not already stopped (also raise the stop if @@ -193,7 +193,7 @@ void Search::Worker::start_searching() { threads.stop = true; // Signal and synchronize all other ranks - Cluster::signals_sync(threads); + Distributed::signals_sync(threads); // Wait until all threads have finished threads.wait_for_search_finished(); @@ -201,7 +201,7 @@ void Search::Worker::start_searching() { // When playing in 'nodes as time' mode, subtract the searched nodes from // the available ones before exiting. if (limits.npmsec) - main_manager()->tm.advance_nodes_time(Cluster::nodes_searched(threads) + main_manager()->tm.advance_nodes_time(Distributed::nodes_searched(threads) - limits.inc[rootPos.side_to_move()]); Worker* bestThread = this; @@ -234,13 +234,13 @@ void Search::Worker::start_searching() { main_manager()->updates.onUpdateFull = std::move(oldOnUpdateFull); // Exchange info as needed - Cluster::MoveInfo mi{bestMove.raw(), ponderMove.raw(), bestThread->completedDepth, - bestThread->rootMoves[0].score, Cluster::rank()}; - Cluster::pick_moves(mi, serializedInfo); + Distributed::MoveInfo mi{bestMove.raw(), ponderMove.raw(), bestThread->completedDepth, + bestThread->rootMoves[0].score, Distributed::rank()}; + Distributed::pick_moves(mi, serializedInfo); main_manager()->bestPreviousScore = static_cast(mi.score); - if (Cluster::is_root()) + if (Distributed::is_root()) { // Send again PV info if we have a new best thread/rank if (bestThread != this || mi.rank != 0) @@ -323,7 +323,7 @@ void Search::Worker::iterative_deepening() { // Iterative deepening loop until requested to stop or the target depth is reached while (++rootDepth < MAX_PLY && !threads.stop - && !(limits.depth && mainThread && Cluster::is_root() && rootDepth > limits.depth)) + && !(limits.depth && mainThread && Distributed::is_root() && rootDepth > limits.depth)) { // Age out PV variability metric if (mainThread) @@ -393,11 +393,11 @@ void Search::Worker::iterative_deepening() { // When failing high/low give some update (without cluttering // the UI) before a re-search. - if (Cluster::is_root() && mainThread && multiPV == 1 + if (Distributed::is_root() && mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta) && elapsed_time() > 3000) { main_manager()->pv(*this, threads, tt, rootDepth); - Cluster::cluster_info(threads, rootDepth, elapsed()); + Distributed::cluster_info(threads, rootDepth, elapsed()); } // In case of failing low/high increase aspiration window and @@ -427,7 +427,7 @@ void Search::Worker::iterative_deepening() { // Sort the PV lines searched so far and update the GUI std::stable_sort(rootMoves.begin() + pvFirst, rootMoves.begin() + pvIdx + 1); - if (Cluster::is_root() && mainThread + if (Distributed::is_root() && mainThread && (threads.stop || pvIdx + 1 == multiPV || elapsed_time() > 3000) // A thread that aborted search can have mated-in/TB-loss PV and score // that cannot be trusted, i.e. it can be delayed or refuted if we would have @@ -436,7 +436,7 @@ void Search::Worker::iterative_deepening() { && !(threads.abortedSearch && rootMoves[0].uciScore <= VALUE_TB_LOSS_IN_MAX_PLY)) { main_manager()->pv(*this, threads, tt, rootDepth); - Cluster::cluster_info(threads, rootDepth, elapsed() + 1); + Distributed::cluster_info(threads, rootDepth, elapsed() + 1); } } @@ -594,16 +594,15 @@ Value Search::Worker::search( StateInfo st; ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); - TTEntry* tte; - Key posKey; - Move ttMove, move, excludedMove, bestMove; - Depth extension, newDepth; - Value bestValue, value, ttValue, eval, maxValue, probCutBeta, singularValue; - bool givesCheck, improving, priorCapture, opponentWorsening; - bool capture, moveCountPruning, ttCapture; - Piece movedPiece; - int moveCount, captureCount, quietCount; - Bound singularBound; + Key posKey; + Move move, excludedMove, bestMove; + Depth extension, newDepth; + Value bestValue, value, eval, maxValue, probCutBeta, singularValue; + bool givesCheck, improving, priorCapture, opponentWorsening; + bool capture, moveCountPruning, ttCapture; + Piece movedPiece; + int moveCount, captureCount, quietCount; + Bound singularBound; // Step 1. Initialize node Worker* thisThread = this; @@ -653,31 +652,32 @@ Value Search::Worker::search( ss->statScore = 0; // Step 4. Transposition table lookup. - excludedMove = ss->excludedMove; - posKey = pos.key(); - tte = tt.probe(posKey, ss->ttHit); - ttValue = ss->ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; - ttMove = rootNode ? thisThread->rootMoves[thisThread->pvIdx].pv[0] - : ss->ttHit ? tte->move() - : Move::none(); - ttCapture = ttMove && pos.capture_stage(ttMove); + excludedMove = ss->excludedMove; + posKey = pos.key(); + auto [ttHit, ttData, ttWriter] = tt.probe(posKey); + // Need further processing of the saved data + ss->ttHit = ttHit; + ttData.move = rootNode ? thisThread->rootMoves[thisThread->pvIdx].pv[0] + : ttHit ? ttData.move + : Move::none(); + ttData.value = ttHit ? value_from_tt(ttData.value, ss->ply, pos.rule50_count()) : VALUE_NONE; + ss->ttPv = excludedMove ? ss->ttPv : PvNode || (ttHit && ttData.is_pv); + ttCapture = ttData.move && pos.capture_stage(ttData.move); // At this point, if excluded, skip straight to step 6, static eval. However, // to save indentation, we list the condition in all code between here and there. - if (!excludedMove) - ss->ttPv = PvNode || (ss->ttHit && tte->is_pv()); // At non-PV nodes we check for an early TT cutoff - if (!PvNode && !excludedMove && tte->depth() > depth - (ttValue <= beta) - && ttValue != VALUE_NONE // Possible in case of TT access race or if !ttHit - && (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER))) + if (!PvNode && !excludedMove && ttData.depth > depth - (ttData.value <= beta) + && ttData.value != VALUE_NONE // Can happen when !ttHit or when access race in probe() + && (ttData.bound & (ttData.value >= beta ? BOUND_LOWER : BOUND_UPPER))) { // If ttMove is quiet, update move sorting heuristics on TT hit (~2 Elo) - if (ttMove && ttValue >= beta) + if (ttData.move && ttData.value >= beta) { // Bonus for a quiet ttMove that fails high (~2 Elo) if (!ttCapture) - update_quiet_stats(pos, ss, *this, ttMove, stat_bonus(depth)); + update_quiet_stats(pos, ss, *this, ttData.move, stat_bonus(depth)); // Extra penalty for early quiet moves of // the previous ply (~1 Elo on STC, ~2 Elo on LTC) @@ -689,7 +689,7 @@ Value Search::Worker::search( // Partial workaround for the graph history interaction problem // For high rule50 counts don't produce transposition table cutoffs. if (pos.rule50_count() < 90) - return ttValue; + return ttData.value; } // Step 5. Tablebases probe @@ -727,9 +727,10 @@ Value Search::Worker::search( if (b == BOUND_EXACT || (b == BOUND_LOWER ? value >= beta : value <= alpha)) { - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(value, ss->ply), - ss->ttPv, b, std::min(MAX_PLY - 1, depth + 6), Move::none(), - VALUE_NONE, tt.generation()); + Distributed::save(tt, threads, thisThread, ttWriter, posKey, + value_to_tt(value, ss->ply), ss->ttPv, b, + std::min(MAX_PLY - 1, depth + 6), Move::none(), VALUE_NONE, + tt.generation()); return value; } @@ -764,7 +765,7 @@ Value Search::Worker::search( else if (ss->ttHit) { // Never assume anything about values stored in TT - unadjustedStaticEval = tte->eval(); + unadjustedStaticEval = ttData.eval; if (unadjustedStaticEval == VALUE_NONE) unadjustedStaticEval = evaluate(networks[numaAccessToken], pos, refreshTable, thisThread->optimism[us]); @@ -774,8 +775,9 @@ Value Search::Worker::search( ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, *thisThread, pos); // ttValue can be used as a better position evaluation (~7 Elo) - if (ttValue != VALUE_NONE && (tte->bound() & (ttValue > eval ? BOUND_LOWER : BOUND_UPPER))) - eval = ttValue; + if (ttData.value != VALUE_NONE + && (ttData.bound & (ttData.value > eval ? BOUND_LOWER : BOUND_UPPER))) + eval = ttData.value; } else { @@ -784,8 +786,9 @@ Value Search::Worker::search( ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, *thisThread, pos); // Static evaluation is saved as it was before adjustment by correction history - Cluster::save(tt, threads, thisThread, tte, posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, - DEPTH_UNSEARCHED, Move::none(), unadjustedStaticEval, tt.generation()); + Distributed::save(tt, threads, thisThread, ttWriter, posKey, VALUE_NONE, ss->ttPv, + BOUND_NONE, DEPTH_UNSEARCHED, Move::none(), unadjustedStaticEval, + tt.generation()); } // Use static evaluation difference to improve quiet move ordering (~9 Elo) @@ -826,7 +829,7 @@ Value Search::Worker::search( && eval - futility_margin(depth, cutNode && !ss->ttHit, improving, opponentWorsening) - (ss - 1)->statScore / 263 >= beta - && eval >= beta && eval < VALUE_TB_WIN_IN_MAX_PLY && (!ttMove || ttCapture)) + && eval >= beta && eval < VALUE_TB_WIN_IN_MAX_PLY && (!ttData.move || ttCapture)) return beta > VALUE_TB_LOSS_IN_MAX_PLY ? beta + (eval - beta) / 3 : eval; // Step 9. Null move search with verification search (~35 Elo) @@ -872,7 +875,7 @@ Value Search::Worker::search( // Step 10. Internal iterative reductions (~9 Elo) // For PV nodes without a ttMove, we decrease depth by 3. - if (PvNode && !ttMove) + if (PvNode && !ttData.move) depth -= 3; // Use qsearch if depth <= 0. @@ -881,8 +884,8 @@ Value Search::Worker::search( // For cutNodes, if depth is high enough, decrease depth by 2 if there is no ttMove, or // by 1 if there is a ttMove with an upper bound. - if (cutNode && depth >= 8 && (!ttMove || tte->bound() == BOUND_UPPER)) - depth -= 1 + !ttMove; + if (cutNode && depth >= 8 && (!ttData.move || ttData.bound == BOUND_UPPER)) + depth -= 1 + !ttData.move; // Step 11. ProbCut (~10 Elo) // If we have a good enough capture (or queen promotion) and a reduced search returns a value @@ -895,11 +898,11 @@ Value Search::Worker::search( // there and in further interactions with transposition table cutoff depth is set to depth - 3 // because probCut search has depth set to depth - 4 but we also do a move before it // So effective depth is equal to depth - 3 - && !(tte->depth() >= depth - 3 && ttValue != VALUE_NONE && ttValue < probCutBeta)) + && !(ttData.depth >= depth - 3 && ttData.value != VALUE_NONE && ttData.value < probCutBeta)) { assert(probCutBeta < VALUE_INFINITE && probCutBeta > beta); - MovePicker mp(pos, ttMove, probCutBeta - ss->staticEval, &thisThread->captureHistory); + MovePicker mp(pos, ttData.move, probCutBeta - ss->staticEval, &thisThread->captureHistory); while ((move = mp.next_move()) != Move::none()) if (move != excludedMove && pos.legal(move)) @@ -930,9 +933,9 @@ Value Search::Worker::search( if (value >= probCutBeta) { // Save ProbCut data into transposition table - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(value, ss->ply), - ss->ttPv, BOUND_LOWER, depth - 3, move, unadjustedStaticEval, - tt.generation()); + Distributed::save(tt, threads, thisThread, ttWriter, posKey, + value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER, depth - 3, + move, unadjustedStaticEval, tt.generation()); return std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY ? value - (probCutBeta - beta) : value; } @@ -945,9 +948,10 @@ moves_loop: // When in check, search starts here // Step 12. A small Probcut idea, when we are in check (~4 Elo) probCutBeta = beta + 388; - if (ss->inCheck && !PvNode && ttCapture && (tte->bound() & BOUND_LOWER) - && tte->depth() >= depth - 4 && ttValue >= probCutBeta - && std::abs(ttValue) < VALUE_TB_WIN_IN_MAX_PLY && std::abs(beta) < VALUE_TB_WIN_IN_MAX_PLY) + if (ss->inCheck && !PvNode && ttCapture && (ttData.bound & BOUND_LOWER) + && ttData.depth >= depth - 4 && ttData.value >= probCutBeta + && std::abs(ttData.value) < VALUE_TB_WIN_IN_MAX_PLY + && std::abs(beta) < VALUE_TB_WIN_IN_MAX_PLY) return probCutBeta; const PieceToHistory* contHist[] = {(ss - 1)->continuationHistory, @@ -960,7 +964,7 @@ moves_loop: // When in check, search starts here Move countermove = prevSq != SQ_NONE ? thisThread->counterMoves[pos.piece_on(prevSq)][prevSq] : Move::none(); - MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &thisThread->captureHistory, + MovePicker mp(pos, ttData.move, depth, &thisThread->mainHistory, &thisThread->captureHistory, contHist, &thisThread->pawnHistory, countermove, ss->killers); value = bestValue; @@ -991,7 +995,7 @@ moves_loop: // When in check, search starts here ss->moveCount = ++moveCount; - if (rootNode && Cluster::is_root() && is_mainthread() && elapsed_time() > 3000) + if (rootNode && Distributed::is_root() && is_mainthread() && elapsed_time() > 3000) { main_manager()->updates.onIter( {depth, UCIEngine::move(move, pos.is_chess960()), moveCount + thisThread->pvIdx}); @@ -1095,12 +1099,12 @@ moves_loop: // When in check, search starts here // Generally, higher singularBeta (i.e closer to ttValue) and lower extension // margins scale well. - if (!rootNode && move == ttMove && !excludedMove + if (!rootNode && move == ttData.move && !excludedMove && depth >= 4 - (thisThread->completedDepth > 35) + ss->ttPv - && std::abs(ttValue) < VALUE_TB_WIN_IN_MAX_PLY && (tte->bound() & BOUND_LOWER) - && tte->depth() >= depth - 3) + && std::abs(ttData.value) < VALUE_TB_WIN_IN_MAX_PLY && (ttData.bound & BOUND_LOWER) + && ttData.depth >= depth - 3) { - Value singularBeta = ttValue - (52 + 80 * (ss->ttPv && !PvNode)) * depth / 64; + Value singularBeta = ttData.value - (52 + 80 * (ss->ttPv && !PvNode)) * depth / 64; Depth singularDepth = newDepth / 2; ss->excludedMove = move; @@ -1135,7 +1139,7 @@ moves_loop: // When in check, search starts here // so we reduce the ttMove in favor of other moves based on some conditions: // If the ttMove is assumed to fail high over current beta (~7 Elo) - else if (ttValue >= beta) + else if (ttData.value >= beta) extension = -3; // If we are on a cutNode but the ttMove is not assumed to fail high over current beta (~1 Elo) @@ -1175,7 +1179,7 @@ moves_loop: // When in check, search starts here // Decrease reduction if position is or has been on the PV (~7 Elo) if (ss->ttPv) - r -= 1 + (ttValue > alpha) + (tte->depth() >= depth); + r -= 1 + (ttData.value > alpha) + (ttData.depth >= depth); // Decrease reduction for PvNodes (~0 Elo on STC, ~2 Elo on LTC) if (PvNode) @@ -1185,8 +1189,8 @@ moves_loop: // When in check, search starts here // Increase reduction for cut nodes (~4 Elo) if (cutNode) - r += 2 - (tte->depth() >= depth && ss->ttPv) - + (!ss->ttPv && move != ttMove && move != ss->killers[0]); + r += 2 - (ttData.depth >= depth && ss->ttPv) + + (!ss->ttPv && move != ttData.move && move != ss->killers[0]); // Increase reduction if ttMove is a capture (~3 Elo) if (ttCapture) @@ -1198,7 +1202,7 @@ moves_loop: // When in check, search starts here // For first picked move (ttMove) reduce reduction // but never allow it to go below 0 (~3 Elo) - else if (move == ttMove) + else if (move == ttData.move) r = std::max(0, r - 2); ss->statScore = 2 * thisThread->mainHistory[us][move.from_to()] @@ -1246,7 +1250,7 @@ moves_loop: // When in check, search starts here else if (!PvNode || moveCount > 1) { // Increase reduction if ttMove is not present (~6 Elo) - if (!ttMove) + if (!ttData.move) r += 2; // Note that if expected reduction is high, we reduce search depth by 1 here (~9 Elo) @@ -1336,7 +1340,7 @@ moves_loop: // When in check, search starts here if (value >= beta) { - ss->cutoffCnt += 1 + !ttMove - (extension >= 2); + ss->cutoffCnt += 1 + !ttData.move - (extension >= 2); assert(value >= beta); // Fail high break; } @@ -1412,12 +1416,12 @@ moves_loop: // When in check, search starts here // Write gathered information in transposition table // Static evaluation is saved as it was before correction history if (!excludedMove && !(rootNode && thisThread->pvIdx)) - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(bestValue, ss->ply), - ss->ttPv, - bestValue >= beta ? BOUND_LOWER - : PvNode && bestMove ? BOUND_EXACT - : BOUND_UPPER, - depth, bestMove, unadjustedStaticEval, tt.generation()); + Distributed::save(tt, threads, thisThread, ttWriter, posKey, + value_to_tt(bestValue, ss->ply), ss->ttPv, + bestValue >= beta ? BOUND_LOWER + : PvNode && bestMove ? BOUND_EXACT + : BOUND_UPPER, + depth, bestMove, unadjustedStaticEval, tt.generation()); // Adjust correction history if (!ss->inCheck && (!bestMove || !pos.capture(bestMove)) @@ -1464,14 +1468,12 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, StateInfo st; ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); - TTEntry* tte; - Key posKey; - Move ttMove, move, bestMove; - Depth ttDepth; - Value bestValue, value, ttValue, futilityBase; - bool pvHit, givesCheck, capture; - int moveCount; - Color us = pos.side_to_move(); + Key posKey; + Move move, bestMove; + Value bestValue, value, futilityBase; + bool pvHit, givesCheck, capture; + int moveCount; + Color us = pos.side_to_move(); // Step 1. Initialize node if (PvNode) @@ -1497,23 +1499,25 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, assert(0 <= ss->ply && ss->ply < MAX_PLY); - // Note that unlike regular search, which stores literal depth, in QS we only store the - // current movegen stage. If in check, we search all evasions and thus store - // DEPTH_QS_CHECKS. (Evasions may be quiet, and _CHECKS includes quiets.) - ttDepth = ss->inCheck || depth >= DEPTH_QS_CHECKS ? DEPTH_QS_CHECKS : DEPTH_QS_NORMAL; + // Note that unlike regular search, which stores the literal depth into the TT, from QS we + // only store the current movegen stage as "depth". If in check, we search all evasions and + // thus store DEPTH_QS_CHECKS. (Evasions may be quiet, and _CHECKS includes quiets.) + Depth qsTtDepth = ss->inCheck || depth >= DEPTH_QS_CHECKS ? DEPTH_QS_CHECKS : DEPTH_QS_NORMAL; // Step 3. Transposition table lookup - posKey = pos.key(); - tte = tt.probe(posKey, ss->ttHit); - ttValue = ss->ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; - ttMove = ss->ttHit ? tte->move() : Move::none(); - pvHit = ss->ttHit && tte->is_pv(); + posKey = pos.key(); + auto [ttHit, ttData, ttWriter] = tt.probe(posKey); + // Need further processing of the saved data + ss->ttHit = ttHit; + ttData.move = ttHit ? ttData.move : Move::none(); + ttData.value = ttHit ? value_from_tt(ttData.value, ss->ply, pos.rule50_count()) : VALUE_NONE; + pvHit = ttHit && ttData.is_pv; // At non-PV nodes we check for an early TT cutoff - if (!PvNode && tte->depth() >= ttDepth - && ttValue != VALUE_NONE // Only in case of TT access race or if !ttHit - && (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER))) - return ttValue; + if (!PvNode && ttData.depth >= qsTtDepth + && ttData.value != VALUE_NONE // Can happen when !ttHit or when access race in probe() + && (ttData.bound & (ttData.value >= beta ? BOUND_LOWER : BOUND_UPPER))) + return ttData.value; // Step 4. Static evaluation of the position Value unadjustedStaticEval = VALUE_NONE; @@ -1524,7 +1528,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, if (ss->ttHit) { // Never assume anything about values stored in TT - unadjustedStaticEval = tte->eval(); + unadjustedStaticEval = ttData.eval; if (unadjustedStaticEval == VALUE_NONE) unadjustedStaticEval = evaluate(networks[numaAccessToken], pos, refreshTable, thisThread->optimism[us]); @@ -1532,9 +1536,9 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, to_corrected_static_eval(unadjustedStaticEval, *thisThread, pos); // ttValue can be used as a better position evaluation (~13 Elo) - if (std::abs(ttValue) < VALUE_TB_WIN_IN_MAX_PLY - && (tte->bound() & (ttValue > bestValue ? BOUND_LOWER : BOUND_UPPER))) - bestValue = ttValue; + if (std::abs(ttData.value) < VALUE_TB_WIN_IN_MAX_PLY + && (ttData.bound & (ttData.value > bestValue ? BOUND_LOWER : BOUND_UPPER))) + bestValue = ttData.value; } else { @@ -1553,9 +1557,10 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, if (std::abs(bestValue) < VALUE_TB_WIN_IN_MAX_PLY && !PvNode) bestValue = (3 * bestValue + beta) / 4; if (!ss->ttHit) - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(bestValue, ss->ply), - false, BOUND_LOWER, DEPTH_UNSEARCHED, Move::none(), - unadjustedStaticEval, tt.generation()); + Distributed::save(tt, threads, thisThread, ttWriter, posKey, + value_to_tt(bestValue, ss->ply), false, BOUND_LOWER, + DEPTH_UNSEARCHED, Move::none(), unadjustedStaticEval, + tt.generation()); return bestValue; } @@ -1575,7 +1580,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, // (Presently, having the checks stage is worth only 1 Elo, and may be removable in the near future, // which would result in only a single stage of QS movegen.) Square prevSq = ((ss - 1)->currentMove).is_ok() ? ((ss - 1)->currentMove).to_sq() : SQ_NONE; - MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &thisThread->captureHistory, + MovePicker mp(pos, ttData.move, depth, &thisThread->mainHistory, &thisThread->captureHistory, contHist, &thisThread->pawnHistory); // Step 5. Loop through all pseudo-legal moves until no moves remain or a beta cutoff occurs. @@ -1694,9 +1699,9 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, // Save gathered info in transposition table // Static evaluation is saved as it was before adjustment by correction history - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(bestValue, ss->ply), pvHit, - bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, ttDepth, bestMove, - unadjustedStaticEval, tt.generation()); + Distributed::save(tt, threads, thisThread, ttWriter, posKey, value_to_tt(bestValue, ss->ply), + pvHit, bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, qsTtDepth, bestMove, + unadjustedStaticEval, tt.generation()); assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); @@ -1717,7 +1722,7 @@ Depth Search::Worker::reduction(bool i, Depth d, int mn, int delta) const { // This function is intended for use only when printing PV outputs, and not used // for making decisions within the search algorithm itself. TimePoint Search::Worker::elapsed() const { - return main_manager()->tm.elapsed([this]() { return Cluster::nodes_searched(threads); }); + return main_manager()->tm.elapsed([this]() { return Distributed::nodes_searched(threads); }); } TimePoint Search::Worker::elapsed_time() const { return main_manager()->tm.elapsed_time(); } @@ -1941,8 +1946,9 @@ void SearchManager::check_time(Search::Worker& worker) { static TimePoint lastInfoTime = now(); - TimePoint elapsed = tm.elapsed([&worker]() { return Cluster::nodes_searched(worker.threads); }); - TimePoint tick = worker.limits.startTime + elapsed; + TimePoint elapsed = + tm.elapsed([&worker]() { return Distributed::nodes_searched(worker.threads); }); + TimePoint tick = worker.limits.startTime + elapsed; if (tick - lastInfoTime >= 1000) { @@ -1951,7 +1957,7 @@ void SearchManager::check_time(Search::Worker& worker) { } // poll on MPI signals - Cluster::signals_poll(worker.threads); + Distributed::signals_poll(worker.threads); // We should not stop pondering until told so by the GUI if (ponder) @@ -1964,7 +1970,7 @@ void SearchManager::check_time(Search::Worker& worker) { && ((worker.limits.use_time_management() && (elapsed > tm.maximum() || stopOnPonderhit)) || (worker.limits.movetime && elapsed >= worker.limits.movetime) || (worker.limits.nodes - && Cluster::nodes_searched(worker.threads) >= worker.limits.nodes))) + && Distributed::nodes_searched(worker.threads) >= worker.limits.nodes))) worker.threads.stop = worker.threads.abortedSearch = true; } @@ -1973,13 +1979,14 @@ void SearchManager::pv(const Search::Worker& worker, const TranspositionTable& tt, Depth depth) const { - const auto nodes = Cluster::nodes_searched(threads); + const auto nodes = Distributed::nodes_searched(threads); const auto& rootMoves = worker.rootMoves; const auto& pos = worker.rootPos; size_t pvIdx = worker.pvIdx; TimePoint time = tm.elapsed_time() + 1; size_t multiPV = std::min(size_t(worker.options["MultiPV"]), rootMoves.size()); - uint64_t tbHits = Cluster::tb_hits(threads) + (worker.tbConfig.rootInTB ? rootMoves.size() : 0); + uint64_t tbHits = + Distributed::tb_hits(threads) + (worker.tbConfig.rootInTB ? rootMoves.size() : 0); for (size_t i = 0; i < multiPV; ++i) { @@ -2041,20 +2048,17 @@ bool RootMove::extract_ponder_from_tt(const TranspositionTable& tt, Position& po StateInfo st; ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); - bool ttHit; - assert(pv.size() == 1); if (pv[0] == Move::none()) return false; pos.do_move(pv[0], st); - TTEntry* tte = tt.probe(pos.key(), ttHit); + auto [ttHit, ttData, ttWriter] = tt.probe(pos.key()); if (ttHit) { - Move m = tte->move(); // Local copy to be SMP safe - if (MoveList(pos).contains(m)) - pv.push_back(m); + if (MoveList(pos).contains(ttData.move)) + pv.push_back(ttData.move); } pos.undo_move(pv[0]); diff --git a/src/search.h b/src/search.h index c99ff689b..dc5342835 100644 --- a/src/search.h +++ b/src/search.h @@ -119,7 +119,9 @@ struct LimitsType { ponderMode = false; } - bool use_time_management() const { return Cluster::is_root() && (time[WHITE] || time[BLACK]); } + bool use_time_management() const { + return Distributed::is_root() && (time[WHITE] || time[BLACK]); + } std::vector searchmoves; TimePoint time[COLOR_NB], inc[COLOR_NB], npmsec, movetime, startTime; @@ -266,25 +268,25 @@ class Worker { #ifdef USE_MPI struct { - std::mutex mutex; - Cluster::TTCache buffer = {}; + std::mutex mutex; + Distributed::TTCache buffer = {}; } ttCache; #endif std::atomic TTsaves; - friend void Cluster::save(TranspositionTable&, - ThreadPool&, - Search::Worker*, - TTEntry* tte, - Key k, - Value v, - bool PvHit, - Bound b, - Depth d, - Move m, - Value ev, - uint8_t generation8); + friend void Distributed::save(TranspositionTable&, + ThreadPool&, + Search::Worker*, + TTWriter ttWriter, + Key k, + Value v, + bool PvHit, + Bound b, + Depth d, + Move m, + Value ev, + uint8_t generation8); private: void iterative_deepening(); diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index fe8d1ddb5..10b684264 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -1467,7 +1467,7 @@ void Tablebases::init(const std::string& paths) { } } - if (Cluster::is_root()) + if (Distributed::is_root()) sync_cout << "info string Found " << TBTables.size() << " tablebases" << sync_endl; } diff --git a/src/thread.cpp b/src/thread.cpp index f348ba5bc..89c2ee886 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -296,7 +296,7 @@ void ThreadPool::start_thinking(const OptionsMap& options, for (auto&& th : threads) th->wait_for_search_finished(); - Cluster::signals_init(); + Distributed::signals_init(); main_thread()->start_searching(); } diff --git a/src/tt.cpp b/src/tt.cpp index 5a44759e4..763e2c9b3 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -25,11 +25,63 @@ #include #include "memory.h" +#include "misc.h" #include "syzygy/tbprobe.h" #include "thread.h" namespace Stockfish { + +// TTEntry struct is the 10 bytes transposition table entry, defined as below: +// +// key 16 bit +// depth 8 bit +// generation 5 bit +// pv node 1 bit +// bound type 2 bit +// move 16 bit +// value 16 bit +// evaluation 16 bit +// +// These fields are in the same order as accessed by TT::probe(), since memory is fastest sequentially. +// Equally, the store order in save() matches this order. + +struct TTEntry { + + // Convert internal bitfields to external types + TTData read() const { + return TTData{Move(move16), Value(value16), + Value(eval16), Depth(depth8 + DEPTH_ENTRY_OFFSET), + Bound(genBound8 & 0x3), bool(genBound8 & 0x4)}; + } + + void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8); + // The returned age is a multiple of TranspositionTable::GENERATION_DELTA + uint8_t relative_age(const uint8_t generation8) const; + + private: + friend class TranspositionTable; + + uint16_t key16; + uint8_t depth8; + uint8_t genBound8; + Move move16; + int16_t value16; + int16_t eval16; +}; + +// `genBound8` is where most of the details are. We use the following constants to manipulate 5 leading generation bits +// and 3 trailing miscellaneous bits. + +// These bits are reserved for other things. +static constexpr unsigned GENERATION_BITS = 3; +// increment for generation field +static constexpr int GENERATION_DELTA = (1 << GENERATION_BITS); +// cycle length +static constexpr int GENERATION_CYCLE = 255 + GENERATION_DELTA; +// mask to pull out generation number +static constexpr int GENERATION_MASK = (0xFF << GENERATION_BITS) & 0xFF; + // DEPTH_ENTRY_OFFSET exists because 1) we use `bool(depth8)` as the occupancy check, but // 2) we need to store negative depths for QS. (`depth8` is the only field with "spare bits": // we sacrifice the ability to store depths greater than 1<<8 less the offset, as asserted below.) @@ -65,12 +117,34 @@ uint8_t TTEntry::relative_age(const uint8_t generation8) const { // is needed to keep the unrelated lowest n bits from affecting // the result) to calculate the entry age correctly even after // generation8 overflows into the next cycle. - - return (TranspositionTable::GENERATION_CYCLE + generation8 - genBound8) - & TranspositionTable::GENERATION_MASK; + return (GENERATION_CYCLE + generation8 - genBound8) & GENERATION_MASK; } +// TTWriter is but a very thin wrapper around the pointer +TTWriter::TTWriter(TTEntry* tte) : + entry(tte) {} + +void TTWriter::write( + Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8) { + entry->save(k, v, pv, b, d, m, ev, generation8); +} + + +// A TranspositionTable is an array of Cluster, of size clusterCount. Each cluster consists of ClusterSize number +// of TTEntry. Each non-empty TTEntry contains information on exactly one position. The size of a Cluster should +// divide the size of a cache line for best performance, as the cacheline is prefetched when possible. + +static constexpr int ClusterSize = 3; + +struct Cluster { + TTEntry entry[ClusterSize]; + char padding[2]; // Pad to 32 bytes +}; + +static_assert(sizeof(Cluster) == 32, "Suboptimal Cluster size"); + + // Sets the size of the transposition table, // measured in megabytes. Transposition table consists // of clusters and each cluster consists of ClusterSize number of TTEntry. @@ -114,32 +188,6 @@ void TranspositionTable::clear(ThreadPool& threads) { } -// Looks up the current position in the transposition -// table. It returns true and a pointer to the TTEntry if the position is found. -// Otherwise, it returns false and a pointer to an empty or least valuable TTEntry -// to be replaced later. The replace value of an entry is calculated as its depth -// minus 8 times its relative age. TTEntry t1 is considered more valuable than -// TTEntry t2 if its replace value is greater than that of t2. -TTEntry* TranspositionTable::probe(const Key key, bool& found) const { - - TTEntry* const tte = first_entry(key); - const uint16_t key16 = uint16_t(key); // Use the low 16 bits as key inside the cluster - - for (int i = 0; i < ClusterSize; ++i) - if (tte[i].key16 == key16) - return found = bool(tte[i].depth8), &tte[i]; - - // Find an entry to be replaced according to the replacement strategy - TTEntry* replace = tte; - for (int i = 1; i < ClusterSize; ++i) - if (replace->depth8 - replace->relative_age(generation8) * 2 - > tte[i].depth8 - tte[i].relative_age(generation8) * 2) - replace = &tte[i]; - - return found = false, replace; -} - - // Returns an approximation of the hashtable // occupation during a search. The hash is x permill full, as per UCI protocol. // Only counts entries which match the current generation. @@ -154,4 +202,46 @@ int TranspositionTable::hashfull() const { return cnt / ClusterSize; } + +void TranspositionTable::new_search() { + // increment by delta to keep lower bits as is + generation8 += GENERATION_DELTA; +} + + +uint8_t TranspositionTable::generation() const { return generation8; } + + +// Looks up the current position in the transposition +// table. It returns true if the position is found. +// Otherwise, it returns false and a pointer to an empty or least valuable TTEntry +// to be replaced later. The replace value of an entry is calculated as its depth +// minus 8 times its relative age. TTEntry t1 is considered more valuable than +// TTEntry t2 if its replace value is greater than that of t2. +std::tuple TranspositionTable::probe(const Key key) const { + + TTEntry* const tte = first_entry(key); + const uint16_t key16 = uint16_t(key); // Use the low 16 bits as key inside the cluster + + for (int i = 0; i < ClusterSize; ++i) + if (tte[i].key16 == key16) + // This gap is the main place for read races. + // After `read()` completes that copy is final, but may be self-inconsistent. + return {bool(tte[i].depth8), tte[i].read(), TTWriter(&tte[i])}; + + // Find an entry to be replaced according to the replacement strategy + TTEntry* replace = tte; + for (int i = 1; i < ClusterSize; ++i) + if (replace->depth8 - replace->relative_age(generation8) * 2 + > tte[i].depth8 - tte[i].relative_age(generation8) * 2) + replace = &tte[i]; + + return {false, replace->read(), TTWriter(replace)}; +} + + +TTEntry* TranspositionTable::first_entry(const Key key) const { + return &table[mul_hi64(key, clusterCount)].entry[0]; +} + } // namespace Stockfish diff --git a/src/tt.h b/src/tt.h index 60cdb323a..c80da0f9d 100644 --- a/src/tt.h +++ b/src/tt.h @@ -21,111 +21,84 @@ #include #include +#include #include "memory.h" -#include "misc.h" #include "types.h" namespace Stockfish { -namespace Cluster { +namespace Distributed { void init(); } -/// TTEntry struct is the 10 bytes transposition table entry, defined as below: -/// -/// key 16 bit -/// depth 8 bit -/// generation 5 bit -/// pv node 1 bit -/// bound type 2 bit -/// move 16 bit -/// value 16 bit -/// eval value 16 bit -// -// These fields are in the same order as accessed by TT::probe(), since memory is fastest sequentially. -// Equally, the store order in save() matches this order. -struct TTEntry { +class ThreadPool; +struct TTEntry; +struct Cluster; - Move move() const { return Move(move16); } - Value value() const { return Value(value16); } - Value eval() const { return Value(eval16); } - Depth depth() const { return Depth(depth8 + DEPTH_ENTRY_OFFSET); } - bool is_pv() const { return bool(genBound8 & 0x4); } - Bound bound() const { return Bound(genBound8 & 0x3); } - void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8); - // The returned age is a multiple of TranspositionTable::GENERATION_DELTA - uint8_t relative_age(const uint8_t generation8) const; +// There is only one global hash table for the engine and all its threads. For chess in particular, we even allow racy +// updates between threads to and from the TT, as taking the time to synchronize access would cost thinking time and +// thus elo. As a hash table, collisions are possible and may cause chess playing issues (bizarre blunders, faulty mate +// reports, etc). Fixing these also loses elo; however such risk decreases quickly with larger TT size. +// +// `probe` is the primary method: given a board position, we lookup its entry in the table, and return a tuple of: +// 1) whether the entry already has this position +// 2) a copy of the prior data (if any) (may be inconsistent due to read races) +// 3) a writer object to this entry +// The copied data and the writer are separated to maintain clear boundaries between local vs global objects. + + +// A copy of the data already in the entry (possibly collided). `probe` may be racy, resulting in inconsistent data. +struct TTData { + Move move; + Value value, eval; + Depth depth; + Bound bound; + bool is_pv; +}; + + +// This is used to make racy writes to the global TT. +struct TTWriter { + public: + void write(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8); private: friend class TranspositionTable; - friend void Cluster::init(); + friend void Distributed::init(); - - uint16_t key16; - uint8_t depth8; - uint8_t genBound8; - Move move16; - int16_t value16; - int16_t eval16; + TTEntry* entry; + TTWriter(TTEntry* tte); }; -class ThreadPool; -// A TranspositionTable is an array of Cluster, of size clusterCount. Each -// cluster consists of ClusterSize number of TTEntry. Each non-empty TTEntry -// contains information on exactly one position. The size of a Cluster should -// divide the size of a cache line for best performance, as the cacheline is -// prefetched when possible. class TranspositionTable { - friend void Cluster::init(); - - static constexpr int ClusterSize = 3; - - struct Cluster { - TTEntry entry[ClusterSize]; - char padding[2]; // Pad to 32 bytes - }; - - static_assert(sizeof(Cluster) == 32, "Unexpected Cluster size"); - - // Constants used to refresh the hash table periodically - - // We have 8 bits available where the lowest 3 bits are - // reserved for other things. - static constexpr unsigned GENERATION_BITS = 3; - // increment for generation field - static constexpr int GENERATION_DELTA = (1 << GENERATION_BITS); - // cycle length - static constexpr int GENERATION_CYCLE = 255 + GENERATION_DELTA; - // mask to pull out generation number - static constexpr int GENERATION_MASK = (0xFF << GENERATION_BITS) & 0xFF; + friend void Distributed::init(); public: ~TranspositionTable() { aligned_large_pages_free(table); } - void new_search() { - // increment by delta to keep lower bits as is - generation8 += GENERATION_DELTA; - } - TTEntry* probe(const Key key, bool& found) const; - int hashfull() const; - void resize(size_t mbSize, ThreadPool& threads); - void clear(ThreadPool& threads); + void resize(size_t mbSize, ThreadPool& threads); // Set TT size + void clear(ThreadPool& threads); // Re-initialize memory, multithreaded + int hashfull() + const; // Approximate what fraction of entries (permille) have been written to during this root search - TTEntry* first_entry(const Key key) const { - return &table[mul_hi64(key, clusterCount)].entry[0]; - } - - uint8_t generation() const { return generation8; } + void + new_search(); // This must be called at the beginning of each root search to track entry aging + uint8_t generation() const; // The current age, used when writing new data to the TT + std::tuple + probe(const Key key) const; // The main method, whose retvals separate local vs global objects + TTEntry* first_entry(const Key key) + const; // This is the hash function; its only external use is memory prefetching. private: friend struct TTEntry; size_t clusterCount; - Cluster* table = nullptr; - uint8_t generation8 = 0; // Size must be not bigger than TTEntry::genBound8 + Cluster* table = nullptr; + + uint8_t generation8 = 0; // Size must be not bigger than TTEntry::genBound8 }; } // namespace Stockfish diff --git a/src/uci.cpp b/src/uci.cpp index 3e452b7bb..0a7fc9dff 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -114,8 +114,8 @@ void UCIEngine::loop() { do { if (cli.argc == 1 - && !Cluster::getline(std::cin, - cmd)) // Wait for an input or an end-of-file (EOF) indication + && !Distributed::getline(std::cin, + cmd)) // Wait for an input or an end-of-file (EOF) indication cmd = "quit"; std::istringstream is(cmd); @@ -133,7 +133,7 @@ void UCIEngine::loop() { else if (token == "ponderhit") engine.set_ponderhit(false); - else if (token == "uci" && Cluster::is_root()) + else if (token == "uci" && Distributed::is_root()) { sync_cout << "id name " << engine_info(true) << "\n" << engine.get_options() << sync_endl; @@ -152,7 +152,7 @@ void UCIEngine::loop() { position(is); else if (token == "ucinewgame") engine.search_clear(); - else if (token == "isready" && Cluster::is_root()) + else if (token == "isready" && Distributed::is_root()) sync_cout << "readyok" << sync_endl; // Add custom non-UCI commands, mainly for debugging purposes. @@ -161,13 +161,13 @@ void UCIEngine::loop() { engine.flip(); else if (token == "bench") bench(is); - else if (token == "d" && Cluster::is_root()) + else if (token == "d" && Distributed::is_root()) sync_cout << engine.visualize() << sync_endl; - else if (token == "eval" && Cluster::is_root()) + else if (token == "eval" && Distributed::is_root()) engine.trace_eval(); - else if (token == "compiler" && Cluster::is_root()) + else if (token == "compiler" && Distributed::is_root()) sync_cout << compiler_info() << sync_endl; - else if (token == "export_net" && Cluster::is_root()) + else if (token == "export_net" && Distributed::is_root()) { std::pair, std::string> files[2]; @@ -181,7 +181,7 @@ void UCIEngine::loop() { } else if ((token == "--help" || token == "help" || token == "--license" || token == "license") - && Cluster::is_root()) + && Distributed::is_root()) sync_cout << "\nStockfish is a powerful chess engine for playing and analyzing." "\nIt is released as free software licensed under the GNU GPLv3 License." @@ -190,7 +190,7 @@ void UCIEngine::loop() { "\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme" "\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n" << sync_endl; - else if (!token.empty() && token[0] != '#' && Cluster::is_root()) + else if (!token.empty() && token[0] != '#' && Distributed::is_root()) sync_cout << "Unknown command: '" << cmd << "'. Type help for more information." << sync_endl; @@ -293,7 +293,7 @@ void UCIEngine::bench(std::istream& args) { if (token == "go" || token == "eval") { - if (Cluster::is_root()) + if (Distributed::is_root()) std::cerr << "\nPosition: " << cnt++ << '/' << num << " (" << engine.fen() << ")" << std::endl; if (token == "go") @@ -311,7 +311,7 @@ void UCIEngine::bench(std::istream& args) { nodes += nodesSearched; nodesSearched = 0; } - else if (Cluster::is_root()) + else if (Distributed::is_root()) engine.trace_eval(); } else if (token == "setoption") @@ -329,7 +329,7 @@ void UCIEngine::bench(std::istream& args) { dbg_print(); - if (Cluster::is_root()) + if (Distributed::is_root()) std::cerr << "\n===========================" // << "\nTotal time (ms) : " << elapsed // << "\nNodes searched : " << nodes // @@ -347,7 +347,7 @@ void UCIEngine::setoption(std::istringstream& is) { std::uint64_t UCIEngine::perft(const Search::LimitsType& limits) { auto nodes = engine.perft(engine.fen(), limits.perft, engine.get_options()["UCI_Chess960"]); - if (Cluster::is_root()) + if (Distributed::is_root()) sync_cout << "\nNodes searched: " << nodes << "\n" << sync_endl; return nodes; } diff --git a/src/ucioption.cpp b/src/ucioption.cpp index 516f8b322..11538dccf 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -52,7 +52,7 @@ void OptionsMap::setoption(std::istringstream& is) { if (options_map.count(name)) options_map[name] = value; - else if (Cluster::is_root()) + else if (Distributed::is_root()) sync_cout << "No such option: " << name << sync_endl; } diff --git a/tests/instrumented.sh b/tests/instrumented.sh index 4c63fc571..e77ee0dd2 100755 --- a/tests/instrumented.sh +++ b/tests/instrumented.sh @@ -39,13 +39,8 @@ case $1 in threads="2" cat << EOF > tsan.supp -race:Stockfish::TTEntry::move -race:Stockfish::TTEntry::depth -race:Stockfish::TTEntry::bound +race:Stockfish::TTEntry::read race:Stockfish::TTEntry::save -race:Stockfish::TTEntry::value -race:Stockfish::TTEntry::eval -race:Stockfish::TTEntry::is_pv race:Stockfish::TranspositionTable::probe race:Stockfish::TranspositionTable::hashfull