From 9b7b00b500edd874e42048c6db4ee2a0f199ea49 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 12:38:15 -0500 Subject: [PATCH 01/11] Add conservative internal iterative reduction --- src/search.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/search.cpp b/src/search.cpp index 393c9b7..2062882 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -22,6 +22,8 @@ namespace ironphoenix { constexpr int SEE_PRUNE_MARGIN_PER_DEPTH = 80; constexpr int QSEARCH_SEE_THRESHOLD = -50; + constexpr int IIR_MIN_DEPTH = 5; + IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { const Color partner = static_cast(static_cast(c) ^ 2u); return pos.occupancy(c) | pos.occupancy(partner); @@ -414,6 +416,18 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); + // Internal Iterative Reduction (IIR): when a sufficiently deep + // non-PV node has no TT move to guide ordering, search it one ply + // shallower. The reduction is deliberately conservative because LMR + // can still reduce later quiet moves independently. + if (!pvNode + && ply > 0 + && depth >= IIR_MIN_DEPTH + && !inCheckNode + && !ttMove) { + --depth; + } + MoveList moves; generatePseudoLegalMoves(pos, moves); orderMoves(pos, moves.begin(), moves.end(), ttMove, ply); From db8aaf4f858902a1805ea858003ac7ebce267efd Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 12:43:17 -0500 Subject: [PATCH 02/11] Apply IIR on PV nodes --- src/search.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 2062882..4ac5913 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -416,11 +416,11 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); - // Internal Iterative Reduction (IIR): when a sufficiently deep - // non-PV node has no TT move to guide ordering, search it one ply - // shallower. The reduction is deliberately conservative because LMR - // can still reduce later quiet moves independently. - if (!pvNode + // Internal Iterative Reduction (IIR): when a sufficiently deep PV + // node has no TT move to guide ordering, search it one ply shallower. + // The reduction is deliberately conservative because LMR can still + // reduce later quiet moves independently. + if (pvNode && ply > 0 && depth >= IIR_MIN_DEPTH && !inCheckNode From 76182225c80d60ff8ce146930b021489eca64b7b Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 19:12:50 -0500 Subject: [PATCH 03/11] Allow non-pv nodes --- src/search.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 19915b6..f5eb5f4 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -415,12 +415,7 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); const auto& tuning = Tuning::search(); - // Internal Iterative Reduction (IIR): when a sufficiently deep PV - // node has no TT move to guide ordering, search it one ply shallower. - // The reduction is deliberately conservative because LMR can still - // reduce later quiet moves independently. - if (pvNode - && ply > 0 + if (ply > 0 && depth >= IIR_MIN_DEPTH && !inCheckNode && !ttMove) { From e84518bde10fbc03cc9ceb95ef0e3eb8abbb8755 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 10 Oct 2026 17:51:28 -0500 Subject: [PATCH 04/11] Tune IIR to PV-only at depth 6 --- src/search.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index f5eb5f4..6f77480 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -18,7 +18,7 @@ namespace ironphoenix { constexpr int LMP_HISTORY_THRESHOLD = 2'000; - constexpr int IIR_MIN_DEPTH = 5; + constexpr int IIR_MIN_DEPTH = 6; IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { const Color partner = static_cast(static_cast(c) ^ 2u); @@ -415,7 +415,11 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); const auto& tuning = Tuning::search(); - if (ply > 0 + // Internal Iterative Reduction (IIR): keep this deliberately conservative + // in 4PC. Only reduce deep PV nodes when move ordering has no TT move to + // guide the search. Non-PV nodes retain their full depth. + if (pvNode + && ply > 0 && depth >= IIR_MIN_DEPTH && !inCheckNode && !ttMove) { From 3045b908d565c0fec31af8f076d8dc25d6e8545b Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 22:57:14 -0500 Subject: [PATCH 05/11] Normalize IIR --- src/search.cpp | 86 ++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 369dd4f..a1afaa9 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -515,21 +515,16 @@ namespace ironphoenix { return ttProbe.score; } + const int staticEval = evaluate(pos); + const bool inCheckNode = pos.inCheck(); const auto& tuning = Tuning::search(); - // Internal Iterative Reduction (IIR): keep this deliberately conservative - // in 4PC. Only reduce deep PV nodes when move ordering has no TT move to - // guide the search. Non-PV nodes retain their full depth. - if (pvNode - && ply > 0 - && depth >= IIR_MIN_DEPTH - && !inCheckNode - && !ttMove) { - --depth; - } - const Color us = pos.sideToMove(); + + const Color partner = static_cast(static_cast(us) ^ 2u); + const bool partnerChecked = pos.inCheck(partner); + const bool mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD); // Null Move Pruning (NMP): keep expensive partner-check detection and @@ -538,42 +533,49 @@ namespace ironphoenix { && !pvNode && ply > 0 && depth >= NULL_MOVE_MIN_DEPTH + && staticEval >= beta && !inCheckNode + && !partnerChecked && !mateWindow && hasNonPawnMaterial(pos, us)) { - const Color partner = static_cast(static_cast(us) ^ 2u); - if (!pos.inCheck(partner)) { - const int staticEval = evaluate(pos); - - if (staticEval >= beta) { - StateInfo nullState; - pos.makeNullMove(nullState); - searchStack_[ply + 1] = HistoryContext{}; - - // Dynamic null-move reduction: reduce the depth more aggressively - // at deeper nodes and for higher static evaluations. - const int reduction = 4 + depth / 5 + std::min(3, (staticEval - beta) / NULL_MOVE_DIVISOR); - const int nullScore = -negamax( - pos, - depth - reduction, - -beta, - -beta + 1, - ply + 1, - false, - false - ); - - pos.undoNullMove(nullState); - - if (shouldStop()) - return 0; + StateInfo nullState; + pos.makeNullMove(nullState); + searchStack_[ply + 1] = HistoryContext{}; + + // Dynamic null-move reduction: reduce the depth more aggressively + // at deeper nodes and for higher static evaluations. + const int reduction = 4 + depth / 5 + std::min(3, (staticEval - beta) / NULL_MOVE_DIVISOR); + const int nullScore = -negamax( + pos, + depth - reduction, + -beta, + -beta + 1, + ply + 1, + false, + false + ); + + pos.undoNullMove(nullState); + + if (shouldStop()) + return 0; + + // Never manufacture a mate score from a synthetic pass. + if (nullScore >= beta && nullScore < MATE_THRESHOLD) + return nullScore; + } - // Never manufacture a mate score from a synthetic pass. - if (nullScore >= beta && nullScore < MATE_THRESHOLD) - return nullScore; - } - } + // Internal Iterative Reduction (IIR): keep this deliberately conservative + // in 4PC. Only reduce deep PV nodes when move ordering has no TT move to + // guide the search. Non-PV nodes retain their full depth. + if (pvNode + && ply > 0 + && depth >= IIR_MIN_DEPTH + && !inCheckNode + && !partnerChecked + && !ttMove) { + depth -= 1; } MoveList moves; From a01dc683850abd7942babc67f332a62118ac4917 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 13 Oct 2026 07:06:58 -0500 Subject: [PATCH 06/11] Add cut/all node search state --- include/ironphoenix/search.hpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 2e61f85..d753ea8 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -87,7 +87,10 @@ namespace ironphoenix { void run(Position position, std::ostream& out); void runMultiPV(Position position, int multiPV, std::ostream& out); - int negamax(Position& pos, int depth, int alpha, int beta, int ply, bool pvNode, bool allowNull = true); + // cutNode marks a non-PV node expected to fail high. allNode is derived + // inside negamax as the complementary non-PV node type. + int negamax(Position& pos, int depth, int alpha, int beta, int ply, + bool pvNode, bool allowNull = true, bool cutNode = false); int qsearch(Position& pos, int alpha, int beta, int ply, bool pvNode); int evaluate(const Position& pos) const noexcept; int moveScore(const Position& pos, Move move, Move ttMove, int ply, bool givesCheck) const noexcept; @@ -132,4 +135,4 @@ namespace ironphoenix { void emitBestMove(std::ostream& out, Move move); }; -} +} \ No newline at end of file From 0fe2db55e38746742266a3199a5936212832f241 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 13 Oct 2026 07:08:27 -0500 Subject: [PATCH 07/11] Propagate cut and all node types --- src/search.cpp | 72 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index a1afaa9..470f855 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -475,7 +475,8 @@ namespace ironphoenix { return bestScore; } - int SearchEngine::negamax(Position& pos, int depth, int alpha, int beta, int ply, bool pvNode, bool allowNull) { + int SearchEngine::negamax(Position& pos, int depth, int alpha, int beta, int ply, + bool pvNode, bool allowNull, bool cutNode) { if (ply >= MAX_PLY - 1) return evaluate(pos); if (shouldStop()) @@ -484,6 +485,11 @@ namespace ironphoenix { if (depth <= 0) return qsearch(pos, alpha, beta, ply, pvNode); + // Node-type classification. PV nodes are never cut nodes. A non-PV node + // that is not expected to fail high is an all node. + cutNode = !pvNode && cutNode; + const bool allNode = !pvNode && !cutNode; + ++nodes_; selDepth_ = std::max(selDepth_, ply); pvLength_[ply] = ply; @@ -553,6 +559,7 @@ namespace ironphoenix { -beta + 1, ply + 1, false, + false, false ); @@ -596,7 +603,7 @@ namespace ironphoenix { givesCheckBits.data()); } else { - // At cut nodes avoid sorting the entire move list and avoid eager + // At non-PV nodes avoid sorting the entire move list and avoid eager // givesCheck() calls. Score cheaply, then select the best few moves // only as they are actually requested by the search loop. for (std::size_t i = 0; i < moves.size; ++i) @@ -712,7 +719,19 @@ namespace ironphoenix { ++legalMoves; if (legalMoves == 1) { - score = -negamax(pos, depth - 1, -beta, -alpha, ply + 1, pvNode); + // PV nodes keep a PV child. For non-PV nodes, an all node + // expects its first child to be a cut node, while a cut node + // expects its first child to be an all node. + score = -negamax( + pos, + depth - 1, + -beta, + -alpha, + ply + 1, + pvNode, + true, + allNode + ); } else { @@ -728,19 +747,58 @@ namespace ironphoenix { reduction = std::clamp(reduction, 0, depth - 2); } + // PVS scout searches are cut nodes: from the child point of + // view we expect a fail-high against the null window when the + // move is not good enough to raise the parent's alpha. if (reduction > 0) { const int reducedDepth = depth - 1 - reduction; - score = -negamax(pos, reducedDepth, -alpha - 1, -alpha, ply + 1, false); + score = -negamax( + pos, + reducedDepth, + -alpha - 1, + -alpha, + ply + 1, + false, + true, + true + ); if (!shouldStop() && score > alpha) - score = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1, false); + score = -negamax( + pos, + depth - 1, + -alpha - 1, + -alpha, + ply + 1, + false, + true, + true + ); } else { - score = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1, false); + score = -negamax( + pos, + depth - 1, + -alpha - 1, + -alpha, + ply + 1, + false, + true, + true + ); } if (!shouldStop() && score > alpha && score < beta) - score = -negamax(pos, depth - 1, -beta, -alpha, ply + 1, pvNode); + score = -negamax( + pos, + depth - 1, + -beta, + -alpha, + ply + 1, + pvNode, + true, + false + ); } } pos.undoMove(move, st); From 2fa7312d74998bd75faa3de6bcb41065ec8c2a33 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 23 Oct 2026 06:27:05 -0500 Subject: [PATCH 08/11] Search: apply new SPSA-tuned defaults and report actual IIR reductions Update all 21 non-IIR search defaults without modifying EvalParameters or PV-only IIR settings. Derive UCI option default values directly from SearchParameters to prevent stale literals. Count applied IIR reductions at the end of regular and MultiPV searches; extend regression tests. --- docs/internal-iterative-reduction.md | 33 ++++++++++++++++++ include/ironphoenix/search.hpp | 1 + include/ironphoenix/tuning.hpp | 34 +++++++++---------- src/multipv.cpp | 2 ++ src/search.cpp | 7 +++- src/tuning.cpp | 51 ++++++++++++++-------------- tests/iir.cpp | 41 ++++++++++++++++++++++ tests/mate_stop.cpp | 2 ++ 8 files changed, 128 insertions(+), 43 deletions(-) diff --git a/docs/internal-iterative-reduction.md b/docs/internal-iterative-reduction.md index beeca80..98b366f 100644 --- a/docs/internal-iterative-reduction.md +++ b/docs/internal-iterative-reduction.md @@ -34,3 +34,36 @@ playing strength and the search tree. This feature's default is active only at sufficiently deep internal PV nodes. It need not raise raw nodes/second (NPS); compare Elo and time to depth rather than assuming a speedup from a smaller search. + + +## Updated tuned search defaults + +The search structure now includes the user's latest SPSA values for all +21 non-IIR parameters. `uci` reports the live `SearchParameters{}` +defaults rather than stale hard-coded literals. Existing evaluation +parameters are unchanged. + +## Is IIR actually running? + +By design, the default policy only reduces **internal PV nodes at depth +at least six** with **no TT move** and **neither king in check**. In iterative +deepening the PV nodes frequently have TT moves from earlier iterations; +therefore a zero count at short time controls is entirely possible. + +After each completed regular or MultiPV search, the engine emits +`info string iir reductions N` immediately before `bestmove`. This is +the count of **actual depth reductions applied**, not just considered nodes. +The counter resets on each search. With the supplied cut/all defaults +both zero, enabling IIR at non-PV nodes requires an explicit UCI change +and independent playing-strength testing. + +Example: +```text +setoption name SearchIIRMinDepth value 6 +setoption name SearchIIRReduction value 1 +setoption name SearchIIRCutNode value 1 +setoption name SearchIIRAllNode value 0 +go depth 10 +``` +This example changes search behavior only if explicitly entered; the +repository defaults remain PV-only. diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 209f171..3be08e5 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -97,6 +97,7 @@ namespace ironphoenix { std::int64_t hardTimeBudgetMs_ = 0; bool pondering_ = false; std::uint64_t nodes_ = 0; + std::uint64_t iirReductions_ = 0; std::uint64_t nextTimeCheckNode_ = 0; int selDepth_ = 0; diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index e00cb07..9be56f7 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -10,27 +10,27 @@ namespace ironphoenix { namespace Tuning { struct SearchParameters final { - int aspirationStartDepth = 4; - int aspirationInitialDelta = 50; + int aspirationStartDepth = 2; + int aspirationInitialDelta = 54; int seePruneMaxDepth = 4; - int seePruneMarginPerDepth = 80; - int historyBonusScale = 64; - int historyBonusMax = 2048; - int counterMoveBonus = 32'000; - int checkMoveBonus = 10'000'000; - int lmrMinDepth = 3; - int lmrFullMoves = 4; - int lmrBaseX100 = 78; - int lmrDivisorX100 = 286; - int lmpHistoryThreshold = 1918; + int seePruneMarginPerDepth = 87; + int historyBonusScale = 63; + int historyBonusMax = 2044; + int counterMoveBonus = 31'995; + int checkMoveBonus = 9'999'994; + int lmrMinDepth = 1; + int lmrFullMoves = 6; + int lmrBaseX100 = 79; + int lmrDivisorX100 = 300; + int lmpHistoryThreshold = 1927; int hmpMaxDepth = 4; - int hmpMarginPerDepth = 1519; - int nullMoveMinDepth = 2; - int nullMoveDivisor = 192; + int hmpMarginPerDepth = 1522; + int nullMoveMinDepth = 3; + int nullMoveDivisor = 194; int rfpMaxDepth = 6; int rfpMarginPerDepth = 141; - int rfpMinMargin = 88; - int rfpMaxImprovingDiscount = 78; + int rfpMinMargin = 93; + int rfpMaxImprovingDiscount = 82; // PV-only IIR at depth 6; cut/all experiments opt-in. int iirMinDepth = 6; int iirReduction = 1; diff --git a/src/multipv.cpp b/src/multipv.cpp index 428c26f..c15c52d 100644 --- a/src/multipv.cpp +++ b/src/multipv.cpp @@ -109,6 +109,7 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out) hardTimeBudgetMs_ = timeBudget.hardMs; pondering_ = limits_.ponder; nodes_ = 0; + iirReductions_ = 0; nextTimeCheckNode_ = 0; selDepth_ = 0; @@ -295,6 +296,7 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out) break; } + out << "info string iir reductions " << iirReductions_ << '\n'; emitBestMove(out, bestMove, bestPonderMove); } diff --git a/src/search.cpp b/src/search.cpp index ab35e34..03e411a 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -816,8 +816,10 @@ namespace ironphoenix { const int iir = IIR::reduction(depth, ply, pvNode, cutNode, static_cast(ttMove), inCheckNode, tuning); if (iir > 0 - && !pos.inCheck(static_cast(static_cast(us) ^ 2u))) + && !pos.inCheck(static_cast(static_cast(us) ^ 2u))) { depth -= iir; + ++iirReductions_; + } MoveList moves; generatePseudoLegalMoves(pos, moves); @@ -1121,6 +1123,7 @@ namespace ironphoenix { hardTimeBudgetMs_ = timeBudget.hardMs; pondering_ = limits_.ponder; nodes_ = 0; + iirReductions_ = 0; nextTimeCheckNode_ = 0; selDepth_ = 0; for (auto& row : pv_) @@ -1320,6 +1323,8 @@ namespace ironphoenix { break; } + // One diagnostic per search, not per node: negligible hot-path cost. + out << "info string iir reductions " << iirReductions_ << '\n'; emitBestMove(out, bestMove, bestPonderMove); } diff --git a/src/tuning.cpp b/src/tuning.cpp index afd8cd9..82d4a5c 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -282,32 +282,33 @@ namespace ironphoenix::Tuning { } void printUciOptions(std::ostream& out) { + const SearchParameters searchDefaults{}; const EvalParameters defaults{}; - out << "option name SearchAspirationStartDepth type spin default 4 min 1 max 32\n" - << "option name SearchAspirationDelta type spin default 50 min 1 max 2000\n" - << "option name SearchSEEPruneMaxDepth type spin default 4 min 0 max 32\n" - << "option name SearchSEEPruneMargin type spin default 80 min 0 max 2000\n" - << "option name SearchHistoryBonusScale type spin default 64 min 0 max 2048\n" - << "option name SearchHistoryBonusMax type spin default 2048 min 1 max 32767\n" - << "option name SearchCounterMoveBonus type spin default 32000 min 0 max 50000000\n" - << "option name SearchCheckMoveBonus type spin default 10000000 min 0 max 50000000\n" - << "option name SearchLMRMinDepth type spin default 3 min 1 max 32\n" - << "option name SearchLMRFullMoves type spin default 3 min 0 max 128\n" - << "option name SearchLMRBaseX100 type spin default 78 min 0 max 1000\n" - << "option name SearchLMRDivisorX100 type spin default 286 min 25 max 5000\n" - << "option name SearchLMPHistoryThreshold type spin default 2000 min 0 max 20000\n" - << "option name SearchHMPMaxDepth type spin default 4 min 0 max 32\n" - << "option name SearchHMPMarginPerDepth type spin default 1500 min 0 max 10000\n" - << "option name SearchNullMoveMinDepth type spin default 2 min 1 max 32\n" - << "option name SearchNullMoveDivisor type spin default 192 min 1 max 5000\n" - << "option name SearchRFPMaxDepth type spin default 6 min 0 max 32\n" - << "option name SearchRFPMarginPerDepth type spin default 141 min 0 max 5000\n" - << "option name SearchRFPMinMargin type spin default 88 min 0 max 5000\n" - << "option name SearchRFPMaxImprovingDiscount type spin default 78 min 0 max 5000\n" - << "option name SearchIIRMinDepth type spin default 6 min 2 max 32\n" - << "option name SearchIIRReduction type spin default 1 min 0 max 4\n" - << "option name SearchIIRCutNode type spin default 0 min 0 max 1\n" - << "option name SearchIIRAllNode type spin default 0 min 0 max 1\n" + out << "option name SearchAspirationStartDepth type spin default " << searchDefaults.aspirationStartDepth << " min 1 max 32\n" + << "option name SearchAspirationDelta type spin default " << searchDefaults.aspirationInitialDelta << " min 1 max 2000\n" + << "option name SearchSEEPruneMaxDepth type spin default " << searchDefaults.seePruneMaxDepth << " min 0 max 32\n" + << "option name SearchSEEPruneMargin type spin default " << searchDefaults.seePruneMarginPerDepth << " min 0 max 2000\n" + << "option name SearchHistoryBonusScale type spin default " << searchDefaults.historyBonusScale << " min 0 max 2048\n" + << "option name SearchHistoryBonusMax type spin default " << searchDefaults.historyBonusMax << " min 1 max 32767\n" + << "option name SearchCounterMoveBonus type spin default " << searchDefaults.counterMoveBonus << " min 0 max 50000000\n" + << "option name SearchCheckMoveBonus type spin default " << searchDefaults.checkMoveBonus << " min 0 max 50000000\n" + << "option name SearchLMRMinDepth type spin default " << searchDefaults.lmrMinDepth << " min 1 max 32\n" + << "option name SearchLMRFullMoves type spin default " << searchDefaults.lmrFullMoves << " min 0 max 128\n" + << "option name SearchLMRBaseX100 type spin default " << searchDefaults.lmrBaseX100 << " min 0 max 1000\n" + << "option name SearchLMRDivisorX100 type spin default " << searchDefaults.lmrDivisorX100 << " min 25 max 5000\n" + << "option name SearchLMPHistoryThreshold type spin default " << searchDefaults.lmpHistoryThreshold << " min 0 max 20000\n" + << "option name SearchHMPMaxDepth type spin default " << searchDefaults.hmpMaxDepth << " min 0 max 32\n" + << "option name SearchHMPMarginPerDepth type spin default " << searchDefaults.hmpMarginPerDepth << " min 0 max 10000\n" + << "option name SearchNullMoveMinDepth type spin default " << searchDefaults.nullMoveMinDepth << " min 1 max 32\n" + << "option name SearchNullMoveDivisor type spin default " << searchDefaults.nullMoveDivisor << " min 1 max 5000\n" + << "option name SearchRFPMaxDepth type spin default " << searchDefaults.rfpMaxDepth << " min 0 max 32\n" + << "option name SearchRFPMarginPerDepth type spin default " << searchDefaults.rfpMarginPerDepth << " min 0 max 5000\n" + << "option name SearchRFPMinMargin type spin default " << searchDefaults.rfpMinMargin << " min 0 max 5000\n" + << "option name SearchRFPMaxImprovingDiscount type spin default " << searchDefaults.rfpMaxImprovingDiscount << " min 0 max 5000\n" + << "option name SearchIIRMinDepth type spin default " << searchDefaults.iirMinDepth << " min 2 max 32\n" + << "option name SearchIIRReduction type spin default " << searchDefaults.iirReduction << " min 0 max 4\n" + << "option name SearchIIRCutNode type spin default " << searchDefaults.iirCutNode << " min 0 max 1\n" + << "option name SearchIIRAllNode type spin default " << searchDefaults.iirAllNode << " min 0 max 1\n" << "option name EvalPawnValue type spin default " << defaults.pawnValue << " min 0 max 5000\n" << "option name EvalKnightValue type spin default " << defaults.knightValue << " min 0 max 5000\n" << "option name EvalBishopValue type spin default " << defaults.bishopValue << " min 0 max 5000\n" diff --git a/tests/iir.cpp b/tests/iir.cpp index a6a431e..ff8a407 100644 --- a/tests/iir.cpp +++ b/tests/iir.cpp @@ -24,6 +24,47 @@ int main() { Geometry::init(); Tuning::resetSearch(); const Tuning::SearchParameters defaults{}; + + // The 21 non-IIR values are the latest SPSA-tuned defaults; the IIR + // controls preserve the user's requested PV-only experiment. + struct Expected { const char* name; int Tuning::SearchParameters::*field; int value; }; + const Expected tuned[] = { + {"SearchAspirationStartDepth", &Tuning::SearchParameters::aspirationStartDepth, 2}, + {"SearchAspirationDelta", &Tuning::SearchParameters::aspirationInitialDelta, 54}, + {"SearchSEEPruneMaxDepth", &Tuning::SearchParameters::seePruneMaxDepth, 4}, + {"SearchSEEPruneMargin", &Tuning::SearchParameters::seePruneMarginPerDepth, 87}, + {"SearchHistoryBonusScale", &Tuning::SearchParameters::historyBonusScale, 63}, + {"SearchHistoryBonusMax", &Tuning::SearchParameters::historyBonusMax, 2044}, + {"SearchCounterMoveBonus", &Tuning::SearchParameters::counterMoveBonus, 31995}, + {"SearchCheckMoveBonus", &Tuning::SearchParameters::checkMoveBonus, 9999994}, + {"SearchLMRMinDepth", &Tuning::SearchParameters::lmrMinDepth, 1}, + {"SearchLMRFullMoves", &Tuning::SearchParameters::lmrFullMoves, 6}, + {"SearchLMRBaseX100", &Tuning::SearchParameters::lmrBaseX100, 79}, + {"SearchLMRDivisorX100", &Tuning::SearchParameters::lmrDivisorX100, 300}, + {"SearchLMPHistoryThreshold", &Tuning::SearchParameters::lmpHistoryThreshold, 1927}, + {"SearchHMPMaxDepth", &Tuning::SearchParameters::hmpMaxDepth, 4}, + {"SearchHMPMarginPerDepth", &Tuning::SearchParameters::hmpMarginPerDepth, 1522}, + {"SearchNullMoveMinDepth", &Tuning::SearchParameters::nullMoveMinDepth, 3}, + {"SearchNullMoveDivisor", &Tuning::SearchParameters::nullMoveDivisor, 194}, + {"SearchRFPMaxDepth", &Tuning::SearchParameters::rfpMaxDepth, 6}, + {"SearchRFPMarginPerDepth", &Tuning::SearchParameters::rfpMarginPerDepth, 141}, + {"SearchRFPMinMargin", &Tuning::SearchParameters::rfpMinMargin, 93}, + {"SearchRFPMaxImprovingDiscount", &Tuning::SearchParameters::rfpMaxImprovingDiscount, 82}, + {"SearchIIRMinDepth", &Tuning::SearchParameters::iirMinDepth, 6}, + {"SearchIIRReduction", &Tuning::SearchParameters::iirReduction, 1}, + {"SearchIIRCutNode", &Tuning::SearchParameters::iirCutNode, 0}, + {"SearchIIRAllNode", &Tuning::SearchParameters::iirAllNode, 0}, + }; + std::ostringstream defaultsUci; + Tuning::printUciOptions(defaultsUci); + for (const auto& row : tuned) { + expect(Tuning::search().*(row.field) == row.value, + "SPSA search default matches requested value"); + expect(defaultsUci.str().find(std::string("option name ") + row.name + + " type spin default " + std::to_string(row.value) + " min ") + != std::string::npos, + "UCI advertises current SPSA default"); + } expect(defaults.iirMinDepth == 6, "IIR default depth six"); expect(defaults.iirReduction == 1, "IIR default reduction one"); expect(defaults.iirCutNode == 0 && defaults.iirAllNode == 0, diff --git a/tests/mate_stop.cpp b/tests/mate_stop.cpp index 86e1b32..91aae06 100644 --- a/tests/mate_stop.cpp +++ b/tests/mate_stop.cpp @@ -85,6 +85,8 @@ void testRootMate(bool multiPV) { expect(completed, multiPV ? "MultiPV returned promptly on mate" : "root search returned promptly on mate"); expect(text.find("score mate 1") != std::string::npos, "engine reports winning mate in one"); expect(text.find("bestmove d7a7") != std::string::npos, "engine returns king-capturing move"); + expect(text.find("info string iir reductions ") != std::string::npos, + "search reports actual applied IIR reductions"); expect(text.find("info depth 2 ") == std::string::npos, "no further deepening after winning mate"); if (multiPV) { expect(text.find("multipv 1 ") != std::string::npos, "MultiPV top line emitted"); From e2ffd4130cbd4dbfec8e4a05789432d52b3e8d07 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 23 Oct 2026 06:43:44 -0500 Subject: [PATCH 09/11] Search: apply IIR at every node when depth threshold met and no TT move Remove PV/cut/all and king-check exclusions from IIR. Retain tunable minimum depth/reduction and clamp to preserve positive depth. Remove obsolete cut/all IIR UCI options and propagation, update tests and docs. --- docs/internal-iterative-reduction.md | 81 ++++++++-------------------- include/ironphoenix/iir.hpp | 20 +++---- include/ironphoenix/search.hpp | 3 +- include/ironphoenix/tuning.hpp | 4 +- src/search.cpp | 33 ++++-------- src/tuning.cpp | 14 +---- tests/iir.cpp | 79 ++++++++++----------------- 7 files changed, 71 insertions(+), 163 deletions(-) diff --git a/docs/internal-iterative-reduction.md b/docs/internal-iterative-reduction.md index 98b366f..2c28867 100644 --- a/docs/internal-iterative-reduction.md +++ b/docs/internal-iterative-reduction.md @@ -1,69 +1,30 @@ # Internal Iterative Reduction (4PC Teams) -This feature branch merges current `main` with the original -`internal-iterative-reduction` experiment. Existing search pruning, eval, -NNUE, MoveGen, test infrastructure, and cut/all-node propagation are retained. +IIR is now allowed at **every `negamax` node**, not just PV nodes. +There is no PV/cut/all-node, king-check, partner-check, or root exclusion. +With the current defaults, the rule is equivalent to: -IIR reduces search depth when a sufficiently deep node lacks an ordering -move from the transposition table. It is deliberately conservative in 4PC: -the moving side's king and its **partner's king** must both be out of check, -and root positions are excluded. In the default configuration, only PV -nodes qualify. We keep the branch's original depth-six, one-ply policy. +```cpp +if (depth >= tuning.iirMinDepth && !ttMove) + depth -= std::min(tuning.iirReduction, depth - 1); +``` + +`SearchIIRMinDepth` and `SearchIIRReduction` are the only two UCI knobs. +The old `SearchIIRCutNode` and `SearchIIRAllNode` toggles were removed +because IIR no longer depends on node types; cut/all propagation was +also removed from `negamax`. | UCI option | Default | Valid range | Purpose | |---|---:|---:|---| -| `SearchIIRMinDepth` | 6 | 2–32 | Minimum node depth | -| `SearchIIRReduction` | 1 | 0–4 | Depth reduction (0 disables) | -| `SearchIIRCutNode` | 0 | 0–1 | Opt into non-PV cut-node IIR | -| `SearchIIRAllNode` | 0 | 0–1 | Opt into non-PV all-node IIR | - -The reduction is clamped so search depth remains at least 1. No IIR is -performed when a TT move exists, at root, in check, or when the teammate is -in check. PVS first-child, scout, null-move, and re-search propagation keeps -cut/all-node context explicit. - -## SPSA / SPRT - -Tunex can tune all four variables using UCI `setoption`. A recommended -starting point is to tune `SearchIIRMinDepth` and -`SearchIIRReduction` with `SearchIIRCutNode=0` and -`SearchIIRAllNode=0`; test enabling the non-PV switches separately. -Use paired-game SPRT before merging into `main` because IIR affects -playing strength and the search tree. - -This feature's default is active only at sufficiently deep internal PV -nodes. It need not raise raw nodes/second (NPS); compare Elo and time -to depth rather than assuming a speedup from a smaller search. +| `SearchIIRMinDepth` | 6 | 2–32 | Minimum depth to apply IIR | +| `SearchIIRReduction` | 1 | 0–4 | Depth reduction; 0 disables | +All other tuned search and evaluation values are unchanged. A safety clamp +keeps the remaining depth >= 1, even when tuning a large reduction. -## Updated tuned search defaults +The engine emits `info string iir reductions N` before the bestmove +for each normal/MultiPV search. This counts applied depth reductions. -The search structure now includes the user's latest SPSA values for all -21 non-IIR parameters. `uci` reports the live `SearchParameters{}` -defaults rather than stale hard-coded literals. Existing evaluation -parameters are unchanged. - -## Is IIR actually running? - -By design, the default policy only reduces **internal PV nodes at depth -at least six** with **no TT move** and **neither king in check**. In iterative -deepening the PV nodes frequently have TT moves from earlier iterations; -therefore a zero count at short time controls is entirely possible. - -After each completed regular or MultiPV search, the engine emits -`info string iir reductions N` immediately before `bestmove`. This is -the count of **actual depth reductions applied**, not just considered nodes. -The counter resets on each search. With the supplied cut/all defaults -both zero, enabling IIR at non-PV nodes requires an explicit UCI change -and independent playing-strength testing. - -Example: -```text -setoption name SearchIIRMinDepth value 6 -setoption name SearchIIRReduction value 1 -setoption name SearchIIRCutNode value 1 -setoption name SearchIIRAllNode value 0 -go depth 10 -``` -This example changes search behavior only if explicitly entered; the -repository defaults remain PV-only. +**Important:** Applying IIR during checks, at the root and at non-PV +nodes can reduce tactical accuracy and playing strength. Run independent +paired SPRT before merging this aggressive variant into `main`. diff --git a/include/ironphoenix/iir.hpp b/include/ironphoenix/iir.hpp index cbe8e19..ae0ca5d 100644 --- a/include/ironphoenix/iir.hpp +++ b/include/ironphoenix/iir.hpp @@ -6,22 +6,14 @@ namespace ironphoenix::IIR { -// Select a reduction using the propagated PVS node type. The caller also -// verifies the partner king is not checked before applying this reduction. -// PV-only is the default, matching the original 4PC IIR experiment. -[[nodiscard]] inline int reduction(int depth, int ply, bool pvNode, - bool cutNode, bool hasTTMove, bool inCheck, +// Internal Iterative Reduction for every negamax node without a TT move. +// The minimum depth and reduction size are independently UCI-tunable. +[[nodiscard]] inline int reduction(int depth, bool hasTTMove, const Tuning::SearchParameters& tune) noexcept { - - if (tune.iirReduction <= 0 || ply <= 0 || depth < tune.iirMinDepth - || depth <= 1 || hasTTMove || inCheck) - return 0; - - if (!pvNode && !(cutNode ? tune.iirCutNode : tune.iirAllNode)) + if (depth < tune.iirMinDepth || hasTTMove || tune.iirReduction <= 0) return 0; - - // Depth never falls below one; avoid unbounded reductions if callers - // pass pathological values even though UCI setters validate ranges. + // Even when the configured reduction exceeds this node's depth, keep + // at least one search ply instead of feeding a negative depth to move gen. return std::min(tune.iirReduction, depth - 1); } diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 3be08e5..00cec82 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -109,9 +109,8 @@ namespace ironphoenix { void run(Position position, std::ostream& out); void runMultiPV(Position position, int multiPV, std::ostream& out); - // Expected cut/all node type propagated through PVS. int negamax(Position& pos, int depth, int alpha, int beta, int ply, - bool pvNode, bool allowNull = true, bool cutNode = false); + bool pvNode, bool allowNull = true); int qsearch(Position& pos, int alpha, int beta, int ply, bool pvNode); int evaluate(const Position& pos) const noexcept; void recordStackEvaluation(int ply, int score) noexcept; diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index 9be56f7..64684f1 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -31,11 +31,9 @@ namespace ironphoenix { int rfpMarginPerDepth = 141; int rfpMinMargin = 93; int rfpMaxImprovingDiscount = 82; - // PV-only IIR at depth 6; cut/all experiments opt-in. + // Applies to every search node without a TT move at depth >= 6. int iirMinDepth = 6; int iirReduction = 1; - int iirCutNode = 0; - int iirAllNode = 0; }; struct EvalParameters final { diff --git a/src/search.cpp b/src/search.cpp index 03e411a..4a02138 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -689,7 +689,7 @@ namespace ironphoenix { } int SearchEngine::negamax(Position& pos, int depth, int alpha, int beta, int ply, - bool pvNode, bool allowNull, bool cutNode) { + bool pvNode, bool allowNull) { if (ply >= MAX_PLY - 1) return evaluate(pos); if (shouldStop()) @@ -698,10 +698,6 @@ namespace ironphoenix { if (depth <= 0) return qsearch(pos, alpha, beta, ply, pvNode); - // Preserve non-PV cut/all node type propagation from the original branch. - cutNode = !pvNode && cutNode; - const bool allNode = !pvNode && !cutNode; - auto& ss = searchStack_[ply]; ss.beginNode(pos.sideToMove(), depth, pvNode); @@ -795,7 +791,6 @@ namespace ironphoenix { -beta + 1, ply + 1, false, - false, false ); @@ -811,12 +806,11 @@ namespace ironphoenix { } } - // Apply IIR only after TT/RFP/NMP. When either king on the - // moving team is checked, retain the full search depth. - const int iir = IIR::reduction(depth, ply, pvNode, cutNode, - static_cast(ttMove), inCheckNode, tuning); - if (iir > 0 - && !pos.inCheck(static_cast(static_cast(us) ^ 2u))) { + // IIR applies at any search node (PV, cut, all, root, or check) + // once the configured depth is reached without a TT ordering move. + // The helper clamps the reduction to keep depth >= 1. + const int iir = IIR::reduction(depth, static_cast(ttMove), tuning); + if (iir > 0) { depth -= iir; ++iirReductions_; } @@ -940,8 +934,7 @@ namespace ironphoenix { ++legalMoves; ss.moveCount = legalMoves; if (legalMoves == 1) { - score = -negamax(pos, depth - 1, -beta, -alpha, ply + 1, - pvNode, true, allNode); + score = -negamax(pos, depth - 1, -beta, -alpha, ply + 1, pvNode); } else { @@ -959,21 +952,17 @@ namespace ironphoenix { if (reduction > 0) { const int reducedDepth = depth - 1 - reduction; - score = -negamax(pos, reducedDepth, -alpha - 1, -alpha, - ply + 1, false, true, true); + score = -negamax(pos, reducedDepth, -alpha - 1, -alpha, ply + 1, false); if (!shouldStop() && score > alpha) - score = -negamax(pos, depth - 1, -alpha - 1, -alpha, - ply + 1, false, true, true); + score = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1, false); } else { - score = -negamax(pos, depth - 1, -alpha - 1, -alpha, - ply + 1, false, true, true); + score = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1, false); } if (!shouldStop() && score > alpha && score < beta) - score = -negamax(pos, depth - 1, -beta, -alpha, - ply + 1, pvNode, true, false); + score = -negamax(pos, depth - 1, -beta, -alpha, ply + 1, pvNode); } } pos.undoMove(move, st); diff --git a/src/tuning.cpp b/src/tuning.cpp index 82d4a5c..255ebb8 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -57,9 +57,7 @@ namespace ironphoenix::Tuning { || name == "SearchRFPMinMargin" || name == "SearchRFPMaxImprovingDiscount" || name == "SearchIIRMinDepth" - || name == "SearchIIRReduction" - || name == "SearchIIRCutNode" - || name == "SearchIIRAllNode"; + || name == "SearchIIRReduction"; } bool isEvalParameter(std::string_view name) noexcept { @@ -172,10 +170,6 @@ namespace ironphoenix::Tuning { p.iirMinDepth = value; else if (name == "SearchIIRReduction" && inRange(value, 0, 4)) p.iirReduction = value; - else if (name == "SearchIIRCutNode" && inRange(value, 0, 1)) - p.iirCutNode = value; - else if (name == "SearchIIRAllNode" && inRange(value, 0, 1)) - p.iirAllNode = value; else changed = false; @@ -307,8 +301,6 @@ namespace ironphoenix::Tuning { << "option name SearchRFPMaxImprovingDiscount type spin default " << searchDefaults.rfpMaxImprovingDiscount << " min 0 max 5000\n" << "option name SearchIIRMinDepth type spin default " << searchDefaults.iirMinDepth << " min 2 max 32\n" << "option name SearchIIRReduction type spin default " << searchDefaults.iirReduction << " min 0 max 4\n" - << "option name SearchIIRCutNode type spin default " << searchDefaults.iirCutNode << " min 0 max 1\n" - << "option name SearchIIRAllNode type spin default " << searchDefaults.iirAllNode << " min 0 max 1\n" << "option name EvalPawnValue type spin default " << defaults.pawnValue << " min 0 max 5000\n" << "option name EvalKnightValue type spin default " << defaults.knightValue << " min 0 max 5000\n" << "option name EvalBishopValue type spin default " << defaults.bishopValue << " min 0 max 5000\n" @@ -359,9 +351,7 @@ namespace ironphoenix::Tuning { << " SearchRFPMinMargin=" << s.rfpMinMargin << " SearchRFPMaxImprovingDiscount=" << s.rfpMaxImprovingDiscount << " SearchIIRMinDepth=" << s.iirMinDepth - << " SearchIIRReduction=" << s.iirReduction - << " SearchIIRCutNode=" << s.iirCutNode - << " SearchIIRAllNode=" << s.iirAllNode << '\n'; + << " SearchIIRReduction=" << s.iirReduction << '\n'; out << "info string EvalPawnValue=" << e.pawnValue << " EvalKnightValue=" << e.knightValue diff --git a/tests/iir.cpp b/tests/iir.cpp index ff8a407..10b4615 100644 --- a/tests/iir.cpp +++ b/tests/iir.cpp @@ -15,18 +15,11 @@ int failures = 0; void expect(bool passed, const char* name) { if (!passed) { std::cerr << "FAIL: " << name << '\n'; ++failures; } } -int reduce(int depth, int ply, bool pv, bool cut, bool tt, bool check) { - return IIR::reduction(depth, ply, pv, cut, tt, check, Tuning::search()); -} -} - int main() { Geometry::init(); Tuning::resetSearch(); const Tuning::SearchParameters defaults{}; - // The 21 non-IIR values are the latest SPSA-tuned defaults; the IIR - // controls preserve the user's requested PV-only experiment. struct Expected { const char* name; int Tuning::SearchParameters::*field; int value; }; const Expected tuned[] = { {"SearchAspirationStartDepth", &Tuning::SearchParameters::aspirationStartDepth, 2}, @@ -52,8 +45,6 @@ int main() { {"SearchRFPMaxImprovingDiscount", &Tuning::SearchParameters::rfpMaxImprovingDiscount, 82}, {"SearchIIRMinDepth", &Tuning::SearchParameters::iirMinDepth, 6}, {"SearchIIRReduction", &Tuning::SearchParameters::iirReduction, 1}, - {"SearchIIRCutNode", &Tuning::SearchParameters::iirCutNode, 0}, - {"SearchIIRAllNode", &Tuning::SearchParameters::iirAllNode, 0}, }; std::ostringstream defaultsUci; Tuning::printUciOptions(defaultsUci); @@ -67,65 +58,53 @@ int main() { } expect(defaults.iirMinDepth == 6, "IIR default depth six"); expect(defaults.iirReduction == 1, "IIR default reduction one"); - expect(defaults.iirCutNode == 0 && defaults.iirAllNode == 0, - "IIR remains PV-only by default"); - expect(reduce(5, 1, true, false, false, false) == 0, "below minimum depth"); - expect(reduce(6, 1, true, false, false, false) == 1, "PV IIR at depth six"); - expect(reduce(9, 2, true, false, false, false) == 1, "PV deep IIR"); - expect(reduce(9, 0, true, false, false, false) == 0, "root excluded"); - expect(reduce(9, 2, true, false, true, false) == 0, "TT move excludes IIR"); - expect(reduce(9, 2, true, false, false, true) == 0, "check excludes IIR"); - expect(reduce(9, 2, false, true, false, false) == 0, "cut node off by default"); - expect(reduce(9, 2, false, false, false, false) == 0, "all node off by default"); + // IIR no longer restricts PV/cut/all nodes, the root, or check status. + // Once the depth threshold is met, absence of a TT move is sufficient. + expect(reduce(5, false) == 0, "depth below threshold"); + expect(reduce(6, false) == 1, "eligible depth six"); + expect(reduce(9, false) == 1, "deep node qualifies"); + expect(reduce(9, true) == 0, "TT move still suppresses IIR"); - expect(Tuning::setSearchParameter("SearchIIRMinDepth", 4), "set minimum depth"); + expect(Tuning::setSearchParameter("SearchIIRMinDepth", 4), "set min depth"); expect(Tuning::setSearchParameter("SearchIIRReduction", 3), "set reduction"); - expect(Tuning::setSearchParameter("SearchIIRCutNode", 1), "enable cut node"); - expect(Tuning::setSearchParameter("SearchIIRAllNode", 1), "enable all node"); - expect(reduce(4, 2, true, false, false, false) == 3, "tuned PV reduction"); - expect(reduce(4, 2, false, true, false, false) == 3, "tuned cut reduction"); - expect(reduce(4, 2, false, false, false, false) == 3, "tuned all reduction"); - expect(reduce(2, 2, true, false, false, false) == 0, "under tuned minimum"); + expect(reduce(4, false) == 3, "configurable reduction"); + expect(reduce(4, true) == 0, "TT move suppresses tuned reduction"); - expect(Tuning::setSearchParameter("SearchIIRMinDepth", 2), "low min depth accepted"); - expect(reduce(2, 1, true, false, false, false) == 1, - "reduction clamps to keep at least one depth"); - expect(Tuning::setSearchParameter("SearchIIRReduction", 0), "disable via zero"); - expect(reduce(9, 2, true, false, false, false) == 0, - "zero reduction disables feature"); + expect(Tuning::setSearchParameter("SearchIIRMinDepth", 2), "accept min depth 2"); + expect(reduce(2, false) == 1, "clamp keeps at least one depth"); + expect(Tuning::setSearchParameter("SearchIIRReduction", 0), "disable IIR"); + expect(reduce(9, false) == 0, "zero reduction disables feature"); - expect(!Tuning::setSearchParameter("SearchIIRMinDepth", 1), "reject shallow min"); - expect(!Tuning::setSearchParameter("SearchIIRMinDepth", 33), "reject excessive min"); + expect(!Tuning::setSearchParameter("SearchIIRMinDepth", 1), "reject min depth 1"); + expect(!Tuning::setSearchParameter("SearchIIRMinDepth", 33), "reject min depth 33"); expect(!Tuning::setSearchParameter("SearchIIRReduction", -1), "reject negative reduction"); - expect(!Tuning::setSearchParameter("SearchIIRReduction", 5), "reject excessive reduction"); - expect(!Tuning::setSearchParameter("SearchIIRCutNode", 2), "reject invalid cut switch"); - expect(!Tuning::setSearchParameter("SearchIIRAllNode", -1), "reject invalid all switch"); + expect(!Tuning::setSearchParameter("SearchIIRReduction", 5), "reject oversized reduction"); + expect(!Tuning::setSearchParameter("SearchIIRCutNode", 1), "obsolete cut knob rejected"); + expect(!Tuning::setSearchParameter("SearchIIRAllNode", 1), "obsolete all knob rejected"); auto engine = std::make_unique(); std::ostringstream out; expect(Tuning::handleSetOption("name SearchIIRMinDepth value 7", *engine, out), - "IIR option processed through UCI"); - expect(Tuning::search().iirMinDepth == 7, "IIR UCI option stores value"); + "UCI setoption recognized"); + expect(Tuning::search().iirMinDepth == 7, "UCI changes IIR minimum depth"); Tuning::printUciOptions(out); Tuning::printCurrent(out); expect(out.str().find("option name SearchIIRMinDepth type spin default 6 min 2 max 32") - != std::string::npos, "min depth advertised"); + != std::string::npos, "min-depth UCI option advertised"); expect(out.str().find("option name SearchIIRReduction type spin default 1 min 0 max 4") - != std::string::npos, "reduction advertised"); - expect(out.str().find("option name SearchIIRCutNode type spin default 0 min 0 max 1") - != std::string::npos, "cut node switch advertised"); - expect(out.str().find("option name SearchIIRAllNode type spin default 0 min 0 max 1") - != std::string::npos, "all node switch advertised"); + != std::string::npos, "reduction UCI option advertised"); + expect(out.str().find("option name SearchIIRCutNode type spin") == std::string::npos, + "cut switch not advertised"); + expect(out.str().find("option name SearchIIRAllNode type spin") == std::string::npos, + "all switch not advertised"); expect(out.str().find("SearchIIRMinDepth=7") != std::string::npos, - "tuneparams includes IIR parameter"); + "tuneparams reports active depth"); Tuning::resetSearch(); expect(Tuning::search().iirMinDepth == defaults.iirMinDepth - && Tuning::search().iirReduction == defaults.iirReduction - && Tuning::search().iirCutNode == defaults.iirCutNode - && Tuning::search().iirAllNode == defaults.iirAllNode, - "reset restores all four IIR defaults"); + && Tuning::search().iirReduction == defaults.iirReduction, + "reset restores both IIR defaults"); if (!failures) std::cout << "Internal iterative reduction tuning: PASS\n"; return failures ? 1 : 0; From 31061e638f578fb6b989760c53eb4ede9ce53890 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 23 Oct 2026 06:45:10 -0500 Subject: [PATCH 10/11] Tests: restore IIR helper and close anonymous namespace after simplification --- tests/iir.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/iir.cpp b/tests/iir.cpp index 10b4615..9213cd7 100644 --- a/tests/iir.cpp +++ b/tests/iir.cpp @@ -15,6 +15,12 @@ int failures = 0; void expect(bool passed, const char* name) { if (!passed) { std::cerr << "FAIL: " << name << '\n'; ++failures; } } + +int reduce(int depth, bool hasTTMove) { + return IIR::reduction(depth, hasTTMove, Tuning::search()); +} +} // namespace + int main() { Geometry::init(); Tuning::resetSearch(); From 06d33ddc8bb8213b88892d9e01a15a85ba6d1622 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 23 Oct 2026 07:07:39 -0500 Subject: [PATCH 11/11] Search: remove IIR reduction count diagnostics from UCI output Drop the reduction counter and end-of-search info strings for normal and MultiPV while retaining IIR behavior, tuned parameters, and regression coverage. --- docs/internal-iterative-reduction.md | 3 --- include/ironphoenix/search.hpp | 1 - src/multipv.cpp | 2 -- src/search.cpp | 7 +------ tests/mate_stop.cpp | 4 ++-- 5 files changed, 3 insertions(+), 14 deletions(-) diff --git a/docs/internal-iterative-reduction.md b/docs/internal-iterative-reduction.md index 2c28867..1abbddc 100644 --- a/docs/internal-iterative-reduction.md +++ b/docs/internal-iterative-reduction.md @@ -22,9 +22,6 @@ also removed from `negamax`. All other tuned search and evaluation values are unchanged. A safety clamp keeps the remaining depth >= 1, even when tuning a large reduction. -The engine emits `info string iir reductions N` before the bestmove -for each normal/MultiPV search. This counts applied depth reductions. - **Important:** Applying IIR during checks, at the root and at non-PV nodes can reduce tactical accuracy and playing strength. Run independent paired SPRT before merging this aggressive variant into `main`. diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 00cec82..6a9a22d 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -97,7 +97,6 @@ namespace ironphoenix { std::int64_t hardTimeBudgetMs_ = 0; bool pondering_ = false; std::uint64_t nodes_ = 0; - std::uint64_t iirReductions_ = 0; std::uint64_t nextTimeCheckNode_ = 0; int selDepth_ = 0; diff --git a/src/multipv.cpp b/src/multipv.cpp index c15c52d..428c26f 100644 --- a/src/multipv.cpp +++ b/src/multipv.cpp @@ -109,7 +109,6 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out) hardTimeBudgetMs_ = timeBudget.hardMs; pondering_ = limits_.ponder; nodes_ = 0; - iirReductions_ = 0; nextTimeCheckNode_ = 0; selDepth_ = 0; @@ -296,7 +295,6 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out) break; } - out << "info string iir reductions " << iirReductions_ << '\n'; emitBestMove(out, bestMove, bestPonderMove); } diff --git a/src/search.cpp b/src/search.cpp index 4a02138..a7a9796 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -810,10 +810,8 @@ namespace ironphoenix { // once the configured depth is reached without a TT ordering move. // The helper clamps the reduction to keep depth >= 1. const int iir = IIR::reduction(depth, static_cast(ttMove), tuning); - if (iir > 0) { + if (iir > 0) depth -= iir; - ++iirReductions_; - } MoveList moves; generatePseudoLegalMoves(pos, moves); @@ -1112,7 +1110,6 @@ namespace ironphoenix { hardTimeBudgetMs_ = timeBudget.hardMs; pondering_ = limits_.ponder; nodes_ = 0; - iirReductions_ = 0; nextTimeCheckNode_ = 0; selDepth_ = 0; for (auto& row : pv_) @@ -1312,8 +1309,6 @@ namespace ironphoenix { break; } - // One diagnostic per search, not per node: negligible hot-path cost. - out << "info string iir reductions " << iirReductions_ << '\n'; emitBestMove(out, bestMove, bestPonderMove); } diff --git a/tests/mate_stop.cpp b/tests/mate_stop.cpp index 91aae06..afcc79b 100644 --- a/tests/mate_stop.cpp +++ b/tests/mate_stop.cpp @@ -85,8 +85,8 @@ void testRootMate(bool multiPV) { expect(completed, multiPV ? "MultiPV returned promptly on mate" : "root search returned promptly on mate"); expect(text.find("score mate 1") != std::string::npos, "engine reports winning mate in one"); expect(text.find("bestmove d7a7") != std::string::npos, "engine returns king-capturing move"); - expect(text.find("info string iir reductions ") != std::string::npos, - "search reports actual applied IIR reductions"); + expect(text.find("info string iir reductions ") == std::string::npos, + "search does not emit IIR diagnostic reporting"); expect(text.find("info depth 2 ") == std::string::npos, "no further deepening after winning mate"); if (multiPV) { expect(text.find("multipv 1 ") != std::string::npos, "MultiPV top line emitted");