Merge commit '2cbc20e846e46da8bfc8e254a7703a0bfad3b850' into cluster

This commit is contained in:
Steinar H. Gunderson
2025-12-25 15:26:23 +01:00
4 changed files with 186 additions and 31 deletions
+161 -16
View File
@@ -20,6 +20,7 @@
#include <algorithm>
#include <array>
#include <list>
#include <atomic>
#include <cassert>
#include <cmath>
@@ -28,6 +29,9 @@
#include <initializer_list>
#include <string>
#include <utility>
#include <chrono>
#include <iostream>
#include <ratio>
#include "cluster.h"
#include "evaluate.h"
@@ -51,6 +55,12 @@ namespace Stockfish {
namespace TB = Tablebases;
void syzygy_extend_pv(const OptionsMap& options,
const Search::LimitsType& limits,
Stockfish::Position& pos,
Stockfish::Search::RootMove& rootMove,
Value& v);
using Eval::evaluate;
using namespace Search;
@@ -2014,18 +2024,145 @@ void SearchManager::check_time(Search::Worker& worker) {
worker.threads.stop = worker.threads.abortedSearch = true;
}
void SearchManager::pv(const Search::Worker& worker,
// Used to correct and extend PVs for moves that have a TB (but not a mate) score.
// Keeps the search based PV for as long as it is verified to maintain the game outcome, truncates afterwards.
// Finally, extends to mate the PV, providing a possible continuation (but not a proven mating line).
void syzygy_extend_pv(const OptionsMap& options,
const Search::LimitsType& limits,
Position& pos,
RootMove& rootMove,
Value& v) {
auto t_start = std::chrono::steady_clock::now();
int moveOverhead = int(options["Move Overhead"]);
// Do not use more than moveOverhead / 2 time, if time management is active.
auto time_abort = [&t_start, &moveOverhead, &limits]() -> bool {
auto t_end = std::chrono::steady_clock::now();
return limits.use_time_management()
&& 2 * std::chrono::duration<double, std::milli>(t_end - t_start).count()
> moveOverhead;
};
std::list<StateInfo> sts;
// Step 1, walk the PV to the last position in TB with correct decisive score
int ply = 0;
while (size_t(ply) < rootMove.pv.size())
{
Move& pvMove = rootMove.pv[ply];
RootMoves legalMoves;
for (const auto& m : MoveList<LEGAL>(pos))
legalMoves.emplace_back(m);
Tablebases::Config config = Tablebases::rank_root_moves(options, pos, legalMoves);
RootMove& rm = *std::find(legalMoves.begin(), legalMoves.end(), pvMove);
if (legalMoves[0].tbRank != rm.tbRank)
break;
ply++;
auto& st = sts.emplace_back();
pos.do_move(pvMove, st);
// don't allow for repetitions or drawing moves along the PV in TB regime.
if (config.rootInTB && pos.is_draw(ply))
{
pos.undo_move(pvMove);
ply--;
break;
}
// Full PV shown will thus be validated and end TB.
// If we can't validate the full PV in time, we don't show it.
if (config.rootInTB && time_abort())
break;
}
// resize the PV to the correct part
rootMove.pv.resize(ply);
// Step 2, now extend the PV to mate, as if the user explores syzygy-tables.info using
// top ranked moves (minimal DTZ), which gives optimal mates only for simple endgames e.g. KRvK
while (!pos.is_draw(0))
{
if (time_abort())
break;
RootMoves legalMoves;
for (const auto& m : MoveList<LEGAL>(pos))
{
auto& rm = legalMoves.emplace_back(m);
StateInfo tmpSI;
pos.do_move(m, tmpSI);
// Give a score of each move to break DTZ ties
// restricting opponent mobility, but not giving the opponent a capture.
for (const auto& mOpp : MoveList<LEGAL>(pos))
rm.tbRank -= pos.capture(mOpp) ? 100 : 1;
pos.undo_move(m);
}
// Mate found
if (legalMoves.size() == 0)
break;
// sort moves according to their above assigned rank,
// This will break ties for moves with equal DTZ in rank_root_moves.
std::stable_sort(
legalMoves.begin(), legalMoves.end(),
[](const Search::RootMove& a, const Search::RootMove& b) { return a.tbRank > b.tbRank; });
// The winning side tries to minimize DTZ, the losing side maximizes it.
Tablebases::Config config = Tablebases::rank_root_moves(options, pos, legalMoves, true);
// If DTZ is not available we might not find a mate, so we bail out.
if (!config.rootInTB || config.cardinality > 0)
break;
ply++;
Move& pvMove = legalMoves[0].pv[0];
rootMove.pv.push_back(pvMove);
auto& st = sts.emplace_back();
pos.do_move(pvMove, st);
}
// Finding a draw in this function is an exceptional case, that cannot happen during engine game play,
// since we have a winning score, and play correctly with TB support.
// However, it can be that a position is draw due to the 50 move rule if it has been been reached
// on the board with a non-optimal 50 move counter e.g. 8/8/6k1/3B4/3K4/4N3/8/8 w - - 54 106
// which TB with dtz counter rounding cannot always correctly rank. See also
// https://github.com/official-stockfish/Stockfish/issues/5175#issuecomment-2058893495
// We adjust the score to match the found PV. Note that a TB loss score can be displayed
// if the engine did not find a drawing move yet, but eventually search will figure it out.
// E.g. 1kq5/q2r4/5K2/8/8/8/8/7Q w - - 96 1
if (pos.is_draw(0))
v = VALUE_DRAW;
// Undo the PV moves.
for (auto it = rootMove.pv.rbegin(); it != rootMove.pv.rend(); ++it)
pos.undo_move(*it);
// Inform if we couldn't get a full extension in time.
if (time_abort())
sync_cout
<< "info string Syzygy based PV extension requires more time, increase Move Overhead as needed."
<< sync_endl;
}
void SearchManager::pv(Search::Worker& worker,
const ThreadPool& threads,
const TranspositionTable& tt,
Depth depth) const {
Depth depth) {
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 =
const auto nodes = Distributed::nodes_searched(threads);
auto& rootMoves = worker.rootMoves;
auto& pos = worker.rootPos;
size_t pvIdx = worker.pvIdx;
size_t multiPV = std::min(size_t(worker.options["MultiPV"]), rootMoves.size());
uint64_t tbHits =
Distributed::tb_hits(threads) + (worker.tbConfig.rootInTB ? rootMoves.size() : 0);
for (size_t i = 0; i < multiPV; ++i)
@@ -2044,6 +2181,13 @@ void SearchManager::pv(const Search::Worker& worker,
bool tb = worker.tbConfig.rootInTB && std::abs(v) <= VALUE_TB;
v = tb ? rootMoves[i].tbScore : v;
bool isExact = i != pvIdx || tb || !updated; // tablebase- and previous-scores are exact
// Potentially correct and extend the PV, and in exceptional cases v
if (std::abs(v) >= VALUE_TB_WIN_IN_MAX_PLY && std::abs(v) < VALUE_MATE_IN_MAX_PLY
&& ((!rootMoves[i].scoreLowerbound && !rootMoves[i].scoreUpperbound) || isExact))
syzygy_extend_pv(worker.options, worker.limits, pos, rootMoves[i], v);
std::string pv;
for (Move m : rootMoves[i].pv)
pv += UCIEngine::move(m, pos.is_chess960()) + " ";
@@ -2065,15 +2209,16 @@ void SearchManager::pv(const Search::Worker& worker,
info.score = {v, pos};
info.wdl = wdl;
if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact
if (!isExact)
info.bound = bound;
info.timeMs = time;
info.nodes = nodes;
info.nps = nodes * 1000 / time;
info.tbHits = tbHits;
info.pv = pv;
info.hashfull = tt.hashfull();
TimePoint time = tm.elapsed_time() + 1;
info.timeMs = time;
info.nodes = nodes;
info.nps = nodes * 1000 / time;
info.tbHits = tbHits;
info.pv = pv;
info.hashfull = tt.hashfull();
updates.onUpdateFull(info);
}
+2 -2
View File
@@ -214,10 +214,10 @@ class SearchManager: public ISearchManager {
void check_time(Search::Worker& worker) override;
void pv(const Search::Worker& worker,
void pv(Search::Worker& worker,
const ThreadPool& threads,
const TranspositionTable& tt,
Depth depth) const;
Depth depth);
Stockfish::TimeManagement tm;
double originalTimeAdjust;
+18 -11
View File
@@ -67,7 +67,7 @@ namespace {
constexpr int TBPIECES = 7; // Max number of supported pieces
constexpr int MAX_DTZ =
1 << 18; // Max DTZ supported, large enough to deal with the syzygy TB limit.
1 << 18; // Max DTZ supported times 2, large enough to deal with the syzygy TB limit.
enum {
BigEndian,
@@ -1576,7 +1576,10 @@ int Tablebases::probe_dtz(Position& pos, ProbeState* result) {
// Use the DTZ tables to rank root moves.
//
// A return value false indicates that not all probes were successful.
bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50) {
bool Tablebases::root_probe(Position& pos,
Search::RootMoves& rootMoves,
bool rule50,
bool rankDTZ) {
ProbeState result = OK;
StateInfo st;
@@ -1587,7 +1590,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool ru
// Check whether a position was repeated since the last zeroing move.
bool rep = pos.has_repeated();
int dtz, bound = rule50 ? (MAX_DTZ - 100) : 1;
int dtz, bound = rule50 ? (MAX_DTZ / 2 - 100) : 1;
// Probe and rank each move
for (auto& m : rootMoves)
@@ -1626,8 +1629,10 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool ru
// Better moves are ranked higher. Certain wins are ranked equally.
// Losing moves are ranked equally unless a 50-move draw is in sight.
int r = dtz > 0 ? (dtz + cnt50 <= 99 && !rep ? MAX_DTZ : MAX_DTZ - (dtz + cnt50))
: dtz < 0 ? (-dtz * 2 + cnt50 < 100 ? -MAX_DTZ : -MAX_DTZ + (-dtz + cnt50))
int r = dtz > 0 ? (dtz + cnt50 <= 99 && !rep ? MAX_DTZ - (rankDTZ ? dtz : 0)
: MAX_DTZ / 2 - (dtz + cnt50))
: dtz < 0 ? (-dtz * 2 + cnt50 < 100 ? -MAX_DTZ - (rankDTZ ? dtz : 0)
: -MAX_DTZ / 2 + (-dtz + cnt50))
: 0;
m.tbRank = r;
@@ -1635,10 +1640,11 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool ru
// 1 cp to cursed wins and let it grow to 49 cp as the positions gets
// closer to a real win.
m.tbScore = r >= bound ? VALUE_MATE - MAX_PLY - 1
: r > 0 ? Value((std::max(3, r - (MAX_DTZ - 200)) * int(PawnValue)) / 200)
: r == 0 ? VALUE_DRAW
: r > -bound ? Value((std::min(-3, r + (MAX_DTZ - 200)) * int(PawnValue)) / 200)
: -VALUE_MATE + MAX_PLY + 1;
: r > 0 ? Value((std::max(3, r - (MAX_DTZ / 2 - 200)) * int(PawnValue)) / 200)
: r == 0 ? VALUE_DRAW
: r > -bound
? Value((std::min(-3, r + (MAX_DTZ / 2 - 200)) * int(PawnValue)) / 200)
: -VALUE_MATE + MAX_PLY + 1;
}
return true;
@@ -1685,7 +1691,8 @@ bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves, boo
Config Tablebases::rank_root_moves(const OptionsMap& options,
Position& pos,
Search::RootMoves& rootMoves) {
Search::RootMoves& rootMoves,
bool rankDTZ) {
Config config;
if (rootMoves.empty())
@@ -1709,7 +1716,7 @@ Config Tablebases::rank_root_moves(const OptionsMap& options,
if (config.cardinality >= popcount(pos.pieces()) && !pos.can_castle(ANY_CASTLING))
{
// Rank moves using DTZ tables
config.rootInTB = root_probe(pos, rootMoves, options["Syzygy50MoveRule"]);
config.rootInTB = root_probe(pos, rootMoves, options["Syzygy50MoveRule"], rankDTZ);
if (!config.rootInTB)
{
+5 -2
View File
@@ -66,9 +66,12 @@ extern int MaxCardinality;
void init(const std::string& paths);
WDLScore probe_wdl(Position& pos, ProbeState* result);
int probe_dtz(Position& pos, ProbeState* result);
bool root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50);
bool root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50, bool rankDTZ);
bool root_probe_wdl(Position& pos, Search::RootMoves& rootMoves, bool rule50);
Config rank_root_moves(const OptionsMap& options, Position& pos, Search::RootMoves& rootMoves);
Config rank_root_moves(const OptionsMap& options,
Position& pos,
Search::RootMoves& rootMoves,
bool rankDTZ = false);
} // namespace Stockfish::Tablebases