diff --git a/src/Makefile b/src/Makefile index d89f37ac4..d045d7f60 100644 --- a/src/Makefile +++ b/src/Makefile @@ -55,7 +55,7 @@ PGOBENCH = $(WINE_PATH) ./$(EXE) bench SRCS = benchmark.cpp bitboard.cpp evaluate.cpp main.cpp \ misc.cpp movegen.cpp movepick.cpp position.cpp cluster.cpp \ search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp \ - nnue/nnue_misc.cpp nnue/features/half_ka_v2_hm.cpp nnue/network.cpp engine.cpp + nnue/nnue_misc.cpp nnue/features/half_ka_v2_hm.cpp nnue/network.cpp engine.cpp score.cpp HEADERS = benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h \ nnue/nnue_misc.h nnue/features/half_ka_v2_hm.h nnue/layers/affine_transform.h \ @@ -63,7 +63,7 @@ HEADERS = benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h \ nnue/layers/sqr_clipped_relu.h nnue/nnue_accumulator.h nnue/nnue_architecture.h \ nnue/nnue_common.h nnue/nnue_feature_transformer.h position.h \ search.h syzygy/tbprobe.h thread.h thread_win32_osx.h timeman.h \ - tt.h tune.h types.h uci.h ucioption.h perft.h nnue/network.h engine.h \ + tt.h tune.h types.h uci.h ucioption.h perft.h nnue/network.h engine.h score.h \ cluster.h OBJS = $(notdir $(SRCS:.cpp=.o)) diff --git a/src/cluster.cpp b/src/cluster.cpp index 939345ac6..72392c372 100644 --- a/src/cluster.cpp +++ b/src/cluster.cpp @@ -387,7 +387,7 @@ void save(TranspositionTable& TT, /// Picks the bestMove across ranks, and send the associated info and PV to the root of the cluster. /// Note that this bestMove and PV must be output by the root, the guarantee proper ordering of output. /// TODO update to the scheme in master.. can this use aggregation of votes? -void pick_moves(MoveInfo& mi, std::string& PVLine) { +void pick_moves(MoveInfo& mi, std::vector>& serializedInfo) { MoveInfo* pMoveInfo = NULL; if (is_root()) @@ -427,21 +427,33 @@ void pick_moves(MoveInfo& mi, std::string& PVLine) { // Send PV line to root as needed if (mi.rank != 0 && mi.rank == rank()) { - int size; - std::vector vec; - vec.assign(PVLine.begin(), PVLine.end()); - size = vec.size(); - MPI_Send(&size, 1, MPI_INT, 0, 42, MoveComm); - MPI_Send(vec.data(), size, MPI_CHAR, 0, 42, MoveComm); + int numLines = serializedInfo.size(); + MPI_Send(&numLines, 1, MPI_INT, 0, 42, MoveComm); + + for (const auto& serializedInfoOne : serializedInfo) + { + int size; + size = serializedInfoOne.size(); + MPI_Send(&size, 1, MPI_INT, 0, 42, MoveComm); + MPI_Send(serializedInfoOne.data(), size, MPI_CHAR, 0, 42, MoveComm); + } } if (mi.rank != 0 && is_root()) { - int size; - std::vector vec; - MPI_Recv(&size, 1, MPI_INT, mi.rank, 42, MoveComm, MPI_STATUS_IGNORE); - vec.resize(size); - MPI_Recv(vec.data(), size, MPI_CHAR, mi.rank, 42, MoveComm, MPI_STATUS_IGNORE); - PVLine.assign(vec.begin(), vec.end()); + serializedInfo.clear(); + + int numLines; + MPI_Recv(&numLines, 1, MPI_INT, mi.rank, 42, MoveComm, MPI_STATUS_IGNORE); + + for (int i = 0; i < numLines; ++i) + { + int size; + std::vector vec; + MPI_Recv(&size, 1, MPI_INT, mi.rank, 42, MoveComm, MPI_STATUS_IGNORE); + vec.resize(size); + MPI_Recv(vec.data(), size, MPI_CHAR, mi.rank, 42, MoveComm, MPI_STATUS_IGNORE); + serializedInfo.push_back(std::move(vec)); + } } } diff --git a/src/cluster.h b/src/cluster.h index 093049c6c..8098ee5bc 100644 --- a/src/cluster.h +++ b/src/cluster.h @@ -105,7 +105,7 @@ void save(TranspositionTable&, Move m, Value ev, uint8_t generation8); -void pick_moves(MoveInfo& mi, std::string& PVLine); +void pick_moves(MoveInfo& mi, std::vector>& PVLine); void ttSendRecvBuff_resize(size_t nThreads); uint64_t nodes_searched(const ThreadPool&); uint64_t tb_hits(const ThreadPool&); @@ -139,7 +139,7 @@ inline void save(TranspositionTable&, uint8_t generation8) { tte->save(k, v, PvHit, b, d, m, ev, generation8); } -inline void pick_moves(MoveInfo&, std::string&) {} +inline void pick_moves(MoveInfo&, std::vector>&) {} inline void ttSendRecvBuff_resize(size_t) {} uint64_t nodes_searched(const ThreadPool&); uint64_t tb_hits(const ThreadPool&); diff --git a/src/engine.cpp b/src/engine.cpp index 79a2c6047..12fa5c3fd 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -18,21 +18,15 @@ #include "engine.h" -#include -#include -#include -#include -#include -#include #include #include -#include -#include +#include +#include +#include #include -#include "benchmark.h" #include "evaluate.h" -#include "movegen.h" +#include "misc.h" #include "nnue/network.h" #include "nnue/nnue_common.h" #include "perft.h" @@ -40,6 +34,7 @@ #include "search.h" #include "syzygy/tbprobe.h" #include "types.h" +#include "uci.h" #include "ucioption.h" namespace Stockfish { @@ -54,7 +49,6 @@ Engine::Engine(std::string path) : networks(NN::Networks( NN::NetworkBig({EvalFileDefaultNameBig, "None", ""}, NN::EmbeddedNNUEType::BIG), NN::NetworkSmall({EvalFileDefaultNameSmall, "None", ""}, NN::EmbeddedNNUEType::SMALL))) { - Tune::init(options); pos.set(StartFEN, false, &states->back()); } @@ -77,10 +71,26 @@ void Engine::search_clear() { tt.clear(options["Threads"]); threads.clear(); - // @TODO wont work multiple instances + // @TODO wont work with multiple instances Tablebases::init(options["SyzygyPath"]); // Free mapped files } +void Engine::set_on_update_no_moves(std::function&& f) { + updateContext.onUpdateNoMoves = std::move(f); +} + +void Engine::set_on_update_full(std::function&& f) { + updateContext.onUpdateFull = std::move(f); +} + +void Engine::set_on_iter(std::function&& f) { + updateContext.onIter = std::move(f); +} + +void Engine::set_on_bestmove(std::function&& f) { + updateContext.onBestmove = std::move(f); +} + void Engine::wait_for_search_finished() { threads.main_thread()->wait_for_search_finished(); } void Engine::set_position(const std::string& fen, const std::vector& moves) { @@ -102,7 +112,7 @@ void Engine::set_position(const std::string& fen, const std::vector // modifiers -void Engine::resize_threads() { threads.set({options, threads, tt, networks}); } +void Engine::resize_threads() { threads.set({options, threads, tt, networks}, updateContext); } void Engine::set_tt_size(size_t mb) { wait_for_search_finished(); @@ -113,7 +123,7 @@ void Engine::set_ponderhit(bool b) { threads.main_manager()->ponder = b; } // network related -void Engine::verify_networks() { +void Engine::verify_networks() const { networks.big.verify(options["EvalFile"]); networks.small.verify(options["EvalFileSmall"]); } @@ -138,9 +148,7 @@ void Engine::save_network(const std::pair, std::strin OptionsMap& Engine::get_options() { return options; } -uint64_t Engine::nodes_searched() const { return threads.nodes_searched(); } - -void Engine::trace_eval() { +void Engine::trace_eval() const { StateListPtr trace_states(new std::deque(1)); Position p; p.set(pos.fen(), options["UCI_Chess960"], &trace_states->back()); diff --git a/src/engine.h b/src/engine.h index 6afc423de..f74209d90 100644 --- a/src/engine.h +++ b/src/engine.h @@ -19,7 +19,14 @@ #ifndef ENGINE_H_INCLUDED #define ENGINE_H_INCLUDED -#include "misc.h" +#include +#include +#include +#include +#include +#include +#include + #include "nnue/network.h" #include "position.h" #include "search.h" @@ -31,6 +38,10 @@ namespace Stockfish { class Engine { public: + using InfoShort = Search::InfoShort; + using InfoFull = Search::InfoFull; + using InfoIter = Search::InfoIteration; + Engine(std::string path = ""); ~Engine() { wait_for_search_finished(); } @@ -51,9 +62,14 @@ class Engine { void set_ponderhit(bool); void search_clear(); + void set_on_update_no_moves(std::function&&); + void set_on_update_full(std::function&&); + void set_on_iter(std::function&&); + void set_on_bestmove(std::function&&); + // network related - void verify_networks(); + void verify_networks() const; void load_networks(); void load_big_network(const std::string& file); void load_small_network(const std::string& file); @@ -61,9 +77,7 @@ class Engine { // utility functions - void trace_eval(); - // nodes since last search clear - uint64_t nodes_searched() const; + void trace_eval() const; OptionsMap& get_options(); private: @@ -76,6 +90,8 @@ class Engine { ThreadPool threads; TranspositionTable tt; Eval::NNUE::Networks networks; + + Search::SearchManager::UpdateContext updateContext; }; } // namespace Stockfish diff --git a/src/main.cpp b/src/main.cpp index ebe21b8af..944e15d1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,9 +21,9 @@ #include "bitboard.h" #include "misc.h" #include "position.h" -#include "tune.h" #include "types.h" #include "uci.h" +#include "tune.h" using namespace Stockfish; @@ -37,6 +37,9 @@ int main(int argc, char* argv[]) { Position::init(); UCIEngine uci(argc, argv); + + Tune::init(uci.engine_options()); + uci.loop(); Cluster::finalize(); diff --git a/src/score.cpp b/src/score.cpp new file mode 100644 index 000000000..d1a8a6abe --- /dev/null +++ b/src/score.cpp @@ -0,0 +1,48 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "score.h" + +#include +#include +#include + +#include "uci.h" + +namespace Stockfish { + +Score::Score(Value v, const Position& pos) { + assert(-VALUE_INFINITE < v && v < VALUE_INFINITE); + + if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY) + { + score = InternalUnits{UCIEngine::to_cp(v, pos)}; + } + else if (std::abs(v) <= VALUE_TB) + { + auto distance = VALUE_TB - std::abs(v); + score = (v > 0) ? TBWin{distance} : TBWin{-distance}; + } + else + { + auto distance = VALUE_MATE - std::abs(v); + score = (v > 0) ? Mate{distance} : Mate{-distance}; + } +} + +} \ No newline at end of file diff --git a/src/score.h b/src/score.h new file mode 100644 index 000000000..b94d9f7fb --- /dev/null +++ b/src/score.h @@ -0,0 +1,69 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef SCORE_H_INCLUDED +#define SCORE_H_INCLUDED + +#include +#include + +#include "types.h" + +namespace Stockfish { + +class Position; + +class Score { + public: + struct Mate { + int plies; + }; + + struct TBWin { + int plies; + }; + + struct InternalUnits { + int value; + }; + + Score() = default; + Score(Value v, const Position& pos); + + template + bool is() const { + return std::holds_alternative(score); + } + + template + T get() const { + return std::get(score); + } + + template + decltype(auto) visit(F&& f) const { + return std::visit(std::forward(f), score); + } + + private: + std::variant score; +}; + +} + +#endif // #ifndef SCORE_H_INCLUDED diff --git a/src/search.cpp b/src/search.cpp index f810cc7e3..dd28de634 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -26,8 +26,7 @@ #include #include #include -#include -#include +#include #include #include "cluster.h" @@ -159,9 +158,8 @@ void Search::Worker::start_searching() { { rootMoves.emplace_back(Move::none()); if (Cluster::is_root()) - sync_cout << "info depth 0 score " - << UCIEngine::to_score(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos) - << sync_endl; + main_manager()->updates.onUpdateNoMoves( + {0, {rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos}}); } else { @@ -203,9 +201,6 @@ void Search::Worker::start_searching() { && rootMoves[0].pv[0] != Move::none()) bestThread = threads.get_best_thread()->worker.get(); - // Prepare PVLine and ponder move - std::string PVLine = main_manager()->pv(*bestThread, threads, tt, bestThread->completedDepth); - main_manager()->bestPreviousScore = bestThread->rootMoves[0].score; main_manager()->bestPreviousAverageScore = bestThread->rootMoves[0].averageScore; @@ -215,10 +210,22 @@ void Search::Worker::start_searching() { || bestThread->rootMoves[0].extract_ponder_from_tt(tt, rootPos)) ponderMove = bestThread->rootMoves[0].pv[1]; + // Temporarily switch out onUpdateFull to capture the PV information that we need, + // so that we can exchange it through MPI. (We may end up not actually printing + // it out.) + auto oldOnUpdateFull = std::move(main_manager()->updates.onUpdateFull); + std::vector> serializedInfo; // One for each MultiPV. + main_manager()->updates.onUpdateFull = [&](const InfoFull& info) { + serializedInfo.push_back(info.serialize()); + }; + main_manager()->pv(*bestThread, threads, tt, bestThread->completedDepth); + assert(!serializedInfo.empty()); + 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, PVLine); + Cluster::pick_moves(mi, serializedInfo); main_manager()->bestPreviousScore = static_cast(mi.score); @@ -226,18 +233,23 @@ void Search::Worker::start_searching() { { // Send again PV info if we have a new best thread/rank if (bestThread != this || mi.rank != 0) - sync_cout << PVLine << sync_endl; + { + for (const auto& serializedInfoOne : serializedInfo) + { + Search::InfoFull info = Search::InfoFull::unserialize(serializedInfoOne); + main_manager()->updates.onUpdateFull(info); + } + } bestMove = static_cast(mi.move); ponderMove = static_cast(mi.ponder); + std::string ponder; if (ponderMove != Move::none()) - sync_cout << "bestmove " << UCIEngine::move(bestMove, rootPos.is_chess960()) - << " ponder " << UCIEngine::move(ponderMove, rootPos.is_chess960()) - << sync_endl; - else - sync_cout << "bestmove " << UCIEngine::move(bestMove, rootPos.is_chess960()) - << sync_endl; + ponder = UCIEngine::move(ponderMove, rootPos.is_chess960()); + + auto bestmove = UCIEngine::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960()); + main_manager()->updates.onBestmove(bestmove, ponder); } } @@ -371,9 +383,9 @@ void Search::Worker::iterative_deepening() { // the UI) before a re-search. if (Cluster::is_root() && mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta) - && mainThread->tm.elapsed(Cluster::nodes_searched(threads)) > 3000) + && mainThread->tm.elapsed(threads.nodes_searched()) > 3000) { - sync_cout << main_manager()->pv(*this, threads, tt, rootDepth) << sync_endl; + main_manager()->pv(*this, threads, tt, rootDepth); Cluster::cluster_info(threads, rootDepth, mainThread->tm.elapsed(Cluster::nodes_searched(threads))); } @@ -414,7 +426,7 @@ void Search::Worker::iterative_deepening() { // below pick a proven score/PV for this thread (from the previous iteration). && !(threads.abortedSearch && rootMoves[0].uciScore <= VALUE_TB_LOSS_IN_MAX_PLY)) { - sync_cout << main_manager()->pv(*this, threads, tt, rootDepth) << sync_endl; + main_manager()->pv(*this, threads, tt, rootDepth); Cluster::cluster_info(threads, rootDepth, mainThread->tm.elapsed(Cluster::nodes_searched(threads)) + 1); } @@ -969,10 +981,11 @@ moves_loop: // When in check, search starts here ss->moveCount = ++moveCount; if (rootNode && Cluster::is_root() && is_mainthread() - && main_manager()->tm.elapsed(Cluster::nodes_searched(threads)) > 3000) - sync_cout << "info depth " << depth << " currmove " - << UCIEngine::move(move, pos.is_chess960()) << " currmovenumber " - << moveCount + thisThread->pvIdx << sync_endl; + && main_manager()->tm.elapsed(threads.nodes_searched()) > 3000) + { + main_manager()->updates.onIter( + {depth, UCIEngine::move(move, pos.is_chess960()), moveCount + thisThread->pvIdx}); + } if (PvNode) (ss + 1)->pv = nullptr; @@ -1913,11 +1926,10 @@ void SearchManager::check_time(Search::Worker& worker) { worker.threads.stop = worker.threads.abortedSearch = true; } -std::string SearchManager::pv(const Search::Worker& worker, - const ThreadPool& threads, - const TranspositionTable& tt, - Depth depth) const { - std::stringstream ss; +void SearchManager::pv(const Search::Worker& worker, + const ThreadPool& threads, + const TranspositionTable& tt, + Depth depth) const { const auto nodes = Cluster::nodes_searched(threads); const auto& rootMoves = worker.rootMoves; @@ -1943,29 +1955,39 @@ std::string SearchManager::pv(const Search::Worker& worker, bool tb = worker.tbConfig.rootInTB && std::abs(v) <= VALUE_TB; v = tb ? rootMoves[i].tbScore : v; - if (ss.rdbuf()->in_avail()) // Not at first line - ss << "\n"; + std::string pv; + for (Move m : rootMoves[i].pv) + pv += UCIEngine::move(m, pos.is_chess960()) + " "; - ss << "info" - << " depth " << d << " seldepth " << rootMoves[i].selDepth << " multipv " << i + 1 - << " score " << UCIEngine::to_score(v, pos); + // remove last whitespace + if (!pv.empty()) + pv.pop_back(); - if (worker.options["UCI_ShowWDL"]) - ss << UCIEngine::wdl(v, pos); + auto wdl = worker.options["UCI_ShowWDL"] ? UCIEngine::wdl(v, pos) : ""; + auto bound = rootMoves[i].scoreLowerbound + ? "lowerbound" + : (rootMoves[i].scoreUpperbound ? "upperbound" : ""); + + InfoFull info; + + info.depth = d; + info.selDepth = rootMoves[i].selDepth; + info.multiPV = i + 1; + info.score = {v, pos}; + info.wdl = wdl; if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact - ss << (rootMoves[i].scoreLowerbound - ? " lowerbound" - : (rootMoves[i].scoreUpperbound ? " upperbound" : "")); + info.bound = bound; - ss << " nodes " << nodes << " nps " << nodes * 1000 / time << " hashfull " << tt.hashfull() - << " tbhits " << tbHits << " time " << time << " pv"; + info.timeMs = time; + info.nodes = nodes; + info.nps = nodes * 1000 / time; + info.tbHits = tbHits; + info.pv = pv; + info.hashfull = tt.hashfull(); - for (Move m : rootMoves[i].pv) - ss << " " << UCIEngine::move(m, pos.is_chess960()); + updates.onUpdateFull(info); } - - return ss.str(); } // Called in case we have no ponder move before exiting the search, @@ -1997,5 +2019,75 @@ bool RootMove::extract_ponder_from_tt(const TranspositionTable& tt, Position& po return pv.size() > 1; } +std::vector Search::InfoFull::serialize() const { + std::vector vec; + vec.resize(sizeof(*this) + 3 * sizeof(size_t) + wdl.size() + bound.size() + pv.size()); + char* ptr = vec.data(); + + // The base struct. + memcpy(ptr, this, sizeof(*this)); + ptr += sizeof(*this); + + // All string lengths. + size_t wdl_len = wdl.size(); + memcpy(ptr, &wdl_len, sizeof(wdl_len)); + ptr += sizeof(wdl_len); + + size_t bound_len = bound.size(); + memcpy(ptr, &bound_len, sizeof(bound_len)); + ptr += sizeof(bound_len); + + size_t pv_len = pv.size(); + memcpy(ptr, &pv_len, sizeof(pv_len)); + ptr += sizeof(pv_len); + + // The string data itself. + memcpy(ptr, wdl.data(), wdl_len); + ptr += wdl_len; + + memcpy(ptr, bound.data(), bound_len); + ptr += bound_len; + + memcpy(ptr, pv.data(), pv_len); + ptr += pv_len; + + assert(ptr == vec.data() + vec.size()); + return vec; +} + +InfoFull Search::InfoFull::unserialize(const std::vector& buf) { + InfoFull info; + const char* ptr = buf.data(); + + // The base struct. + memcpy(&info, ptr, sizeof(info)); + ptr += sizeof(info); + + // All string lengths. + size_t wdl_len; + memcpy(&wdl_len, ptr, sizeof(wdl_len)); + ptr += sizeof(wdl_len); + + size_t bound_len; + memcpy(&bound_len, ptr, sizeof(bound_len)); + ptr += sizeof(bound_len); + + size_t pv_len; + memcpy(&pv_len, ptr, sizeof(pv_len)); + ptr += sizeof(pv_len); + + // The string data itself. + info.wdl = std::string_view(ptr, wdl_len); + ptr += wdl_len; + + info.bound = std::string_view(ptr, bound_len); + ptr += bound_len; + + info.pv = std::string_view(ptr, pv_len); + ptr += pv_len; + + assert(ptr == buf.data() + buf.size()); + return info; +} } // namespace Stockfish diff --git a/src/search.h b/src/search.h index f966bcbf6..634a37513 100644 --- a/src/search.h +++ b/src/search.h @@ -24,8 +24,9 @@ #include #include #include +#include #include -#include +#include #include #include @@ -33,6 +34,7 @@ #include "misc.h" #include "movepick.h" #include "position.h" +#include "score.h" #include "syzygy/tbprobe.h" #include "timeman.h" #include "types.h" @@ -141,7 +143,6 @@ struct SharedState { tt(transpositionTable), networks(nets) {} - const OptionsMap& options; ThreadPool& threads; TranspositionTable& tt; @@ -158,16 +159,64 @@ class ISearchManager { virtual void check_time(Search::Worker&) = 0; }; +struct InfoShort { + int depth; + Score score; +}; + +struct InfoFull: InfoShort { + int selDepth; + size_t multiPV; + std::string_view wdl; + std::string_view bound; + size_t timeMs; + size_t nodes; + size_t nps; + size_t tbHits; + std::string_view pv; + int hashfull; + + std::vector serialize() const; + + // NOTE: The returned InfoFull will contain string_views that point + // into the given buffer, so it must live (and not be reallocated) + // (and not be reallocated) for at least as long as you want to + // access fields in the InfoFull. + static InfoFull unserialize(const std::vector& buf); +}; + +struct InfoIteration { + int depth; + std::string_view currmove; + size_t currmovenumber; +}; + // SearchManager manages the search from the main thread. It is responsible for // keeping track of the time, and storing data strictly related to the main thread. class SearchManager: public ISearchManager { public: + using UpdateShort = std::function; + using UpdateFull = std::function; + using UpdateIter = std::function; + using UpdateBestmove = std::function; + + struct UpdateContext { + UpdateShort onUpdateNoMoves; + UpdateFull onUpdateFull; + UpdateIter onIter; + UpdateBestmove onBestmove; + }; + + + SearchManager(UpdateContext& updateContext) : + updates(updateContext) {} + void check_time(Search::Worker& worker) override; - std::string pv(const Search::Worker& worker, - const ThreadPool& threads, - const TranspositionTable& tt, - Depth depth) const; + void pv(const Search::Worker& worker, + const ThreadPool& threads, + const TranspositionTable& tt, + Depth depth) const; Stockfish::TimeManagement tm; int callsCnt; @@ -180,6 +229,8 @@ class SearchManager: public ISearchManager { bool stopOnPonderhit; size_t id; + + UpdateContext& updates; }; class NullSearchManager: public ISearchManager { diff --git a/src/thread.cpp b/src/thread.cpp index 59aeeae46..888dfb25f 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -121,7 +121,8 @@ uint64_t ThreadPool::TT_saves() const { return accumulate(&Search::Worker::TTsav // Creates/destroys threads to match the requested number. // Created and launched threads will immediately go to sleep in idle_loop. // Upon resizing, threads are recreated to allow for binding if necessary. -void ThreadPool::set(Search::SharedState sharedState) { +void ThreadPool::set(Search::SharedState sharedState, + Search::SearchManager::UpdateContext& updateContext) { if (threads.size() > 0) // destroy any existing thread(s) { @@ -135,14 +136,15 @@ void ThreadPool::set(Search::SharedState sharedState) { if (requested > 0) // create new thread(s) { - threads.push_back(new Thread( - sharedState, std::unique_ptr(new Search::SearchManager()), 0)); - + auto manager = std::make_unique(updateContext); + threads.push_back(new Thread(sharedState, std::move(manager), 0)); while (threads.size() < requested) - threads.push_back(new Thread( - sharedState, std::unique_ptr(new Search::NullSearchManager()), - threads.size())); + { + auto null_manager = std::make_unique(); + threads.push_back(new Thread(sharedState, std::move(null_manager), threads.size())); + } + clear(); main_thread()->wait_for_search_finished(); diff --git a/src/thread.h b/src/thread.h index 4bad9e8c8..a26db7a3b 100644 --- a/src/thread.h +++ b/src/thread.h @@ -83,7 +83,7 @@ class ThreadPool { void start_thinking(const OptionsMap&, Position&, StateListPtr&, Search::LimitsType); void clear(); - void set(Search::SharedState); + void set(Search::SharedState, Search::SearchManager::UpdateContext&); Search::SearchManager* main_manager(); Thread* main_thread() const { return threads.front(); } diff --git a/src/uci.cpp b/src/uci.cpp index 5dd2aefcb..6b120b8ec 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -19,15 +19,14 @@ #include "uci.h" #include -#include #include #include #include -#include #include #include #include #include +#include #include #include @@ -36,10 +35,8 @@ #include "engine.h" #include "evaluate.h" #include "movegen.h" -#include "nnue/network.h" -#include "nnue/nnue_common.h" -#include "perft.h" #include "position.h" +#include "score.h" #include "search.h" #include "syzygy/tbprobe.h" #include "types.h" @@ -50,6 +47,13 @@ namespace Stockfish { constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048; +template +struct overload: Ts... { + using Ts::operator()...; +}; + +template +overload(Ts...) -> overload; UCIEngine::UCIEngine(int argc, char** argv) : engine(argv[0]), @@ -82,6 +86,12 @@ UCIEngine::UCIEngine(int argc, char** argv) : options["EvalFileSmall"] << Option(EvalFileDefaultNameSmall, [this](const Option& o) { engine.load_small_network(o); }); + + engine.set_on_iter([](const auto& i) { on_iter(i); }); + engine.set_on_update_no_moves([](const auto& i) { on_update_no_moves(i); }); + engine.set_on_update_full([&](const auto& i) { on_update_full(i, options["UCI_ShowWDL"]); }); + engine.set_on_bestmove([](const auto& bm, const auto& p) { on_bestmove(bm, p); }); + engine.load_networks(); engine.resize_threads(); engine.search_clear(); // After threads are up @@ -225,6 +235,13 @@ void UCIEngine::go(Position& pos, std::istringstream& is) { void UCIEngine::bench(Position& pos, std::istream& args) { std::string token; uint64_t num, nodes = 0, cnt = 1; + uint64_t nodesSearched = 0; + const auto& options = engine.get_options(); + + engine.set_on_update_full([&](const auto& i) { + nodesSearched = i.nodes; + on_update_full(i, options["UCI_ShowWDL"]); + }); std::vector list = setup_bench(pos, args); @@ -247,7 +264,8 @@ void UCIEngine::bench(Position& pos, std::istream& args) { { go(pos, is); engine.wait_for_search_finished(); - nodes += Cluster::nodes_searched(engine); + nodes += nodesSearched; + nodesSearched = 0; } else if (Cluster::is_root()) engine.trace_eval(); @@ -271,6 +289,9 @@ void UCIEngine::bench(Position& pos, std::istream& args) { std::cerr << "\n===========================" << "\nTotal time (ms) : " << elapsed << "\nNodes searched : " << nodes << "\nNodes/second : " << 1000 * nodes / elapsed << std::endl; + + // reset callback, to not capture a dangling reference to nodesSearched + engine.set_on_update_full([&](const auto& i) { on_update_full(i, options["UCI_ShowWDL"]); }); } @@ -341,22 +362,22 @@ int win_rate_model(Value v, const Position& pos) { } } -std::string UCIEngine::to_score(Value v, const Position& pos) { - assert(-VALUE_INFINITE < v && v < VALUE_INFINITE); +std::string UCIEngine::format_score(const Score& s) { + constexpr int TB_CP = 20000; + const auto format = + overload{[](Score::Mate mate) -> std::string { + auto m = (mate.plies > 0 ? (mate.plies + 1) : -mate.plies) / 2; + return std::string("mate ") + std::to_string(m); + }, + [](Score::TBWin tb) -> std::string { + return std::string("cp ") + + std::to_string((tb.plies > 0 ? TB_CP - tb.plies : -TB_CP + tb.plies)); + }, + [](Score::InternalUnits units) -> std::string { + return std::string("cp ") + std::to_string(units.value); + }}; - std::stringstream ss; - - if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY) - ss << "cp " << to_cp(v, pos); - else if (std::abs(v) <= VALUE_TB) - { - const int ply = VALUE_TB - std::abs(v); // recompute ss->ply - ss << "cp " << (v > 0 ? 20000 - ply : -20000 + ply); - } - else - ss << "mate " << (v > 0 ? VALUE_MATE - v + 1 : -VALUE_MATE - v) / 2; - - return ss.str(); + return s.visit(format); } // Turns a Value to an integer centipawn number, @@ -420,4 +441,51 @@ Move UCIEngine::to_move(const Position& pos, std::string str) { return Move::none(); } +void UCIEngine::on_update_no_moves(const Engine::InfoShort& info) { + sync_cout << "info depth" << info.depth << " score " << format_score(info.score) << sync_endl; +} + +void UCIEngine::on_update_full(const Engine::InfoFull& info, bool showWDL) { + std::stringstream ss; + + ss << "info"; + ss << " depth " << info.depth // + << " seldepth " << info.selDepth // + << " multipv " << info.multiPV // + << " score " << format_score(info.score); // + + if (showWDL) + ss << " wdl " << info.wdl; + + if (!info.bound.empty()) + ss << " " << info.bound; + + ss << " nodes " << info.nodes // + << " nps " << info.nps // + << " hashfull " << info.hashfull // + << " tbhits " << info.tbHits // + << " time " << info.timeMs // + << " pv " << info.pv; // + + sync_cout << ss.str() << sync_endl; +} + +void UCIEngine::on_iter(const Engine::InfoIter& info) { + std::stringstream ss; + + ss << "info"; + ss << " depth " << info.depth // + << " currmove " << info.currmove // + << " currmovenumber " << info.currmovenumber; // + + sync_cout << ss.str() << sync_endl; +} + +void UCIEngine::on_bestmove(std::string_view bestmove, std::string_view ponder) { + sync_cout << "bestmove " << bestmove; + if (!ponder.empty()) + std::cout << " ponder " << ponder; + std::cout << sync_endl; +} + } // namespace Stockfish diff --git a/src/uci.h b/src/uci.h index c4e90b48d..fa8c57fd9 100644 --- a/src/uci.h +++ b/src/uci.h @@ -21,19 +21,17 @@ #include #include +#include #include "engine.h" #include "misc.h" -#include "nnue/network.h" -#include "position.h" #include "search.h" -#include "thread.h" -#include "tt.h" -#include "ucioption.h" namespace Stockfish { +class Position; class Move; +class Score; enum Square : int; using Value = int; @@ -44,7 +42,7 @@ class UCIEngine { void loop(); static int to_cp(Value v, const Position& pos); - static std::string to_score(Value v, const Position& pos); + static std::string format_score(const Score& s); static std::string square(Square s); static std::string move(Move m, bool chess960); static std::string wdl(Value v, const Position& pos); @@ -52,6 +50,8 @@ class UCIEngine { static Search::LimitsType parse_limits(const Position& pos, std::istream& is); + auto& engine_options() { return engine.get_options(); } + private: Engine engine; CommandLine cli; @@ -60,6 +60,11 @@ class UCIEngine { void bench(Position& pos, std::istream& args); void position(std::istringstream& is); void setoption(std::istringstream& is); + + static void on_update_no_moves(const Engine::InfoShort& info); + static void on_update_full(const Engine::InfoFull& info, bool showWDL); + static void on_iter(const Engine::InfoIter& info); + static void on_bestmove(std::string_view bestmove, std::string_view ponder); }; } // namespace Stockfish