From 50e8ff1e23ad0f9c9f15f06cb5650b277129b744 Mon Sep 17 00:00:00 2001 From: Robert Nurnberg <28635489+robertnurnberg@users.noreply.github.com> Date: Sun, 26 Apr 2026 09:24:28 +0200 Subject: [PATCH] multiPV: protect mated-ins and allow thread selection This PR is a replacement for #6652 and takes into account the changes made to this part of the code since then. In multiPV analysis master may suppress genuine proven mated-in scores, if the iteration for a secondary PV line is aborted. In very rare cases, it may also evict a proven mated-in score from the best PV line with a score from an incomplete iteration of a secondary PV line. This PR avoids both. In addition, we allow best thread selection in multithreaded multiPV searches. This was originally disabled in #524. Single PV searches are not affected by this patch at all. Fixes #6642. closes https://github.com/official-stockfish/Stockfish/pull/6728 No functional change. --- src/search.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 28fa48b65..50aa07708 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -235,7 +235,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 && !skill.enabled()) + if (!limits.depth && !skill.enabled()) bestThread = threads.get_best_thread()->worker.get(); main_manager()->bestPreviousScore = bestThread->rootMoves[0].score; @@ -426,6 +426,24 @@ bool Search::Worker::iterative_deepening() { assert(alpha >= -VALUE_INFINITE && beta <= VALUE_INFINITE); } + // In multiPV analysis we do not let aborted searches spoil mated-in/ + // TB loss scores from a completed search in an earlier PV line. + // A mated-in/TB loss from an aborted search for pvIdx > 0 can only become + // bestmove in the sorting below, if the current bestmove (and hence also + // the previously searched pvIdx - 1 line) is already a proven loss. + if (threads.stop && pvIdx && is_loss(rootMoves[pvIdx - 1].score) + && rootMoves[pvIdx] < rootMoves[pvIdx - 1]) + { + rootMoves[pvIdx].score = rootMoves[pvIdx].uciScore = + (rootMoves[pvIdx].previousScore != -VALUE_INFINITE + && rootMoves[pvIdx].previousScore < rootMoves[pvIdx - 1].score) + ? rootMoves[pvIdx].previousScore + : rootMoves[pvIdx - 1].score; + rootMoves[pvIdx].previousScore = -VALUE_INFINITE; + rootMoves[pvIdx].scoreLowerbound = rootMoves[pvIdx].scoreUpperbound = false; + rootMoves[pvIdx].pv.resize(1); + } + // Sort the PV lines searched so far and update the GUI std::stable_sort(rootMoves.begin() + pvFirst, rootMoves.begin() + pvIdx + 1); @@ -452,7 +470,7 @@ bool Search::Worker::iterative_deepening() { // A mated-in/TB-loss score from an aborted search cannot be trusted: the loss // could be delayed or refuted upon exploring the remaining root-moves. // Thus here we roll back to the score from the previous iteration. - else if (rootMoves[0].score != -VALUE_INFINITE && is_loss(rootMoves[0].score)) + else if (!pvIdx && rootMoves[0].score != -VALUE_INFINITE && is_loss(rootMoves[0].score)) { // Bring the last best move to the front for best thread selection. if (!lastIterationPV.empty())