diff options
| author | Scott Gasch <[email protected]> | 2026-08-27 07:41:41 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-08-27 07:42:25 -0700 |
| commit | 4ce6a76b0946e4ba943d29c506c9e2fb00601efd (patch) | |
| tree | b072c6fe2a5e8527a28f5c821d76591e93fa86f6 /src/lmr_testing | |
| parent | 7857096f39e16619a42ee85b4aa593abd846b74a (diff) | |
Baseline: uPositional data-calibrated fix, enprise/trapped hints, EBF/beta-cutoff/counter-move stats, script.c FPE fix.
No LMR, no counter-move-driven move ordering (both explored separately,
kept out for now -- counter-move measured worse, ~655->647 solved on
ecm879 @ sn=4M with a leaner tree beforehand). Futility pruning restored.
Verified: 647/879 solved, EBF 4.609 @ sn=4M; 684/879 solved, EBF 3.995
@ 20s/move, 1cpu, 256m hash (typhoon_baseline.log).
The counter-move table is still written and its stats still tracked
(dynamic.c) for diagnostic purposes, but generate.c no longer reads it
for move ordering, so it has no effect on search behavior in this
commit.
lmr_testing/ holds the in-flight graded-LMR + counter-move code (not
applied here) with notes on what was already tried and measured, so a
future session can resume without re-deriving it.
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); +} |
