Skip to content

Commit

Permalink
Fix nodestime
Browse files Browse the repository at this point in the history
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.

No functional changes
  • Loading branch information
MinetaS authored and PikaCat-OuO committed May 9, 2024
1 parent 2c04408 commit 85f77f5
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 27 deletions.
22 changes: 16 additions & 6 deletions src/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ 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(limits.inc[rootPos.side_to_move()]
- threads.nodes_searched());
main_manager()->tm.advance_nodes_time(threads.nodes_searched()
- limits.inc[rootPos.side_to_move()]);

Worker* bestThread = this;

Expand Down Expand Up @@ -303,7 +303,7 @@ void Search::Worker::iterative_deepening() {
// When failing high/low give some update (without cluttering
// the UI) before a re-search.
if (mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta)
&& elapsed() > 3000)
&& elapsed_time() > 3000)
main_manager()->pv(*this, threads, tt, rootDepth);

// In case of failing low/high increase aspiration window and
Expand Down Expand Up @@ -334,7 +334,7 @@ void Search::Worker::iterative_deepening() {
std::stable_sort(rootMoves.begin() + pvFirst, rootMoves.begin() + pvIdx + 1);

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
// 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
Expand Down Expand Up @@ -830,7 +830,7 @@ Value Search::Worker::search(

ss->moveCount = ++moveCount;

if (rootNode && is_mainthread() && elapsed() > 3000)
if (rootNode && is_mainthread() && elapsed_time() > 3000)
{
main_manager()->updates.onIter(
{depth, UCIEngine::move(move), moveCount + thisThread->pvIdx});
Expand Down Expand Up @@ -1547,10 +1547,20 @@ Depth Search::Worker::reduction(bool i, Depth d, int mn, int delta) {
return (reductionScale + 1883 - delta * 1567 / rootDelta) / 1318 + (!i && reductionScale > 903);
}

// 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 {
return main_manager()->tm.elapsed([this]() { return threads.nodes_searched(); });
}

TimePoint Search::Worker::elapsed_time() const { return main_manager()->tm.elapsed_time(); }


namespace {
// Adjusts a mate from "plies to mate from the root" to
Expand Down Expand Up @@ -1751,7 +1761,7 @@ void SearchManager::pv(const Search::Worker& worker,
const auto& rootMoves = worker.rootMoves;
const auto& pos = worker.rootPos;
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());

for (size_t i = 0; i < multiPV; ++i)
Expand Down
1 change: 1 addition & 0 deletions src/search.h
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ class Worker {
}

TimePoint elapsed() const;
TimePoint elapsed_time() const;

LimitsType limits;

Expand Down
46 changes: 27 additions & 19 deletions src/timeman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ TimePoint TimeManagement::optimum() const { return optimumTime; }
TimePoint TimeManagement::maximum() const { return maximumTime; }

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) {
assert(useNodesTime);
availableNodes += nodes;
availableNodes = std::max(int64_t(0), availableNodes - nodes);
}

// Called at the beginning of the search and calculates
Expand All @@ -49,14 +49,17 @@ void TimeManagement::init(Search::LimitsType& limits,
Color us,
int ply,
const OptionsMap& options) {
// If we have no time, no need to initialize TM, except for the start time,
// which is used by movetime.
startTime = limits.startTime;
TimePoint npmsec = TimePoint(options["nodestime"]);

// 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)
return;

TimePoint moveOverhead = TimePoint(options["Move Overhead"]);
TimePoint npmsec = TimePoint(options["nodestime"]);

// optScale is a percentage of available time to use for the current move.
// maxScale is a multiplier applied to optimumTime.
Expand All @@ -66,26 +69,31 @@ void TimeManagement::init(Search::LimitsType& limits,
// to nodes, and use resulting values in time management formulas.
// WARNING: to avoid time losses, the given npmsec (nodes per millisecond)
// must be much lower than the real engine speed.
if (npmsec)
if (useNodesTime)
{
useNodesTime = true;

if (!availableNodes) // Only once at game start
if (availableNodes == -1) // Only once at game start
availableNodes = npmsec * limits.time[us]; // Time is in msec

// Convert from milliseconds to nodes
limits.time[us] = TimePoint(availableNodes);
limits.inc[us] *= 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 60 moves
int mtg = limits.movestogo ? std::min(limits.movestogo, 60) : 60;

// if less than one second, gradually reduce mtg
if (limits.time[us] < 1000 && (double(mtg) / limits.time[us] > 0.05))
// If less than one second, gradually reduce mtg
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
Expand All @@ -98,23 +106,23 @@ void TimeManagement::init(Search::LimitsType& limits,
if (limits.movestogo == 0)
{
// Use extra time with larger increments
double optExtra = limits.inc[us] < 500 ? 1.0 : 1.10;
double optExtra = scaledInc < 500 ? 1.0 : 1.10;

// Calculate time constants based on current time left.
double optConstant =
std::min(0.00344 + 0.0002 * std::log10(limits.time[us] / 1000.0), 0.0045);
double maxConstant = std::max(3.9 + 3.1 * std::log10(limits.time[us] / 1000.0), 2.5);
double logTimeInSec = std::log10(scaledTime / 1000.0);
double optConstant = std::min(0.00344 + 0.000200 * logTimeInSec, 0.00450);
double maxConstant = std::max(3.90 + 3.10 * logTimeInSec, 2.50);

optScale = std::min(0.0155 + std::pow(ply + 3.0, 0.45) * optConstant,
0.2 * limits.time[us] / double(timeLeft))
0.2 * limits.time[us] / timeLeft)
* optExtra;
maxScale = std::min(6.5, maxConstant + ply / 13.6);
}

// x moves in y seconds (+ z increment)
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);
}

Expand Down
5 changes: 3 additions & 2 deletions src/timeman.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ class TimeManagement {
TimePoint maximum() const;
template<typename FUNC>
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 advance_nodes_time(std::int64_t nodes);
Expand All @@ -53,7 +54,7 @@ class TimeManagement {
TimePoint optimumTime;
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
};

Expand Down

0 comments on commit 85f77f5

Please sign in to comment.