Merge commit '9032c6cbe74ccf7e8963755501e7e6cc473ae471' into cluster

This is a rather involved merge, since the cluster changes interact
fairly badly with the refactoring we're merging:

The new code wants the search to know nothing at all about the UCI protocol,
it just calls a callback with some worker information (including a Position),
which then transforms that information into a InfoFull which is then sent
through another callback, upon which UCIEngine converts it to UCI and writes
it to stdout.

On the other hand, the MPI information wants to exchange PVs between workers
at this point, and it wants to use the UCI string as the primary medium of
exchange. Position is a lot of work to serialize, so we choose a middle road;
we capture the InfoFull by switching out the callback, serializes it, does the
MPI exchange on that, unserializes it and then continues with the print callback
(if there is new information).
This commit is contained in:
Steinar H. Gunderson
2025-12-24 13:50:14 +01:00
14 changed files with 499 additions and 125 deletions
+2 -2
View File
@@ -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))
+25 -13
View File
@@ -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<std::vector<char>>& 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<char> 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<char> 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<char> 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));
}
}
}
+2 -2
View File
@@ -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<std::vector<char>>& 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<std::vector<char>>&) {}
inline void ttSendRecvBuff_resize(size_t) {}
uint64_t nodes_searched(const ThreadPool&);
uint64_t tb_hits(const ThreadPool&);
+25 -17
View File
@@ -18,21 +18,15 @@
#include "engine.h"
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <deque>
#include <memory>
#include <optional>
#include <sstream>
#include <ostream>
#include <string_view>
#include <utility>
#include <vector>
#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<void(const Engine::InfoShort&)>&& f) {
updateContext.onUpdateNoMoves = std::move(f);
}
void Engine::set_on_update_full(std::function<void(const Engine::InfoFull&)>&& f) {
updateContext.onUpdateFull = std::move(f);
}
void Engine::set_on_iter(std::function<void(const Engine::InfoIter&)>&& f) {
updateContext.onIter = std::move(f);
}
void Engine::set_on_bestmove(std::function<void(std::string_view, std::string_view)>&& 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<std::string>& moves) {
@@ -102,7 +112,7 @@ void Engine::set_position(const std::string& fen, const std::vector<std::string>
// 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::optional<std::string>, 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<StateInfo>(1));
Position p;
p.set(pos.fen(), options["UCI_Chess960"], &trace_states->back());
+21 -5
View File
@@ -19,7 +19,14 @@
#ifndef ENGINE_H_INCLUDED
#define ENGINE_H_INCLUDED
#include "misc.h"
#include <cstddef>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#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(const InfoShort&)>&&);
void set_on_update_full(std::function<void(const InfoFull&)>&&);
void set_on_iter(std::function<void(const InfoIter&)>&&);
void set_on_bestmove(std::function<void(std::string_view, std::string_view)>&&);
// 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
+4 -1
View File
@@ -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();
+48
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "score.h"
#include <cassert>
#include <cmath>
#include <cstdlib>
#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};
}
}
}
+69
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef SCORE_H_INCLUDED
#define SCORE_H_INCLUDED
#include <variant>
#include <utility>
#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<typename T>
bool is() const {
return std::holds_alternative<T>(score);
}
template<typename T>
T get() const {
return std::get<T>(score);
}
template<typename F>
decltype(auto) visit(F&& f) const {
return std::visit(std::forward<F>(f), score);
}
private:
std::variant<Mate, TBWin, InternalUnits> score;
};
}
#endif // #ifndef SCORE_H_INCLUDED
+136 -44
View File
@@ -26,8 +26,7 @@
#include <cstdint>
#include <cstdlib>
#include <initializer_list>
#include <iostream>
#include <sstream>
#include <string>
#include <utility>
#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<std::vector<char>> 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<Value>(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<Move>(mi.move);
ponderMove = static_cast<Move>(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<char> Search::InfoFull::serialize() const {
std::vector<char> 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<char>& 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
+57 -6
View File
@@ -24,8 +24,9 @@
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include <mutex>
@@ -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<char> 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<char>& 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<void(const InfoShort&)>;
using UpdateFull = std::function<void(const InfoFull&)>;
using UpdateIter = std::function<void(const InfoIteration&)>;
using UpdateBestmove = std::function<void(std::string_view, std::string_view)>;
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 {
+9 -7
View File
@@ -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<Search::ISearchManager>(new Search::SearchManager()), 0));
auto manager = std::make_unique<Search::SearchManager>(updateContext);
threads.push_back(new Thread(sharedState, std::move(manager), 0));
while (threads.size() < requested)
threads.push_back(new Thread(
sharedState, std::unique_ptr<Search::ISearchManager>(new Search::NullSearchManager()),
threads.size()));
{
auto null_manager = std::make_unique<Search::NullSearchManager>();
threads.push_back(new Thread(sharedState, std::move(null_manager), threads.size()));
}
clear();
main_thread()->wait_for_search_finished();
+1 -1
View File
@@ -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(); }
+89 -21
View File
@@ -19,15 +19,14 @@
#include "uci.h"
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <deque>
#include <memory>
#include <optional>
#include <sstream>
#include <string_view>
#include <utility>
#include <vector>
@@ -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<typename... Ts>
struct overload: Ts... {
using Ts::operator()...;
};
template<typename... Ts>
overload(Ts...) -> overload<Ts...>;
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<std::string> 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
+11 -6
View File
@@ -21,19 +21,17 @@
#include <iostream>
#include <string>
#include <string_view>
#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