diff --git a/.github/workflows/matetrack.yml b/.github/workflows/matetrack.yml index f6eaead5f..f0ff643eb 100644 --- a/.github/workflows/matetrack.yml +++ b/.github/workflows/matetrack.yml @@ -24,7 +24,7 @@ jobs: with: repository: vondele/matetrack path: matetrack - ref: b2c8c9599cd7173f537b28bba88f31690a6d6f62 + ref: 9d77be284e8cba37e2f8a59e54cd2bcbf4e3ee4d persist-credentials: false - name: matetrack install deps @@ -68,10 +68,7 @@ jobs: - 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 + python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile matetrack.epd matedtrack.epd --bmMax 2 --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}') @@ -83,13 +80,13 @@ jobs: - name: Run matetrack th1 multiPV working-directory: matetrack run: | - python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 --multiPV 4 | tee matecheck1mpv.out + python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile matetrack.epd matedtrack.epd --bmMax 10 --nodes 5000 --multiPV 20 --checkMultiPVs | tee matecheck1mpv.out ! grep "issues were detected" matecheck1mpv.out > /dev/null - name: Run matetrack th4 multiPV working-directory: matetrack run: | - python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 --threads 4 --multiPV 4 | tee matecheck4mpv.out + python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile matetrack.epd matedtrack.epd --bmMax 10 --nodes 5000 --threads 4 --multiPV 20 --checkMultiPVs | tee matecheck4mpv.out ! grep "issues were detected" matecheck4mpv.out > /dev/null - name: Run matetrack th1 with --syzygy50MoveRule false diff --git a/src/search.cpp b/src/search.cpp index bd3608f11..aa48c21d3 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -187,7 +187,6 @@ void Search::Worker::ensure_network_replicated() { void Search::Worker::start_searching() { accumulatorStack.reset(); - lastIterationPV.clear(); // Non-main threads go directly to iterative_deepening() if (!is_mainthread()) @@ -269,6 +268,7 @@ bool Search::Worker::iterative_deepening() { PVMoves pv; + Move lastBestMove = Move::none(); Depth lastBestMoveDepth = 0; Value alpha, beta; @@ -340,7 +340,10 @@ bool Search::Worker::iterative_deepening() { // Save the last iteration's scores before the first PV line is searched and // all the move scores except the (new) PV are set to -VALUE_INFINITE. for (RootMove& rm : rootMoves) + { rm.previousScore = rm.score; + rm.previousPV = rm.pv; + } usize pvFirst = 0; pvLast = 0; @@ -359,6 +362,8 @@ bool Search::Worker::iterative_deepening() { break; } + lastIterationIdxPV = rootMoves[pvIdx].previousPV; + // Reset UCI info selDepth for each depth and each PV line selDepth = 0; @@ -433,20 +438,42 @@ bool Search::Worker::iterative_deepening() { // 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]) + // Hence we guard against an aborted pvIdx line overtaking pvIdx - 1 + // when pvIdx - 1 is a proven loss. + // Moreover, we do not trust an exact loss score from an aborted search. + if (threads.stop && pvIdx + && ((is_loss(rootMoves[pvIdx - 1].score) && rootMoves[pvIdx] < rootMoves[pvIdx - 1]) + || rootMoves[pvIdx].score_is_exact_loss())) { - 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].unset_bound_flags(); - rootMoves[pvIdx].pv.resize(1); + // If the previous score is worse than pvIdx - 1, we can safely use it. + // If it is equal, we make sure it cannot overtake pvIdx - 1. + if (rootMoves[pvIdx].previousScore != -VALUE_INFINITE + && rootMoves[pvIdx].previousScore <= rootMoves[pvIdx - 1].score) + { + rootMoves[pvIdx].score = rootMoves[pvIdx].uciScore = + rootMoves[pvIdx].previousScore; + rootMoves[pvIdx].previousScore = -VALUE_INFINITE; + rootMoves[pvIdx].pv = rootMoves[pvIdx].previousPV; + rootMoves[pvIdx].unset_bound_flags(); + } + + // Otherwise, if we can, we cap the score to the best possible, and mark + // the score as a bound (also a valid excuse for the incomplete PV.) + else + { + if (is_loss(rootMoves[pvIdx - 1].score)) + { + rootMoves[pvIdx].score = rootMoves[pvIdx].uciScore = + rootMoves[pvIdx - 1].score; + rootMoves[pvIdx].previousScore = -VALUE_INFINITE; + rootMoves[pvIdx].pv.resize(1); + rootMoves[pvIdx].scoreUpperbound = true; + } + else + rootMoves[pvIdx].scoreUpperbound = false; + + rootMoves[pvIdx].scoreLowerbound = !rootMoves[pvIdx].scoreUpperbound; + } } // Sort the PV lines searched so far and update the GUI @@ -469,20 +496,18 @@ bool Search::Worker::iterative_deepening() { if (!threads.stop) { - if (lastIterationPV.empty() || rootMoves[0].pv[0] != lastIterationPV[0]) + if (lastBestMove != rootMoves[0].pv[0]) lastBestMoveDepth = rootDepth; // Do not replace (shorter) mate scores from a previous iteration. if (!forgottenMate) { - lastIterationPV = rootMoves[0].pv; + lastBestMove = rootMoves[0].pv[0]; lastIterationScore = rootMoves[0].score; } } - const bool abortedLossSearch = - threads.stop && !pvIdx && rootMoves[0].score != -VALUE_INFINITE - && is_loss(rootMoves[0].score) && !rootMoves[0].score_is_bound(); + const bool abortedLossSearch = threads.stop && !pvIdx && rootMoves[0].score_is_exact_loss(); // An exact 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. @@ -491,12 +516,13 @@ bool Search::Worker::iterative_deepening() { // in a previous iteration. if (abortedLossSearch || (rootMoves[0].score != -VALUE_INFINITE && forgottenMate)) { - if (!lastIterationPV.empty()) + // Bring the last best move to the front for best thread selection. + if (lastBestMove != Move::none()) { - Utility::move_to_front(rootMoves, [&lastPV = std::as_const(lastIterationPV)]( - const auto& rm) { return rm == lastPV[0]; }); - rootMoves[0].pv = lastIterationPV; - rootMoves[0].score = rootMoves[0].uciScore = lastIterationScore; + Utility::move_to_front( + rootMoves, [lastBestMove](const auto& rm) { return rm == lastBestMove; }); + rootMoves[0].score = rootMoves[0].uciScore = rootMoves[0].previousScore; + rootMoves[0].pv = rootMoves[0].previousPV; rootMoves[0].unset_bound_flags(); if (mainThread) @@ -709,8 +735,9 @@ Value Search::Worker::search( maxValue = VALUE_INFINITE; ss->followPV = rootNode - || ((ss - 1)->followPV && static_cast(ss->ply - 1) < lastIterationPV.size() - && (ss - 1)->currentMove == lastIterationPV[ss->ply - 1]); + || ((ss - 1)->followPV + && (static_cast(ss->ply - 1) < lastIterationIdxPV.size() + && (ss - 1)->currentMove == lastIterationIdxPV[ss->ply - 1])); // Check for the available remaining time if (is_mainthread()) @@ -2189,12 +2216,13 @@ void SearchManager::pv(Search::Worker& worker, v = isTBScore ? rootMoves[i].tbScore : v; // Potentially correct and extend the PV, and in exceptional cases v. - // Bound flags indicate an unreliable PV, also when we usePreviousScore. - if (is_decisive(v) && !is_mate_or_mated(v) && (!rootMoves[i].score_is_bound() || isTBScore)) + // Previous PVs have already been extended. Bound flags indicate an unreliable PV. + if (is_decisive(v) && !is_mate_or_mated(v) && !usePreviousScore + && (!rootMoves[i].score_is_bound() || isTBScore)) syzygy_extend_pv(worker.options, worker.limits, pos, rootMoves[i], v); std::string pv; - for (Move m : rootMoves[i].pv) + for (Move m : usePreviousScore ? rootMoves[i].previousPV : rootMoves[i].pv) pv += UCIEngine::move(m, pos.is_chess960()) + " "; // Remove last whitespace diff --git a/src/search.h b/src/search.h index 050e8e847..05853631d 100644 --- a/src/search.h +++ b/src/search.h @@ -128,6 +128,9 @@ struct RootMove { explicit RootMove(Move m) { pv.push_back(m); } bool extract_ponder_from_tt(const TranspositionTable& tt, Position& pos); bool score_is_bound() const { return scoreLowerbound || scoreUpperbound; } + bool score_is_exact_loss() const { + return score != -VALUE_INFINITE && is_loss(score) && !score_is_bound(); + } void unset_bound_flags() { scoreLowerbound = scoreUpperbound = false; } bool operator==(const Move& m) const { return pv[0] == m; } // Sort in descending order @@ -146,7 +149,7 @@ struct RootMove { int selDepth = 0; int tbRank = 0; Value tbScore; - PVMoves pv; + PVMoves pv, previousPV; }; using RootMoves = std::vector; @@ -384,7 +387,7 @@ class Worker { Depth rootDepth; Value rootDelta; - PVMoves lastIterationPV; + PVMoves lastIterationIdxPV; usize threadIdx, numaThreadIdx, numaTotal; NumaReplicatedAccessToken numaAccessToken;