Time Management¶
A fixed-depth search is a benchmark, not a game. Against a clock the engine must decide, before each move, how long it can afford to think — and the quality of that decision is worth real Elo independently of how well the search itself plays.
The three ways to bound a search¶
UCI form |
Bound |
Use |
|---|---|---|
|
plies |
debugging; not comparable across builds |
|
nodes visited |
reproducible A/B testing |
|
wall-clock ms |
fixed thinking time per move |
|
a real clock |
managed — the engine budgets |
Only the last form exercises the code in this chapter. That has a direct consequence for testing which Ch. Measuring Strength returns to: a change to time management cannot be validated with --nodes, because the node-budget path never consults the clock.
The budget calculation¶
From kirin/src/engine/uci.cpp:
int base = (remainingMs * slowMover) / (100 * divisor);
int incrementUse = (incrementMs * slowMover) / 200;
int budget = base + incrementUse - moveOverheadMs;
int hardCap = std::max(1, (remainingMs - moveOverheadMs) / 8);
if (maxManagedMoveTimeMs > 0) {
hardCap = std::min(hardCap, maxManagedMoveTimeMs);
}
Read plainly: take a fraction of the remaining clock, add half the increment, subtract the communication overhead, then refuse to spend more than an eighth of what is left on any single move. The three tunables:
Option |
Default |
Range |
Meaning |
|---|---|---|---|
|
25 |
0–5000 |
ms reserved for transport and GUI latency |
|
85 |
10–200 |
percentage scaler on the whole budget |
|
0 |
0–60000 |
absolute per-move ceiling; 0 = none |
Move Overhead is insurance against losing on time to latency rather than to the position — it is subtracted from every budget, so it should reflect the worst realistic round-trip, not the average. Slow Mover below 100 makes the engine deliberately thrifty, banking time for later in the game.
The cap that cost 17 Elo¶
D-16 — Max Managed Move Time defaults to 0 (no cap), not 1500 ms
Context Max Managed Move Time was introduced as a safety ceiling and defaulted to 1500 ms. On the physical board that is defensible — a gantry move takes seconds anyway, and a player watching a machine think for thirty seconds is a worse experience than a slightly weaker move. The default was never revisited when the same engine started playing on a clock.
Decision Default the cap to 0, meaning no absolute ceiling, and leave the proportional remaining/8 hard cap as the only limit. Products that want a ceiling set one explicitly.
Rejected Keeping 1500 ms as the default was measured, not argued away: at a 3+2 time control the capped engine banked its clock and lost to the uncapped one by roughly 17 Elo. The engine was reaching the ceiling in the middlegame — exactly where extra depth is worth the most — and then carrying the unspent time into an endgame it had already compromised. Lowering the cap instead of removing it was rejected for the same reason: any fixed millisecond ceiling is wrong at some time control, whereas the proportional cap scales.
Consequences The engine will now spend up to an eighth of its remaining clock on a single move, which is correct for competitive play and can look alarming on a physical board. The appliance is free to set Max Managed Move Time for its own use — the option did not go away, only its default. This is also the canonical example of why time-management changes must be validated at --tc and never at --nodes: at a fixed node budget this regression is completely invisible, because the cap is never consulted.
Checking the clock during search¶
The search polls rather than being interrupted. In search.cpp, when timeSet is active, the remaining time against stopTime is checked periodically and the global stopped flag is raised when it runs out.
Two properties follow, and both matter:
Polling happens on a node interval, not every node, so the engine can overshoot its budget slightly.
Move Overheadabsorbs this.Because the flag is the same
stoppedglobal used by the UCIstopcommand and byGameSession’s cancellation path, an aborted search must always still yield a legal move. An iteration that is abandoned mid-way keeps the best move from the last completed iteration rather than a partial result.
Warning
The second property is a standing invariant, not an implementation detail. The touch GUI’s Stop and Restart controls set the same flag, so “search interrupted” is a routine event on the appliance rather than an edge case, and any change to iterative deepening has to preserve it.