From aeaf01d196032748a83008d4215492ec36786506 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 01:46:44 +0000 Subject: [PATCH 1/2] Fix visit-cap tests for terminal leaves Validate terminal positions by replay before accepting absent snapshots. Preserve nonterminal snapshot checks and add a deterministic pass/pass regression. Port the isolated test-only fix from the fork without search or near-end changes. --- cpp/tests/testsearchnonn.cpp | 69 ++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/cpp/tests/testsearchnonn.cpp b/cpp/tests/testsearchnonn.cpp index cf71ec76a..e43c1db7a 100644 --- a/cpp/tests/testsearchnonn.cpp +++ b/cpp/tests/testsearchnonn.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "../core/fileutils.h" #include "../dataio/sgf.h" @@ -2638,14 +2639,25 @@ x.x.x ......... )%%"); - //Check that exactly the capped nodes that reached the cap have a snapshot, and that their child weight distribution - //matches it. Relies on this test net giving every visit the same weight. Multithreading and virtual losses can - //make the distribution overshoot, so the tolerance is looser then. + //Check that capped nonterminal nodes that reached the cap have a snapshot, and that their child weight distribution + //matches it. Terminal leaves have no children to freeze. Relies on this test net giving every visit the same weight. + //Multithreading and virtual losses can make the distribution overshoot, so the tolerance is looser then. //Returns the number of snapshot nodes well past the cap. auto checkTree = [&](Search* search, Player cappedPla, int64_t visitCap, int numThreads) { testAssert(search->getVisitCap(P_BLACK) == (cappedPla == P_BLACK ? visitCap : 0)); testAssert(search->getVisitCap(P_WHITE) == (cappedPla == P_WHITE ? visitCap : 0)); + testAssert(!search->searchParams.useGraphSearch); vector nodes = search->enumerateTreePostOrder(); + std::unordered_map> parents; + for(const SearchNode* parent: nodes) { + ConstSearchNodeChildrenReference children = parent->getChildren(); + for(int i = 0; i < children.getCapacity(); i++) { + const SearchNode* child = children[i].getIfAllocated(); + if(child == NULL) + break; + testAssert(parents.emplace(child, std::make_pair(parent, children[i].getMoveLoc())).second); + } + } int numNodesWellPastCap = 0; for(SearchNode* node: nodes) { int64_t visits = node->stats.visits.load(std::memory_order_acquire); @@ -2654,6 +2666,32 @@ x.x.x testAssert(snapshot == NULL); continue; } + if(node->getNNOutput() == NULL) { + //Terminal playouts add their outcome directly, without NN evaluation or + //child-weight recomputation. Replay the path to require an actual terminal + //position, so a missing snapshot on a nonterminal node cannot be excused. + vector path; + for(const SearchNode* current = node; current != search->rootNode;) { + auto parent = parents.find(current); + testAssert(parent != parents.end()); + path.push_back(parent->second.second); + current = parent->second.first; + } + Board replayBoard = search->rootBoard; + BoardHistory replayHistory = search->rootHistory; + Player replayPla = search->rootPla; + for(auto move = path.rbegin(); move != path.rend(); ++move) { + replayHistory.makeBoardMoveAssumeLegal(replayBoard, *move, replayPla, NULL); + replayPla = getOpp(replayPla); + } + testAssert(replayHistory.isGameFinished); + testAssert(replayPla == node->nextPla); + testAssert(!node->forceNonTerminal); + testAssert(node->state.load(std::memory_order_acquire) == SearchNode::STATE_UNEVALUATED); + testAssert(node->getChildren().getCapacity() == 0); + testAssert(snapshot == NULL); + continue; + } testAssert(snapshot != NULL); if(visits >= 3 * visitCap) numNodesWellPastCap++; @@ -2697,6 +2735,31 @@ x.x.x return numNodesWellPastCap; }; + { + SearchParams params; + params.maxVisits = 80; + params.visitCapContempt = 20; + params.visitCapContemptPla = P_BLACK; + Search search(params, nnEval, &logger, "visitCapTerminalSeed"); + BoardHistory hist(board, P_BLACK, rules, 0, BoardHistoryModes(false,false)); + search.setPosition(P_BLACK, board, hist); + //Only pass is allowed for both plies, so the capped terminal child must + //accumulate well past the cap regardless of NN preferences or scheduling. + vector avoid(Board::MAX_ARR_SIZE, 2); + avoid[Board::PASS_LOC] = 0; + search.setAvoidMoveUntilByLoc(avoid, avoid); + search.runWholeSearch(P_BLACK); + const SearchNode* afterPass = search.getChildForMove(search.rootNode, Board::PASS_LOC); + testAssert(afterPass != NULL); + const SearchNode* terminal = search.getChildForMove(afterPass, Board::PASS_LOC); + testAssert(terminal != NULL); + testAssert(terminal->stats.visits.load(std::memory_order_acquire) >= 3 * params.visitCapContempt); + testAssert(terminal->getNNOutput() == NULL); + testAssert(terminal->visitCapSnapshot.load(std::memory_order_acquire) == NULL); + checkTree(&search, P_BLACK, params.visitCapContempt, params.numThreads); + cout << "Terminal leaves past the cap have no child distribution to freeze: 1" << endl; + } + auto runTest = [&](int numThreads) { cout << "Threads: " << numThreads << endl; SearchParams params; From 0750698f6a12ee6e2b5f9adc5ce30a9cf33f4833 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 01:57:36 +0000 Subject: [PATCH 2/2] Preserve the visit-cap output snapshot Keep the deterministic terminal regression silent; all assertions remain active without adding an unrecorded line to runoutputtests.sh output. --- cpp/tests/testsearchnonn.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/tests/testsearchnonn.cpp b/cpp/tests/testsearchnonn.cpp index e43c1db7a..2468e219f 100644 --- a/cpp/tests/testsearchnonn.cpp +++ b/cpp/tests/testsearchnonn.cpp @@ -2757,7 +2757,6 @@ x.x.x testAssert(terminal->getNNOutput() == NULL); testAssert(terminal->visitCapSnapshot.load(std::memory_order_acquire) == NULL); checkTree(&search, P_BLACK, params.visitCapContempt, params.numThreads); - cout << "Terminal leaves past the cap have no child distribution to freeze: 1" << endl; } auto runTest = [&](int numThreads) {