summaryrefslogtreecommitdiff
path: root/src/lmr_testing
diff options
context:
space:
mode:
Diffstat (limited to 'src/lmr_testing')
-rw-r--r--src/lmr_testing/README.md73
-rw-r--r--src/lmr_testing/generate_counter_move_block.c58
-rw-r--r--src/lmr_testing/search_c_snippets.txt33
-rw-r--r--src/lmr_testing/searchsup_GetLMRReduction.c114
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);
+}