From 0b12137376929d96da81cd088d3d288cb1eec32d Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 29 Aug 2026 15:03:54 -0700 Subject: Dynamic move ordering overhaul: continuation-history, evidence-gated countermove promotion, retired hung-piece-escape and NumLeftoverMovesToSelect. Full session was built on a "measure the pick, not the game" methodology: aggregate solve counts on curated suites are too noisy to tune move-ordering knobs against, so most decisions here came from per-move fail-high/alpha-raise rates at much larger sample sizes (leftover FH% instrumentation, a zero- selection-budget diagnostic that isolates a single best-of-remaining pick, and evidence-bucket calibration), not solve-count deltas alone. See CLAUDE.md's "Dynamic move ordering experiments" section for the reusable methodology and generate.c's _ScoreAllMoves comment for the resulting ordering hierarchy. Changes: - Added g_ContinuationHistory: same growth/decay math as the existing g_HistoryCounters butterfly table, additionally keyed by the previous move, so its magnitude is self-calibrated rather than a hand-picked constant. Flat, sufficient response across a 256x scale sweep. - Countermove-table matches now get a real GOOD_MOVE-tier promotion (previously the table was write-only, tracked for stats but never read for ordering), but only when the match's own accumulated history+continuation evidence clears COUNTERMOVE_EVIDENCE_THRESHOLD (10,000) -- a raw match with no track record was shown to perform identically to an ordinary leftover (~0.6-0.85% FH), so promoting on match alone would have repeated hung-piece-escape's mistake below. - Retired hung-piece-escape's unconditional GOOD_MOVE-tier promotion. Evidence-calibration showed the overwhelming majority of triggers (a zero-evidence population 250-1000x larger than countermove's) performed at the plain-leftover baseline -- the promotion was mostly free tier- escape treatment for moves that hadn't earned it. Replaced with FLEE_BONUS, a flat same-tier nudge inside SelectBestWithHistory (never escapes GOOD_MOVE/leftover classification, unlike a generation-time promotion) at the magnitude found to plateau a same-tier-nudge sweep. - Retired NumLeftoverMovesToSelect (the depth-indexed budget on how many leftover moves got a full selection scan before falling back to unsorted order). search.c's main move loop now always fully selects -- the leftover pool was shown to contain real, findable signal a bailout budget was discarding for a node-count savings that didn't hold up net- net once measured by solve counts and fail-high rates rather than raw node counts (noisy on small suites independent of this change). - Collapsed leftover-move instrumentation from sorted/raw pairs down to a single set now that "raw" (unsorted fallback) is structurally impossible; kept the countermove evidence-bucket calibration counters (ongoing check that COUNTERMOVE_EVIDENCE_THRESHOLD stays well- calibrated); removed the contested-node A/B harness and hung-piece evidence calibration now that the decisions they were built to inform are made. Net effect on the three curated suites (sd 10): solve counts wash (tied, +1, -1 across ringers/confident/hard), leftover fail-high rate improved consistently on all three (the intended, directly-measured target of this work). Not yet validated beyond sd 10 -- an sn-based run or eval_tune/match_play.py head-to-head gate is the natural next check before leaning on this as a proven strength gain rather than a directionally- sound, sd-10-clean change. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014XePz6Sk4qQsTaP2jVJWJu --- src/split.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) (limited to 'src/split.c') diff --git a/src/split.c b/src/split.c index 17b77f6..8bc4e60 100755 --- a/src/split.c +++ b/src/split.c @@ -604,6 +604,9 @@ Return value: g_SplitInfo[u].sCounters.tree.u64TotalNodeCount = 0; g_SplitInfo[u].sCounters.tree.u64BetaCutoffs = 0; g_SplitInfo[u].sCounters.tree.u64BetaCutoffsOnFirstMove = 0; + g_SplitInfo[u].sCounters.tree.u64LeftoverTries = 0; + g_SplitInfo[u].sCounters.tree.u64LeftoverAlpha = 0; + g_SplitInfo[u].sCounters.tree.u64LeftoverFH = 0; g_SplitInfo[u].PV[0] = NULLMOVE; // @@ -628,9 +631,10 @@ Return value: // work for naught. We also want to know as soon as // possible so that we can vacate this split point, // free up a worker thread and get back to the main - // search. So forget about the leftover-selection - // budget (NumLeftoverMovesToSelect in search.c) here - // and sort the whole list of moves from best..worst. + // search. So sort the whole list of moves from + // best..worst (search.c's main loop does the same + // unconditionally now too -- see its retired + // NumLeftoverMovesToSelect budget comment). // SelectBestWithHistory(ctx, v); ctx->sMoveStack.mvf[v].mv.bvFlags |= @@ -761,6 +765,12 @@ Return value: g_SplitInfo[u].sCounters.tree.u64BetaCutoffs; ctx->sCounters.tree.u64BetaCutoffsOnFirstMove = g_SplitInfo[u].sCounters.tree.u64BetaCutoffsOnFirstMove; + ctx->sCounters.tree.u64LeftoverTries = + g_SplitInfo[u].sCounters.tree.u64LeftoverTries; + ctx->sCounters.tree.u64LeftoverAlpha = + g_SplitInfo[u].sCounters.tree.u64LeftoverAlpha; + ctx->sCounters.tree.u64LeftoverFH = + g_SplitInfo[u].sCounters.tree.u64LeftoverFH; #endif // // Pop off the split info ptr from the stack in the thread's @@ -932,8 +942,14 @@ Return value: ctx->sCounters.tree.u64TotalNodeCount; g_SplitInfo[u].sCounters.tree.u64BetaCutoffs += ctx->sCounters.tree.u64BetaCutoffs; - g_SplitInfo[u].sCounters.tree.u64BetaCutoffsOnFirstMove += + g_SplitInfo[u].sCounters.tree.u64BetaCutoffsOnFirstMove += ctx->sCounters.tree.u64BetaCutoffsOnFirstMove; + g_SplitInfo[u].sCounters.tree.u64LeftoverTries += + ctx->sCounters.tree.u64LeftoverTries; + g_SplitInfo[u].sCounters.tree.u64LeftoverAlpha += + ctx->sCounters.tree.u64LeftoverAlpha; + g_SplitInfo[u].sCounters.tree.u64LeftoverFH += + ctx->sCounters.tree.u64LeftoverFH; // // TODO: Any other counters we care about? -- cgit v1.3