From 67a2c247d45034509fbdd669751f3dabe11e797d Mon Sep 17 00:00:00 2001 From: "Robert Nurnberg @ elitebook" Date: Wed, 18 Mar 2026 20:43:41 +0100 Subject: [PATCH] Improve multi-threaded go-mate searches Ever since #5094 master only lets the main thread terminate `go mate` searches, reverting the earlier improvement #1215. This PR restores the old logic. So any thread that found a "mate in x" can now stop the search. To make this work robustly, we need to guard against inexact mate scores in the best thread selection. In addition, in contrast to time limits, the main thread may now not complete a d1 search for a mated-in position. In master an aborted d1 search may have a PV beginning with `Move::none()`, in which case no thread selection is performed. See #623. We fix this bug here by checking if `lastBestPV` is empty or not. For interrupted d1 searches we now label mated-in scores as inexact. While at it, we also simplify the logic for detecting if we can terminate a go mate x search, using the fact that threads.stop can only be false if we have a completed iteration with a valid score. The PR has no effect on game play, but should slightly improve general mate finding and speed up multi-threaded `go mate` searches. We also add a corresponding matecheck run to the CI. This only involves 61 mates up to mate-in-2. Test runs with the first 50 or 100 mates from `mates2000.epd` did at times not finish within 30 minutes on my fork or in local tests, possibly due to search explosions for some mate-in-3 positions. closes https://github.com/official-stockfish/Stockfish/pull/6668 No functional change --- .github/workflows/matetrack.yml | 27 +++++++++++++++----- src/search.cpp | 44 +++++++++++++++++++++------------ src/thread.cpp | 16 ++++++++---- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/.github/workflows/matetrack.yml b/.github/workflows/matetrack.yml index cef560fc1..fcaa0b063 100644 --- a/.github/workflows/matetrack.yml +++ b/.github/workflows/matetrack.yml @@ -50,20 +50,35 @@ jobs: - name: Run matetrack th1 working-directory: matetrack run: | - python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 | tee matecheckout1.out - ! grep "issues were detected" matecheckout1.out > /dev/null + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 | tee matecheck1.out + ! grep "issues were detected" matecheck1.out > /dev/null - name: Run matetrack th4 working-directory: matetrack run: | - python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 --threads 4 | tee matecheckout4.out - ! grep "issues were detected" matecheckout4.out > /dev/null + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 --threads 4 | tee matecheck4.out + ! grep "issues were detected" matecheck4.out > /dev/null - name: Run matetrack th4 gameplay working-directory: matetrack run: | - python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --time 3 --timeinc 0.01 --threads 4 | tee matecheckout4g.out - ! grep "issues were detected" matecheckout4g.out > /dev/null + python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --time 3 --timeinc 0.01 --threads 4 | tee matecheck4g.out + ! grep "issues were detected" matecheck4g.out > /dev/null + + - name: Run matetrack th4 go-mate + working-directory: matetrack + run: | + head -n 21 matetrack.epd > gomates.epd + head -n 44 matedtrack.epd >> gomates.epd + head -n 18 mates2000.epd >> gomates.epd + python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile gomates.epd --mate 0 --threads 4 | tee matecheck4gm.out + ! grep "issues were detected" matecheck4gm.out > /dev/null + total=$(grep "Total FENs:" matecheck4gm.out | awk '{print $3}') + bmates=$(grep "Best mates:" matecheck4gm.out | awk '{print $3}') + if [ $bmates -ne $total ]; then + echo "At least one go-mate search did not yield expected mate, see matecheck4gm.out" >&2 + exit 1 + fi - name: Run matetrack th1 with --syzygy50MoveRule false working-directory: matetrack diff --git a/src/search.cpp b/src/search.cpp index 34ebb2611..5f37e8704 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -232,7 +232,7 @@ void Search::Worker::start_searching() { Skill skill = Skill(options["Skill Level"], options["UCI_LimitStrength"] ? int(options["UCI_Elo"]) : 0); - if (int(options["MultiPV"]) == 1 && !limits.depth && !limits.mate && !skill.enabled() + if (int(options["MultiPV"]) == 1 && !limits.depth && !skill.enabled() && rootMoves[0].pv[0] != Move::none()) bestThread = threads.get_best_thread()->worker.get(); @@ -262,9 +262,9 @@ void Search::Worker::iterative_deepening() { Move pv[MAX_PLY + 1]; - Depth lastBestMoveDepth = 0; - Value lastBestScore = -VALUE_INFINITE; - auto lastBestPV = std::vector{Move::none()}; + Depth lastBestMoveDepth = 0; + Value lastBestScore = -VALUE_INFINITE; + std::vector lastBestPV; Value alpha, beta; Value bestValue = -VALUE_INFINITE; @@ -450,30 +450,42 @@ void Search::Worker::iterative_deepening() { && is_loss(rootMoves[0].score)) { // Bring the last best move to the front for best thread selection. - Utility::move_to_front(rootMoves, [&lastBestPV = std::as_const(lastBestPV)]( - const auto& rm) { return rm == lastBestPV[0]; }); - rootMoves[0].pv = lastBestPV; - rootMoves[0].score = rootMoves[0].uciScore = lastBestScore; + // For an aborted d1 search we label the loss score as inexact. + if (!lastBestPV.empty()) + { + Utility::move_to_front(rootMoves, + [&lastBestPV = std::as_const(lastBestPV)](const auto& rm) { + return rm == lastBestPV[0]; + }); + rootMoves[0].pv = lastBestPV; + rootMoves[0].score = rootMoves[0].uciScore = lastBestScore; + } + else + { + if (!rootMoves[0].scoreLowerbound) + rootMoves[0].scoreUpperbound = true; + if (mainThread) + main_manager()->pv(*this, threads, tt, rootDepth); + } } - else if (rootMoves[0].pv[0] != lastBestPV[0]) + else if (lastBestPV.empty() || rootMoves[0].pv[0] != lastBestPV[0]) { lastBestPV = rootMoves[0].pv; lastBestScore = rootMoves[0].score; lastBestMoveDepth = rootDepth; } - if (!mainThread) - continue; - - // Have we found a "mate in x"? - if (limits.mate && rootMoves[0].score == rootMoves[0].uciScore + // Have we found a "mate in x" after a completed iteration? + if (limits.mate && !threads.stop && ((rootMoves[0].score >= VALUE_MATE_IN_MAX_PLY && VALUE_MATE - rootMoves[0].score <= 2 * limits.mate) - || (rootMoves[0].score != -VALUE_INFINITE - && rootMoves[0].score <= VALUE_MATED_IN_MAX_PLY + || (rootMoves[0].score <= VALUE_MATED_IN_MAX_PLY && VALUE_MATE + rootMoves[0].score <= 2 * limits.mate))) threads.stop = true; + if (!mainThread) + continue; + // If the skill level is enabled and time is up, pick a sub-optimal best move if (skill.enabled() && skill.time_to_pick(rootDepth)) skill.pick_best(rootMoves, multiPV); diff --git a/src/thread.cpp b/src/thread.cpp index a2f59d5b1..4260e66cd 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -364,6 +364,10 @@ Thread* ThreadPool::get_best_thread() const { for (auto&& th : threads) votes[th->worker->rootMoves[0].pv[0]] += thread_voting_value(th.get()); + auto has_bound = [](const Thread* th) { + return th->worker->rootMoves[0].scoreLowerbound || th->worker->rootMoves[0].scoreUpperbound; + }; + for (auto&& th : threads) { const auto bestThreadScore = bestThread->worker->rootMoves[0].score; @@ -375,13 +379,15 @@ Thread* ThreadPool::get_best_thread() const { const auto bestThreadMoveVote = votes[bestThreadPV[0]]; const auto newThreadMoveVote = votes[newThreadPV[0]]; - const bool bestThreadInProvenWin = is_win(bestThreadScore); - const bool newThreadInProvenWin = is_win(newThreadScore); + // Aborted searches may lead to inexact win scores. + const bool bestThreadInProvenWin = is_win(bestThreadScore) && !has_bound(bestThread); + const bool newThreadInProvenWin = is_win(newThreadScore) && !has_bound(th.get()); + // Loss scores may be inexact only for aborted d1 searches. const bool bestThreadInProvenLoss = - bestThreadScore != -VALUE_INFINITE && is_loss(bestThreadScore); + bestThreadScore != -VALUE_INFINITE && is_loss(bestThreadScore) && !has_bound(bestThread); const bool newThreadInProvenLoss = - newThreadScore != -VALUE_INFINITE && is_loss(newThreadScore); + newThreadScore != -VALUE_INFINITE && is_loss(newThreadScore) && !has_bound(th.get()); // We make sure not to pick a thread with truncated principal variation const bool betterVotingValue = @@ -391,7 +397,7 @@ Thread* ThreadPool::get_best_thread() const { if (bestThreadInProvenWin) { // Make sure we pick the shortest mate / TB conversion - if (newThreadScore > bestThreadScore) + if (newThreadInProvenWin && newThreadScore > bestThreadScore) bestThread = th.get(); } else if (bestThreadInProvenLoss)