diff options
Diffstat (limited to 'src/lmr_testing')
| -rw-r--r-- | src/lmr_testing/README.md | 73 | ||||
| -rw-r--r-- | src/lmr_testing/generate_counter_move_block.c | 58 | ||||
| -rw-r--r-- | src/lmr_testing/search_c_snippets.txt | 33 | ||||
| -rw-r--r-- | src/lmr_testing/searchsup_GetLMRReduction.c | 114 |
4 files changed, 278 insertions, 0 deletions
diff --git a/src/lmr_testing/README.md b/src/lmr_testing/README.md new file mode 100644 index 0000000..8ac983f --- /dev/null +++ b/src/lmr_testing/README.md @@ -0,0 +1,73 @@ +# In-flight LMR/counter-move work, stashed 2026-08-27 + +The main tree (`/usr/home/scott/typhoon/src`) was reverted to a clean +pre-LMR/pre-counter-move baseline (LMR=fixed -ONE_PLY history pruning, +futility restored, no counter-move scoring) so an overnight ECM baseline +run could establish ground truth (`typhoon_baseline.log` @ 20s/move, +`typhoon_baseline_ecm4m.log` @ sn=4M, both 1cpu/256m hash). + +This directory holds the exact bodies that were reverted, so they can be +re-applied on top of the (soon to be committed) clean baseline instead of +reconstructing from memory. + +## What's still intact in the main tree (never touched) + +- `dynamic.c`: `InitLMRTable()`, `g_iLMRQuietReduction` table population, + and the counter-move table write (`_NewCounterMove`, still runs and + updates `u64CounterMoveTries`/`u64CounterMoveHits` stats -- just + nothing reads `ctx->mvCounter` for move ordering/reduction anymore). +- `main.c`: still calls `InitLMRTable()`. +- `chess.h`: still has all struct fields (`mvCounter`, `fPvNode`, + `g_iLMRQuietReduction` extern, `GetLMRReduction` prototype, counter-move + bit flags, stats counters). No chess.h changes needed to restore LMR. + +## What needs restoring (saved in this directory) + +- `searchsup_GetLMRReduction.c` -- the graded-LMR body: Ethereal formula + (`0.7844 + ln(depth)*ln(moves)/2.4696`) + `ONE_PLY` base, soft + PV-adjacency discount (Crafty-style, 1 ply less instead of hard skip + when `ctx->sPlyInfo[ctx->uPly-1].fPvNode`), counter-move exemption + (added last, alongside killer exemptions), move-count threshold `> 3`, + fail-high gate `<= 10`. This is the *best validated* config from + yesterday's sweep: 24/30 solved, EBF 4.223 on `ecm_quick.ep_` @ sn=4M. + Drop this in to replace `GetLMRReduction` in `searchsup.c`. + +- `generate_counter_move_block.c` -- the three generate.c hunks: struct + decl (`sKillers[6]`, `mvLast`), the counter-move bonus pre-population + block, and the `s += sKillers[4]/[5]` scoring lines. See inline + `// LOCATION:` comments for where each piece goes. + +- `search_c_snippets.txt` -- the two one-line search.c changes: + `pi->fPvNode = (iBeta != iAlpha + 1);` (near `iInitialAlpha = iAlpha;`) + and the futility-pruning `FALSE &&` isolation-test disable (optional -- + only re-add if resuming the "isolate LMR's effect alone" testing + methodology; leave futility on to test LMR combined with it instead). + +## Known results/dead ends from yesterday's sweep (don't re-try blindly) + +See conversation history for full detail, but in brief, all measured on +`ecm_quick.ep_` @ sn=4M against this config's 24/30 EBF 4.223 baseline: +- Table-only (no `+ONE_PLY` base): 23/30, EBF 4.224 -- worse. +- Fail-high gate `<=5`: 24/30, EBF 4.274 -- worse. `<=20`: 23/30, EBF + 4.101 -- best EBF but costs a solve (same shape as several other + knobs -- EBF-vs-solve-count tradeoff, not a free win). +- Grandparent PV guard (uPly-2): 23/30, EBF 4.258 -- worse both ways. +- Obsidian formula (`0.99 + ln(d)*ln(m)/3.14`, table-alone): 23/30, EBF + 4.175 -- good EBF, costs a solve. +- "Improving" signal (Crafty/Berserk/SF-style, eval vs 2 plies ago), + tried with both `GetRoughEvalScore` (material-only past uPly 4) and a + real `Eval()` call: both measured identically worse, 23/30 EBF 4.252. + GetRoughEvalScore's material-only fallback deep in the tree was ruled + out as the cause since the real-Eval version scored the same. + +## The real methodology finding (more important than any single knob) + +ECM.016 case study: baseline's "stable" answer through depth 11 (`Rxc5`) +was actually a shallow, unconvicted pick -- at 24.5M+ nodes even the +baseline flips to `dxe3` and stays there (matches Crafty's own stable +depth-18-21 preference for `dxe3`). Full-ecm879 solve-count deltas from +sn=4M runs are contaminated by positions like this where neither config +actually understands the position yet. Before trusting any future +solve-count delta on a small suite, verify the "lost" positions are ones +where a long/deep baseline run is actually stable and correct -- that's +what tonight's overnight run is for (finding the "confident" ECM subset). diff --git a/src/lmr_testing/generate_counter_move_block.c b/src/lmr_testing/generate_counter_move_block.c new file mode 100644 index 0000000..f58efda --- /dev/null +++ b/src/lmr_testing/generate_counter_move_block.c @@ -0,0 +1,58 @@ +// generate.c changes to restore counter-move scoring. Three pieces, in +// the same function (the one with PRECOMP_KILLERS sKillers[...] and the +// "Pre-populate killer/bonuses" comment -- search for that to find it). + +// LOCATION 1: local var decls at top of the function -- add mvLast and +// pi, bump sKillers to 6: +// +// PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly]; // ADD +// ULONG uPly = ctx->uPly; +// POSITION *pos = &ctx->sPosition; +// ULONG u; +// MOVE mv; +// MOVE mvLast = (pi-1)->mv; // ADD +// SCORE s; +// MOVE_STACK_MOVE_VALUE_FLAGS mvf; +// ULONG uHashMoveLoc = (ULONG)-1; +// ULONG uColor = pos->uToMove; +// PRECOMP_KILLERS sKillers[6]; // was [4] +// COOR cEnprise = FindEnprisePiece(ctx, uColor); + +// LOCATION 2: right after the killer sKillers[0..3] population block +// (after the SORT_THESE_FIRST |= lines for sKillers[0..3]), insert: + + // + // Pre-populate counter-move bonuses -- keyed by whatever move the + // opponent just played to reach this node, not by ply. A/B test: + // applied as a small *additive* nudge (like history counters), + // not a hard priority-tier flag -- the ~56% measured hit rate + // isn't confident enough to justify unconditionally outranking + // ordinary PSQT-scored quiet moves. Bonus scales with the ply + // depth the entry was recorded at (deeper = more confident), + // capped at the same 400/200 ceiling the flat version used. + // + sKillers[4].mv.uMove = sKillers[5].mv.uMove = 0; + if (mvLast.uMove != 0) + { + u = MOVE_TO_INDEX(mvLast); + sKillers[4].mv = ctx->mvCounter[u][0]; + sKillers[4].uBonus = 400; + sKillers[5].mv = ctx->mvCounter[u][1]; + sKillers[5].uBonus = 200; + } + +// LOCATION 3: in the quiet-move scoring branch (the `else` branch that +// computes `s = g_iPSQT[...]` and applies killer bonuses via `s |= ...`), +// right after the GOOD_MOVE/cEnprise line and before `ASSERT(s >= 0);`, +// add (note: additive `+=`, not `|=` -- this was the measured-best +// config vs. a hard flag): + + s += (IS_SAME_MOVE(sKillers[4].mv, mv) * sKillers[4].uBonus); + s += (IS_SAME_MOVE(sKillers[5].mv, mv) * sKillers[5].uBonus); + +// This same three-part change applies in BOTH scoring functions in +// generate.c that have this sKillers array (there are two -- one for +// the normal move-scoring path, one for escaping-check; check whether +// the second one had the counter-move block too before assuming it's +// identical -- verify via `grep -n PRECOMP_KILLERS generate.c` and +// diff both call sites against this file's saved state if unsure). diff --git a/src/lmr_testing/search_c_snippets.txt b/src/lmr_testing/search_c_snippets.txt new file mode 100644 index 0000000..72d4ae5 --- /dev/null +++ b/src/lmr_testing/search_c_snippets.txt @@ -0,0 +1,33 @@ +search.c changes to restore (both trivial one-liners): + +1. Near `iInitialAlpha = iAlpha;` (right after it), add: + + pi->fPvNode = (iBeta != iAlpha + 1); + +2. OPTIONAL -- only if resuming "isolate LMR's effect alone" testing + methodology (i.e. you want LMR-only numbers uncontaminated by + futility pruning again). If instead you want to test LMR *combined* + with futility (recommended next step per yesterday's session), skip + this and leave futility on as it is in the clean baseline. + + Find the futility-pruning block: + + ASSERT(!uFutilityMargin); + if ((iRoughEval + VALUE_ROOK <= iAlpha) && + (uDepth <= TWO_PLY) && + ... + + and disable it for isolation testing: + + ASSERT(!uFutilityMargin); + if (FALSE && // temporarily disabled to isolate graded-LMR's + // effect in isolation during testing + (iRoughEval + VALUE_ROOK <= iAlpha) && + (uDepth <= TWO_PLY) && + ... + +Everything else in search.c (the GetLMRReduction call site, the +re-search-add-back using `uNextDepth -= iExtend`) is already generic +and needs NO changes -- it was written to work with any reduction +magnitude GetLMRReduction returns, so it already works correctly with +both the baseline's fixed -ONE_PLY and the graded version. diff --git a/src/lmr_testing/searchsup_GetLMRReduction.c b/src/lmr_testing/searchsup_GetLMRReduction.c new file mode 100644 index 0000000..0ee69f6 --- /dev/null +++ b/src/lmr_testing/searchsup_GetLMRReduction.c @@ -0,0 +1,114 @@ +// Replace searchsup.c's GetLMRReduction body with this (signature/name +// unchanged, so search.c/split.c call sites need no changes). + +INT +GetLMRReduction(IN SCORE iRoughEval, + IN SCORE iAlpha, + IN SCORE iBeta, + IN SEARCHER_THREAD_CONTEXT *ctx, + IN ULONG uRemainingDepth, + IN ULONG uLegalMoves, + IN MOVE mv, + IN ULONG uMoveNum, + IN INT iExtend) +/** + +Routine description: + + Decide how much (if any) to reduce this move's search depth by -- + graded LMR, replacing the old fixed -ONE_PLY history pruning. + Note: this function is called after the move has been played on + the board (ctx->uPly is already the *child's* ply; ctx->uPly - 1 + is the node whose move loop we're in, i.e. the parent of the + search we're about to reduce). + + PV-parent protection: if the node whose move this is (uPly - 1) + was itself reached with a wide window (a genuine PV node, not + just non-null-window), don't reduce at all here. This is + deliberately about the *parent*, not "am I a PV node myself" -- + every non-first move at a PV node is searched null-window + regardless (standard PVS), so that alone doesn't distinguish + "one ply below a real PV" from "deep inside an already-non-PV + subtree". Confirmed empirically (not just theoretically) to + matter: without this, graded LMR measured worse than baseline; + with it, break-even. Grandparent protection (uPly - 2) was + tried and measured worse (23/30, EBF 4.258 vs this config's + 24/30, EBF 4.223 on ecm_quick.ep_ @ sn=4M) -- not worth it. + +Parameters: + + SEARCHER_THREAD_CONTEXT *ctx, + ULONG uRemainingDepth, + ULONG uLegalMoves, + MOVE mv, + INT iExtend + +Return value: + + INT : 0 if no reduction, else a negative ply-fraction (ONE_PLY + units) suitable for adding directly into iExtend. + +**/ +{ + ULONG uDepthPly, uMoveIdx; + INT iReduction; + + ASSERT(ctx->uPly > 0); + ASSERT(mv.uMove); + ASSERT((uMoveNum > 0) || (uLegalMoves == 0)); + if ((uRemainingDepth >= TWO_PLY) && + (iBeta == (iAlpha + 1)) && + (uLegalMoves > 3) && + (0 == iExtend) && + (!IS_ESCAPING_CHECK(mv)) && + (!IS_CAPTURE_OR_PROMOTION(mv)) && + (!IS_CHECKING_MOVE(mv)) && + (!IS_SAME_MOVE(mv, ctx->mvKiller[ctx->uPly-1][0])) && + (!IS_SAME_MOVE(mv, ctx->mvKiller[ctx->uPly-1][1])) && + ((ctx->uPly < 3) || + (!IS_SAME_MOVE(mv, ctx->mvKiller[ctx->uPly-3][0]) && + !IS_SAME_MOVE(mv, ctx->mvKiller[ctx->uPly-3][1]))) && + (!IS_SAME_MOVE(mv, ctx->mvCounter[MOVE_TO_INDEX(ctx->sPlyInfo[ctx->uPly-1].mv)][0])) && + (!IS_SAME_MOVE(mv, ctx->mvCounter[MOVE_TO_INDEX(ctx->sPlyInfo[ctx->uPly-1].mv)][1])) && + (GetMoveFailHighPercentage(mv) <= 10)) + { + ASSERT(!InCheck(&ctx->sPosition, ctx->sPosition.uToMove)); + uDepthPly = MINU(uRemainingDepth / ONE_PLY, MAX_PLY_PER_SEARCH); + uMoveIdx = MINU(uLegalMoves, LMR_TABLE_MAX_MOVES); + + // Base reduction (matches the old fixed -ONE_PLY history-pruning + // behavior) plus a graded extra on top from the table, same + // structure as the prior validated implementation. + iReduction = -(ONE_PLY + g_iLMRQuietReduction[uDepthPly][uMoveIdx]); + + // NOTE: tried an "improving" signal here (Crafty/Berserk/SF-style, + // comparing static eval to 2 plies ago), both with + // GetRoughEvalScore's material-only fallback and with a real + // Eval() call (LAZY_EVAL-fast-pathed) feeding pi->iEval -- both + // measured identically worse (23/30, EBF 4.252 vs this config's + // 24/30, EBF 4.223). The signal itself isn't paying off here, not + // just the eval-quality proxy; not worth pursuing further without + // a different formulation. + + // Soft PV-adjacency discount (Crafty-style): if the parent node + // (whose move loop we're in) was itself a genuine PV node, reduce + // one ply less rather than skipping the reduction outright. + if (TRUE == ctx->sPlyInfo[ctx->uPly - 1].fPvNode) + { + iReduction += ONE_PLY; + if (iReduction > 0) iReduction = 0; + } + if (0 == iReduction) return(0); + + // Never reduce past leaving less than TWO_PLY of remaining depth -- + // a reduced child must still get a real search, not be treated as + // a leaf/qsearch node by accident. + if ((INT)uRemainingDepth + iReduction < TWO_PLY) + { + iReduction = -MAX((INT)uRemainingDepth - TWO_PLY, 0); + } + ASSERT(iReduction <= 0); + return(iReduction); + } + return(0); +} |
