diff options
| author | Scott Gasch <[email protected]> | 2026-10-04 08:17:52 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-04 08:17:52 -0700 |
| commit | bbdf1bda8f410df6aa9be0a38e71b7db04982198 (patch) | |
| tree | e8315247f700ee94875cd6d2eb5dd81aef71395d /src/lmr_testing/2026-10-03_UNCOMMITTED_TRIAGE.md | |
| parent | 9b28080f8b3dca4588cb2aaaf748451f45e3c626 (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.md | 137 |
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. |
