1
0
Fork 0
mirror of https://github.com/sockspls/badfish synced 2025-04-29 16:23:09 +00:00

Fix nodestime

1. The current time management system utilizes limits.inc and
limits.time, which can represent either milliseconds or node count,
depending on whether the nodestime option is active. There have been
several modifications which brought Elo gain for typical uses (i.e.
real-time matches), however some of these changes overlooked such
distinction. This patch adjusts constants and multiplication/division to
more accurately simulate real TC conditions when nodestime is used.

2. The advance_nodes_time function has a bug that can extend the time
limit when availableNodes reaches exact zero. This patch fixes the bug
by initializing the variable to -1 and make sure it does not go below
zero.

3. elapsed_time function is newly introduced to print PV in the UCI
output based on real time. This makes PV output more consistent with the
behavior of trivial use cases.

closes https://github.com/official-stockfish/Stockfish/pull/5186

No functional changes
This commit is contained in:
MinetaS 2024-05-08 03:26:09 +09:00 committed by Joost VandeVondele
parent db147fe258
commit 2dbb44e28d
4 changed files with 47 additions and 27 deletions

View file

@ -190,8 +190,8 @@ void Search::Worker::start_searching() {
// When playing in 'nodes as time' mode, subtract the searched nodes from // When playing in 'nodes as time' mode, subtract the searched nodes from
// the available ones before exiting. // the available ones before exiting.
if (limits.npmsec) if (limits.npmsec)
main_manager()->tm.advance_nodes_time(limits.inc[rootPos.side_to_move()] main_manager()->tm.advance_nodes_time(threads.nodes_searched()
- threads.nodes_searched()); - limits.inc[rootPos.side_to_move()]);
Worker* bestThread = this; Worker* bestThread = this;
Skill skill = Skill skill =
@ -347,7 +347,7 @@ void Search::Worker::iterative_deepening() {
// When failing high/low give some update (without cluttering // When failing high/low give some update (without cluttering
// the UI) before a re-search. // the UI) before a re-search.
if (mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta) if (mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta)
&& elapsed() > 3000) && elapsed_time() > 3000)
main_manager()->pv(*this, threads, tt, rootDepth); main_manager()->pv(*this, threads, tt, rootDepth);
// In case of failing low/high increase aspiration window and // In case of failing low/high increase aspiration window and
@ -378,7 +378,7 @@ void Search::Worker::iterative_deepening() {
std::stable_sort(rootMoves.begin() + pvFirst, rootMoves.begin() + pvIdx + 1); std::stable_sort(rootMoves.begin() + pvFirst, rootMoves.begin() + pvIdx + 1);
if (mainThread if (mainThread
&& (threads.stop || pvIdx + 1 == multiPV || elapsed() > 3000) && (threads.stop || pvIdx + 1 == multiPV || elapsed_time() > 3000)
// A thread that aborted search can have mated-in/TB-loss PV and score // 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 // that cannot be trusted, i.e. it can be delayed or refuted if we would have
// had time to fully search other root-moves. Thus we suppress this output and // had time to fully search other root-moves. Thus we suppress this output and
@ -935,7 +935,7 @@ moves_loop: // When in check, search starts here
ss->moveCount = ++moveCount; ss->moveCount = ++moveCount;
if (rootNode && is_mainthread() && elapsed() > 3000) if (rootNode && is_mainthread() && elapsed_time() > 3000)
{ {
main_manager()->updates.onIter( main_manager()->updates.onIter(
{depth, UCIEngine::move(move, pos.is_chess960()), moveCount + thisThread->pvIdx}); {depth, UCIEngine::move(move, pos.is_chess960()), moveCount + thisThread->pvIdx});
@ -1647,10 +1647,20 @@ Depth Search::Worker::reduction(bool i, Depth d, int mn, int delta) {
return (reductionScale + 1318 - delta * 760 / rootDelta) / 1024 + (!i && reductionScale > 1066); return (reductionScale + 1318 - delta * 760 / rootDelta) / 1024 + (!i && reductionScale > 1066);
} }
// elapsed() returns the time elapsed since the search started. If the
// 'nodestime' option is enabled, it will return the count of nodes searched
// instead. This function is called to check whether the search should be
// stopped based on predefined thresholds like time limits or nodes searched.
//
// elapsed_time() returns the actual time elapsed since the start of the search.
// 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 { TimePoint Search::Worker::elapsed() const {
return main_manager()->tm.elapsed([this]() { return threads.nodes_searched(); }); return main_manager()->tm.elapsed([this]() { return threads.nodes_searched(); });
} }
TimePoint Search::Worker::elapsed_time() const { return main_manager()->tm.elapsed_time(); }
namespace { namespace {
// Adjusts a mate or TB score from "plies to mate from the root" // Adjusts a mate or TB score from "plies to mate from the root"
@ -1900,7 +1910,7 @@ void SearchManager::pv(const Search::Worker& worker,
const auto& rootMoves = worker.rootMoves; const auto& rootMoves = worker.rootMoves;
const auto& pos = worker.rootPos; const auto& pos = worker.rootPos;
size_t pvIdx = worker.pvIdx; size_t pvIdx = worker.pvIdx;
TimePoint time = tm.elapsed([nodes]() { return nodes; }) + 1; TimePoint time = tm.elapsed_time() + 1;
size_t multiPV = std::min(size_t(worker.options["MultiPV"]), rootMoves.size()); size_t multiPV = std::min(size_t(worker.options["MultiPV"]), rootMoves.size());
uint64_t tbHits = threads.tb_hits() + (worker.tbConfig.rootInTB ? rootMoves.size() : 0); uint64_t tbHits = threads.tb_hits() + (worker.tbConfig.rootInTB ? rootMoves.size() : 0);

View file

@ -276,6 +276,7 @@ class Worker {
} }
TimePoint elapsed() const; TimePoint elapsed() const;
TimePoint elapsed_time() const;
LimitsType limits; LimitsType limits;

View file

@ -32,12 +32,12 @@ TimePoint TimeManagement::optimum() const { return optimumTime; }
TimePoint TimeManagement::maximum() const { return maximumTime; } TimePoint TimeManagement::maximum() const { return maximumTime; }
void TimeManagement::clear() { void TimeManagement::clear() {
availableNodes = 0; // When in 'nodes as time' mode availableNodes = -1; // When in 'nodes as time' mode
} }
void TimeManagement::advance_nodes_time(std::int64_t nodes) { void TimeManagement::advance_nodes_time(std::int64_t nodes) {
assert(useNodesTime); assert(useNodesTime);
availableNodes += nodes; availableNodes = std::max(int64_t(0), availableNodes - nodes);
} }
// Called at the beginning of the search and calculates // Called at the beginning of the search and calculates
@ -48,14 +48,17 @@ void TimeManagement::init(Search::LimitsType& limits,
Color us, Color us,
int ply, int ply,
const OptionsMap& options) { const OptionsMap& options) {
// If we have no time, no need to initialize TM, except for the start time, TimePoint npmsec = TimePoint(options["nodestime"]);
// which is used by movetime.
startTime = limits.startTime; // If we have no time, we don't need to fully initialize TM.
// startTime is used by movetime and useNodesTime is used in elapsed calls.
startTime = limits.startTime;
useNodesTime = npmsec != 0;
if (limits.time[us] == 0) if (limits.time[us] == 0)
return; return;
TimePoint moveOverhead = TimePoint(options["Move Overhead"]); TimePoint moveOverhead = TimePoint(options["Move Overhead"]);
TimePoint npmsec = TimePoint(options["nodestime"]);
// optScale is a percentage of available time to use for the current move. // optScale is a percentage of available time to use for the current move.
// maxScale is a multiplier applied to optimumTime. // maxScale is a multiplier applied to optimumTime.
@ -65,26 +68,31 @@ void TimeManagement::init(Search::LimitsType& limits,
// to nodes, and use resulting values in time management formulas. // to nodes, and use resulting values in time management formulas.
// WARNING: to avoid time losses, the given npmsec (nodes per millisecond) // WARNING: to avoid time losses, the given npmsec (nodes per millisecond)
// must be much lower than the real engine speed. // must be much lower than the real engine speed.
if (npmsec) if (useNodesTime)
{ {
useNodesTime = true; if (availableNodes == -1) // Only once at game start
if (!availableNodes) // Only once at game start
availableNodes = npmsec * limits.time[us]; // Time is in msec availableNodes = npmsec * limits.time[us]; // Time is in msec
// Convert from milliseconds to nodes // Convert from milliseconds to nodes
limits.time[us] = TimePoint(availableNodes); limits.time[us] = TimePoint(availableNodes);
limits.inc[us] *= npmsec; limits.inc[us] *= npmsec;
limits.npmsec = npmsec; limits.npmsec = npmsec;
moveOverhead *= npmsec;
} }
// These numbers are used where multiplications, divisions or comparisons
// with constants are involved.
const int64_t scaleFactor = useNodesTime ? npmsec : 1;
const TimePoint scaledTime = limits.time[us] / scaleFactor;
const TimePoint scaledInc = limits.inc[us] / scaleFactor;
// Maximum move horizon of 50 moves // Maximum move horizon of 50 moves
int mtg = limits.movestogo ? std::min(limits.movestogo, 50) : 50; int mtg = limits.movestogo ? std::min(limits.movestogo, 50) : 50;
// if less than one second, gradually reduce mtg // If less than one second, gradually reduce mtg
if (limits.time[us] < 1000 && (double(mtg) / limits.time[us] > 0.05)) if (scaledTime < 1000 && double(mtg) / scaledInc > 0.05)
{ {
mtg = limits.time[us] * 0.05; mtg = scaledTime * 0.05;
} }
// Make sure timeLeft is > 0 since we may use it as a divisor // Make sure timeLeft is > 0 since we may use it as a divisor
@ -97,15 +105,15 @@ void TimeManagement::init(Search::LimitsType& limits,
if (limits.movestogo == 0) if (limits.movestogo == 0)
{ {
// Use extra time with larger increments // Use extra time with larger increments
double optExtra = limits.inc[us] < 500 ? 1.0 : 1.13; double optExtra = scaledInc < 500 ? 1.0 : 1.13;
// Calculate time constants based on current time left. // Calculate time constants based on current time left.
double optConstant = double logTimeInSec = std::log10(scaledTime / 1000.0);
std::min(0.00308 + 0.000319 * std::log10(limits.time[us] / 1000.0), 0.00506); double optConstant = std::min(0.00308 + 0.000319 * logTimeInSec, 0.00506);
double maxConstant = std::max(3.39 + 3.01 * std::log10(limits.time[us] / 1000.0), 2.93); double maxConstant = std::max(3.39 + 3.01 * logTimeInSec, 2.93);
optScale = std::min(0.0122 + std::pow(ply + 2.95, 0.462) * optConstant, optScale = std::min(0.0122 + std::pow(ply + 2.95, 0.462) * optConstant,
0.213 * limits.time[us] / double(timeLeft)) 0.213 * limits.time[us] / timeLeft)
* optExtra; * optExtra;
maxScale = std::min(6.64, maxConstant + ply / 12.0); maxScale = std::min(6.64, maxConstant + ply / 12.0);
} }
@ -113,7 +121,7 @@ void TimeManagement::init(Search::LimitsType& limits,
// x moves in y seconds (+ z increment) // x moves in y seconds (+ z increment)
else else
{ {
optScale = std::min((0.88 + ply / 116.4) / mtg, 0.88 * limits.time[us] / double(timeLeft)); optScale = std::min((0.88 + ply / 116.4) / mtg, 0.88 * limits.time[us] / timeLeft);
maxScale = std::min(6.3, 1.5 + 0.11 * mtg); maxScale = std::min(6.3, 1.5 + 0.11 * mtg);
} }

View file

@ -42,8 +42,9 @@ class TimeManagement {
TimePoint maximum() const; TimePoint maximum() const;
template<typename FUNC> template<typename FUNC>
TimePoint elapsed(FUNC nodes) const { TimePoint elapsed(FUNC nodes) const {
return useNodesTime ? TimePoint(nodes()) : now() - startTime; return useNodesTime ? TimePoint(nodes()) : elapsed_time();
} }
TimePoint elapsed_time() const { return now() - startTime; };
void clear(); void clear();
void advance_nodes_time(std::int64_t nodes); void advance_nodes_time(std::int64_t nodes);
@ -53,7 +54,7 @@ class TimeManagement {
TimePoint optimumTime; TimePoint optimumTime;
TimePoint maximumTime; TimePoint maximumTime;
std::int64_t availableNodes = 0; // When in 'nodes as time' mode std::int64_t availableNodes = -1; // When in 'nodes as time' mode
bool useNodesTime = false; // True if we are in 'nodes as time' mode bool useNodesTime = false; // True if we are in 'nodes as time' mode
}; };