summaryrefslogtreecommitdiff
path: root/src/lmr_testing/2026-10-03_UNCOMMITTED_TRIAGE.md
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-04 08:17:52 -0700
committerScott Gasch <[email protected]>2026-10-04 08:17:52 -0700
commitbbdf1bda8f410df6aa9be0a38e71b7db04982198 (patch)
treee8315247f700ee94875cd6d2eb5dd81aef71395d /src/lmr_testing/2026-10-03_UNCOMMITTED_TRIAGE.md
parent9b28080f8b3dca4588cb2aaaf748451f45e3c626 (diff)
Dynamic move-ordering/LMR session: honest FH tracking, decoupled admission/magnitude, bugfixes, tooling
Lands the 2026-09-28..10-03 move-ordering investigation (full history in lmr_testing/*.md). Summary of what ships: LMR/move-ordering redesign (searchsup.c, dynamic.c, data.c, chess.h): - GetLMRReduction's admission decision (do we reduce at all) is now decoupled from magnitude (how much): LMR_STATIC_ADMIT_THOUSANDTHS=500 gates admission via the 4D joint fail-high table (data.c) plus a countermove-match veto; magnitude is a flat Crafty/Ethereal-shaped log(depth)*log(moves) table (g_iLMRQuietReduction), which measured as good or better than the old per-tier joint-table payout while being structurally immune to the per-bucket miscalibration that sank the tiered approach. g_FailHighs honesty fix (real 0/0 init, not a fake prior) underlies the dynamic-danger signal. - Retired NumLeftoverMovesToSelect's depth-indexed selection budget -- evidence showed the leftover pool has real findable signal a bailout was discarding. - ShouldLatePrune (overt late-move pruning) implemented but disabled at its call site -- measured net-negative at the tested threshold; kept for reference and as a base for a future hang-bucket-9-targeted retry. - PRUNE_VERIFY, PRUNE_LIVE, DISABLE_LMR: opt-in diagnostic build flags (GNUmakefile), off in the standard profile. Bug fixes found and fixed while exercising this work: - JOINT_EXTRA_TIER_COUNT 4->6 (chess.h): fixed an out-of-bounds counter write reachable at remaining depth >= 14 (unreachable at sd10, routine in real search/self-play). - Node-type instrumentation (search.c, root.c, searchsup.c): fixed four compounding defects that produced impossible percentages (e.g. 106.2% of PV nodes) -- wrong denominator, qsearch misclassified as main search, untyped early-exit nodes, and fail-low tests using the in-place-raised alpha instead of the initial one. - FH-rate unit unification (search.c, searchsup.c, dynamic.c, chess.h): GetMoveFailHigh* returned per-mille while g_uJointFHThousandths is thousandths-of-a-percent, two different units sharing a "_THOUSANDTHS" suffix. Unified to one scale, renamed to "_PCT_THOUSANDTHS". RateBucket's band edges fixed to match. - QSearch option B (search.c): waives the opponent-stand-pat requirement at NODE_PV only (sound there since the exact score propagates; not sound at null-window nodes). iExtensionAmount unit fix (was 0/2 into a ONE_PLY-scaled field). pi->iEval now published when Eval() runs. - eval_tune/match_play.py local_ebf: was reporting 0.000 in every match since 2026-09-18 (regex only matched the trailing-stats parser, not the live move-scan loop; engines were also spawned with "nopost" so root.c never emitted the lines at all). True per-ply EBF is ~2.0, not the ~3.9 every prior match report implied. - eval_tune/test_vs_head.sh: positional-arg shift ordering bug caused every no-extra-args run to silently rebuild ./typhoon with a non-standard profile and, separately, could feed a flag's value in as the candidate binary path. - PERF_COUNTERS cleanup (eval.c, search.c): moved 3 leaked rdtsc sites to the existing EVAL_TIME guard, deleted two concluded one-off diagnostics (u64HeadStyle*, u64FHJointHist/RateBucket/AttemptsBucket) and their report plumbing, and re-gated a per-move hang-material estimate behind FH_TABLE_COLLECTION instead of running it unconditionally. Infrastructure: - match_play.py time-odds support (--odds/--st-head/--st-candidate), validated against the engine seat rather than color. - Wilson-upper-bound re-bake of the joint table (data.c): cells store a confidence-aware bound instead of a raw point estimate, so a consumer can threshold on the value alone; monotone, so every consumer became strictly more conservative. Verified behavior-neutral for LMR. Known-open items (not resolved here, see lmr_testing/GET_LMR_REDUCTION.md section 10 and 2026-10-03_UNCOMMITTED_TRIAGE.md): the self-play gate on this candidate is underpowered (+5 Elo, 95% CI roughly [-6, +17]); the curated-suite solve-count deltas used earlier in the investigation turned out to be noise-dominated cancellation when diffed per position, not the clean signal they first appeared to be. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/lmr_testing/2026-10-03_UNCOMMITTED_TRIAGE.md')
-rw-r--r--src/lmr_testing/2026-10-03_UNCOMMITTED_TRIAGE.md137
1 files changed, 137 insertions, 0 deletions
diff --git a/src/lmr_testing/2026-10-03_UNCOMMITTED_TRIAGE.md b/src/lmr_testing/2026-10-03_UNCOMMITTED_TRIAGE.md
new file mode 100644
index 0000000..f379e96
--- /dev/null
+++ b/src/lmr_testing/2026-10-03_UNCOMMITTED_TRIAGE.md
@@ -0,0 +1,137 @@
+# Uncommitted-change triage, 2026-10-03 EOD
+
+`git status` shows 15 modified files, **5,313 insertions / 194 deletions**.
+That spans several sessions, not just today (date markers in the added lines:
+33 from 2026-10-03, 24 from 10-02, 21 from 09-28, plus 09-29/09-30/10-01).
+This file sorts it into KEEP / DECIDE / DISCARD so a commit can be assembled
+without re-deriving the reasoning.
+
+Nothing here is committed. Evidence for every claim is in
+`GET_LMR_REDUCTION.md` section 10 and `SPEC.md`.
+
+---
+
+## KEEP -- unambiguous, no open questions
+
+### Bug fixes (all found by exercising code, all verified)
+
+| what | file | why it is solid |
+|---|---|---|
+| `JOINT_EXTRA_TIER_COUNT` 4 -> 6 | chess.h | `JOINT_EXTRA_MAX_PLIES` is 5, so `*puExtraTier` could be 4 or 5 and both consumers indexed 4-element arrays out of bounds -- into `u64JointExtraResearch[0]` and into `u64LMPPruned`. Unreachable at sd10 (needs remaining depth >= 14), live in any real search. Counters-only, but it corrupted the exact calibration diagnostic candidates get judged on. 10.4 |
+| Node-type surprise line: 4 defects | search.c, root.c, searchsup.c | (1) denominator was total nodes not per-type; (2) qsearch included, ~84% of the tree, so a stand-pat cutoff read as "ALL failed high"; (3) `CommonSearchInit` early exits reached the counters un-typed and scored against a stale `eNodeType` -- produced an impossible "106.2% of PV nodes"; (4) fail-low test used the IN-PLACE-RAISED `iAlpha`, so every successful alpha-raise counted as a fail-low (PV row read 99.1%). Now uses `iInitialAlpha`. 10.12f |
+| FH-rate unit unification | search.c, searchsup.c, dynamic.c, chess.h | `GetMoveFailHigh*` returned per-mille while `g_uJointFHThousandths` is thousandths-of-a-PERCENT -- 100x apart, same `_THOUSANDTHS` suffix. `ShouldLatePrune` held `LMP_DYNAMIC_MAX_THOUSANDTHS (1)` meaning 0.1% four lines from `LMP_STATIC_MAX_THOUSANDTHS (1)` meaning 0.001%. Every caller was individually correct; the naming made that luck. Converted the whole dynamic family to the table's unit, renamed to `..._PCT_THOUSANDTHS`. 10.8 |
+| `RateBucket` band edges x100 | searchsup.c | Collateral of the unit change: edges were per-mille, so every rate >= 0.1% collapsed into the top bucket and five middle bands read zero. Caught because the histogram printed only its first and last columns. |
+| `local_ebf` fix | eval_tune/match_play.py | Reported 0.000 in **every match since it was added (2026-09-18)**. Two causes: `_ITER_RE` was applied only in the trailing stats parser but the PV lines arrive during the search in the move-scan loop; and `Engine.__init__` sends `nopost`, so `root.c:248` never emitted them at all. Now parsed in both loops plus an opt-in `--post`. This is the metric that proved the published `ebf` (cumulative `nodes**(1/depth)`) has been wrong in every match report -- true per-ply ratio is **~2.0, not ~3.9**. 10.10c |
+| `test_vs_head.sh` shift ordering | eval_tune/test_vs_head.sh | Positionals were shifted BEFORE `$2` was read, so (a) every no-extra-args run took the rebuild branch and clobbered `./typhoon` with a non-standard profile (`gmake release MP=1 PERF_COUNTERS=1`, missing GENETIC/SIXTYFOUR), and (b) the first pass-through flag's VALUE became the candidate binary path -- producing a baffling `PermissionError` from Popen trying to execute a directory. Also `${VAR:-default}` -> `${VAR-default}` so an explicitly empty `SPRT_ARGS=""` is honored. |
+
+### PERF_COUNTERS cleanup (10.12g)
+The standard profile was carrying investigation debris that did real WORK, not
+counter increments.
+
+- **3 `rdtsc` sites moved to `EVAL_TIME`** (eval.c). Bracketed every
+ king-safety hash probe. An `EVAL_TIME` flag already existed for exactly this
+ and already guarded the other **32** rdtsc sites in the file -- these were
+ the only leaks. Report side in root.c was already EVAL_TIME-guarded.
+- **2 one-off diagnostics deleted outright**: `u64HeadStyle*` (called
+ `GetMoveFailHighRawCounts`) and `u64FHJointHist` (called
+ `GetMoveFailHighPctThousandths`). Both had comments saying to remove them
+ once their investigations concluded; both concluded, and both findings are
+ recorded (10.2's admission rates; section 4's bimodal dynamic rate). Removed
+ counters, `g_Options` mirrors, suite accumulation AND both report blocks --
+ a diagnostic printing zeros forever is worse than none. Binary -7,760 bytes.
+- **4D collection block re-gated to `FH_TABLE_COLLECTION`** (search.c). It
+ calls `EstimateHangingMaterialCp` per eligible move; CLAUDE.md already keeps
+ that flag out of the standard profile *because it costs nps*. Verified by
+ guard inspection: `search.c:1637` is now `PERF_COUNTERS > FH_TABLE_COLLECTION`.
+ It was also computing the hang bucket TWICE per move in any build with a
+ prune gate.
+
+**nps payoff NOT MEASURED** -- two attempts invalid (32-engine match on the
+box; one produced `perf_clean` faster than `noperf`, which is impossible).
+Re-measure idle, round-robin arms. The changes are justified on inspection
+regardless.
+
+### Infrastructure
+- **`match_play.py` time-odds support** (`--odds`, `--st-head`,
+ `--st-candidate`). Strict validation because a degraded odds match still
+ scores ~0.500 and looks healthy. Odds follow the ENGINE, not the seat --
+ verified live, since the harness swaps colours every other game, so keying on
+ White would silently balance the match. `TC=` stamped into the result line.
+ This is what produced the time-odds calibration.
+- **`PRUNE_VERIFY`** (GNUmakefile, search.c, chess.h, root.c, script.c).
+ Audit-only: evaluates the prune gate, records banded by predicted FH%, then
+ declines to prune and watches the move's own ordinary search. No shadow
+ `Search()` call, so no null-move-bailout exposure. Banding by the decision
+ variable answers "is `prune if pred <= K` sound" for every K in one run. This
+ is the instrument any future pruning work needs. 10.12h
+- **`DISABLE_LMR`** (GNUmakefile, searchsup.c). Diagnostic flag that produced
+ the control experiment. Keep the flag, never ship the configuration.
+
+### QSearch changes (10.12e) -- smoke-tested with asserts
+- **Option B**: `fIncludeChecks` waives the opponent-stand-pat requirement at
+ `NODE_PV`, keeping the material requirement. The suppression is sound for
+ null-window nodes (a fail-low there IS the parent's cutoff, so precision buys
+ nothing) but not for wide-window nodes, where the exact score propagates and
+ mate-vs-queen-loss is a different PV -- the forced-mate case the machinery
+ exists for. Reads `eNodeType`, never writes it.
+- **`iExtensionAmount` unit fix**: was written as 0 or 2 (qsearch check PLIES)
+ into a ONE_PLY-scaled field -- a latent 32x error, benign only because both
+ readers test sign.
+- **`pi->iEval` published** when the lazy `Eval()` actually runs.
+
+### Wilson re-bake (data.c, joint_table_collection/splice_joint.py) -- 10.11e
+Cells store the **Wilson upper bound** instead of the point estimate, so a
+cell's value encodes its own sample confidence and a consumer can threshold on
+the value ALONE. Under point estimates **8,248 of 21,780 cells read exactly 0**,
+mostly on little evidence, so any "prune where the table says 0" rule was far
+broader than the data supported; under Wilson, 136 do. Monotone (no cell's value
+is ever lowered), so every consumer became strictly more conservative.
+Verified behaviour-neutral for LMR: admission 92.79% vs 92.88% before, tier
+split and ringers solve count unchanged. `LMR_STATIC_ADMIT_PCT_THOUSANDTHS`
+stays at 500 -- it was briefly raised to 1000 on a CELL-count argument, which
+was wrong because cell counts are not traffic-weighted.
+
+---
+
+## DECIDE -- real, but with an open question
+
+| what | open question |
+|---|---|
+| **The 4D table itself** (data.c, 1,777 lines) + `joint_table_collection/` | Its only live shipping consumer is `LMR_STATIC_ADMIT_PCT_THOUSANDTHS`, which vetoes 2.3% of cell space / 7.4% of traffic -- and that threshold was chosen on solve-count deltas 10.6 showed were noise. Decisive cheap test: disable admission (`= 100000`) and run vs current; if neutral the table buys nothing in a shipping build. **But item 1 of 10.13 (give EFP the dumb axis) may give it a real consumer -- do that first.** |
+| **Graded LMR** (`LMR_STATIC_ADMIT`, flat-table magnitude, dynamic/countermove vetoes) | Worth ~8 Elo/ply marginal, i.e. a few Elo total, never cleanly self-play tested against flat -1 (10.12c). The `log(d)*log(m)` magnitude contributes mean **0.164 extra plies** because at remaining depth <= 2 the curve prescribes ~1 ply, which IS flat -1. Keep or simplify per the redesign in `2026-10-03_LMR_REDESIGN.md`. |
+| **`PRUNE_LIVE`** | Measured +0.88 ply, +7 Elo -- exactly its corrected ceiling. Keep the flag for measurement; do not ship the configuration. |
+| **`split.c` zero-snapshot passing** | `HelpSearch` passes all-zero attack snapshots to `GetLMRReduction`, so `EstimateHangingMaterialCp` degrades there. Documented and deliberate (every comparison protocol runs `--cpus 1`), but it means MP search reduces differently from single-threaded. Fine as long as that stays true. |
+
+---
+
+## DISCARD -- scratch that leaked into the tree
+
+Per CLAUDE.md's scratch policy these belong under `/tmp/typhoon/`, not `src/`:
+
+- `cdefault_games.pgn`, `chkdefault_games.pgn`, `codds_games.pgn`,
+ `dflt_games.pgn` -- from this session's `test_vs_head.sh` argument-construction
+ tests (GAMES=0/2 runs). **Delete.**
+- `kiwi_log.txt`, `eval_tune/kiwi_log.txt`, `eval_tune/eval_comp.txt`,
+ `blunders.pgn`, `players.dat`, `.xboardrc` -- untracked clutter from earlier
+ sessions; not mine to judge, but none look like deliberate artifacts.
+
+Deliberately-kept untracked directories (do NOT delete): `head_reference/`,
+`joint_table_collection/`, `nullmove_calib/`, `eval_tune/position_cache/`,
+`eval_tune/opening_cache/`.
+
+---
+
+## Suggested commit split
+
+1. **Bug fixes** -- OOB counter, node-type x4, unit unification, RateBucket,
+ `local_ebf`, `test_vs_head.sh` shift. Independently correct, no behaviour
+ bets. Run `./precommit_check.sh` first (search.c behaviour did change via
+ the QSearch work).
+2. **PERF_COUNTERS cleanup** -- after an idle-box nps measurement, so the
+ commit message can state what it bought.
+3. **Infrastructure** -- time-odds, `PRUNE_VERIFY`, `DISABLE_LMR`.
+4. **QSearch changes** -- option B et al. Smoke-tested; ideally also a short
+ self-play run, since option B is a real behaviour change never measured in
+ isolation.
+5. **Table + graded LMR + PRUNE_LIVE** -- hold until 10.13 items 1 and 5 are
+ resolved.