diff options
| author | Scott Gasch <[email protected]> | 2026-10-23 13:19:17 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-23 13:19:17 -0700 |
| commit | 69525eb2f42ea9cca8255f6cc3b85e6b1312fd05 (patch) | |
| tree | d78420b6c45d584a3f09b4dcbaf4f1edb36e8d5d /src/eval_tune | |
| parent | 6b883e83935f5e8110788565cfcb77cd6fbf8950 (diff) | |
NNUE search integration: FH% tables, NODE_TYPE refinement, TREEDUMP rewrite
- data.c/chess.h: widen the joint FH% table's late axis 16->17, dedicating
the new bucket 0 to a whole-node ("will something here fail high at
all") sample rather than sharing it with the first-leftover-move curve.
Add an NNUE-eval counterpart table (g_uJointFHThousandths_NNUE) and an
NNUE counterpart of the beta-side table, selected at runtime based on
whether NNUE eval is active.
- search.c/searchsup.c: add _RefineNodeTypeGuess, which uses the FH%
tables (via g_NodeTypeGuessTable) to refine a node's NODE_TYPE guess
instead of the old heuristic; thread uParentMoveFHEstimate through
PLY_INFO so a node can see its parent's prediction for the move that
led to it.
- generate.c: initialize sKillers[2]/[3].uBonus unconditionally (an
uninitialized-stack read caught by valgrind under LTO; harmless today
but a real bug).
- dumptree.c/chess.h/GNUmakefile: replace the long-dead DUMP_TREE XML
tree dump (written for a GUI this project no longer has) with TREEDUMP,
a flat, path-keyed move/result trace meant to diff against an
identically-instrumented Stockfish build -- see docs/TREEDUMP_FORMAT.md.
- docs/DEEPER_PRUNING.md: notes on closing the search-efficiency gap vs
Stockfish.
- crafty_egtb/stockfish_xboard/typhoon_nnue: wrapper scripts so
match_play.py's Engine class can drive Crafty, Stockfish (via
PolyGlot), and an NNUE-enabled typhoon the same way it drives HCE
typhoon.
- tests/lop: rename lop.txt -> lop.ep_; update compare_lop.cpp.
- annotate_pgns: add aggregate_marginsafety.py, dedup_pgn.py,
fit_lazy_probe.py; update annotate.py/run_nnue_pipeline.py for the NNUE
training pipeline.
- .gitignore: ignore compile_commands.json (clangd build DB) and
eval.nnue (a symlink into the untracked head_reference/ comparison
baseline), neither of which belongs in the repo.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/eval_tune')
| -rw-r--r-- | src/eval_tune/king_safety_data/HANDOFF.md | 254 |
1 files changed, 0 insertions, 254 deletions
diff --git a/src/eval_tune/king_safety_data/HANDOFF.md b/src/eval_tune/king_safety_data/HANDOFF.md deleted file mode 100644 index b05e3d2..0000000 --- a/src/eval_tune/king_safety_data/HANDOFF.md +++ /dev/null @@ -1,254 +0,0 @@ -# King-safety defect regression: handoff - -## Goal - -`_EvalKing` (eval.c) sums a pile of hand-picked "defect" constants into one -scalar (`uCounter`), then runs that through a single lookup table -(`KING_SAFETY_BY_COUNTER`). The hand-picked weights assume every defect -counts equally, which is suspect -- e.g. a queen touching an undefended -ring square and a knight touching one add the exact same `+1`. This -project mines real games to see which defects actually predict getting -mated, so `eval.c`'s weights can be informed by data instead of guesswork. - -**Method**: find real middlegame checkmates in human games, look at the -losing king's position N plies earlier, and compare its raw defect -pattern against a matched "sound" control position (drawn games) with the -same attacker material and development level. Fit a logistic regression; -features with a real, stable coefficient are candidates for reweighting -in `eval.c`. See `CLAUDE.md`'s "King-safety eval improvement project" -section (search for it) for the full narrative this file summarizes. - -## Where everything is - -- **Final training data**: `remine_v4/king_features_matched_v4.csv` -- - 86,362 rows (43,181 matched positive/control pairs). **Use this one.** -- `remine_v4/merged.csv` -- raw mined FENs before feature extraction - (172,735 rows: 43,181 positives + 129,554 control candidates). -- `remine_v4/king_features_v4.csv` -- all 172,735 rows with full - `_EvalKing` defect breakdown, before matching trims it down to the - 86,362 confound-controlled rows. -- Pipeline scripts live in `eval_tune/` (sibling of this dir), not here: - `mine_king_mates.py`, `shard_mine.py`, `extract_king_features.py`, - `match_controls.py`, `train_king_safety_model.py`. -- Engine side: `chess.h`'s `KING_DEFECT_DUMP_INFO` struct, - `_EvalKing`'s `#ifdef KING_DEFECT_DUMP` instrumentation in eval.c, and - command.c's `evalkingdump` command -- all gated behind the - `KING_DEFECT_DUMP=1` gmake build flag (NOT part of a normal build). - `../typhoon_kingdump` is a saved binary built with that flag; rebuild - it (`gmake clean` first -- flag changes don't trigger recompilation on - their own, see "Gotchas" below) if `eval.c`/`chess.h`/`command.c` - change. - -## Final result (from `king_features_matched_v4.csv`) - -Logistic regression, 80/20 train/test split, standardized coefficients. -Test AUC 0.91 (plausible for "engineered danger features vs. proven-sound -draws", not suspiciously perfect). Ranked by |coefficient|, most-to-least -trustworthy tier first (stability = full-population vs. top-tercile-army- -strength fits agreeing): - -**Tier 1 -- dominant, very stable**: `filestormopenfile` (+0.57 to +0.80), -`filestormpawn` (+0.51 to +0.54). These are eval.c's `KingFileDefects[3]` -and `KingStormingPawnDefects[8]` tables (open/half-open file near the -king; how far an enemy pawn has advanced up that file), already -independent tables -- lowest-risk place to start a real retune. - -**Tier 2 -- strong, stable**: `front_offboard`, `front_def`, `sq4_offboard`, -`front_undef`, `front_atk_minor`, `front_atk_rook`, `misscastleshort`, -`front_flight` (+0.33 to +0.45). "front" = the diagonal/vertical ring -squares one rank away, in the direction each side attacks toward (see -"Feature schema" below for the exact slot mapping -- do NOT assume this -means "in front of the king" the way a human would first guess; see the -correction note there). `sq3`/`sq4` are the two same-rank side squares, -kept individually resolved. - -**Tier 3 -- moderate, less stable**: `back_offboard` (drifts the most of -any surviving feature, treat with suspicion), `sq3_undef`, -`attackerarmyscaler`, `queenproximity`, `back_atk_minor`, `front_atk_queen`. - -**Confirmed noise -- do not retune based on this data**: `minorsathome` -(coefficient ~0.001, was previously the #1 "signal" before two sampling -bugs got fixed -- see "Bugs found" below), `initloc`, `castled` (exactly -0.0000, fully collinear with `misscastleshort`/`misscastlelong`). - -## Translating this into eval.c -- confidence and plan - -This is NOT a "throw away eval.c and use the model" situation. The plan -is targeted retuning of existing constants/tables, informed by relative -coefficient *ratios*, not literal log-odds-to-centipawn conversion: - -1. **`KingFileDefects`/`KingStormingPawnDefects` deserve a real weight - increase relative to everything else** -- they're the strongest - signal by a clear margin and already live in their own small, - isolated tables (`_ComputeFileStormDefects` in eval.c). Lowest risk, - highest expected payoff, start here. -2. **`EmptyAttackedSquare`/`EmptyUnattackedSquare`'s front/back asymmetry - is directionally right but probably too weak a spread** -- front- - bucket features consistently outrank back-bucket ones here by a good - margin. -3. **`misscastleshort`'s constant likely deserves a bump.** -4. **Don't touch `KING_INITIAL_COUNTER_BY_LOCATION` or the undeveloped- - minors term** based on this data -- both came back as noise. -5. Neither `KingFileDefects`/`KingStormingPawnDefects` nor the - ring-scan tables are currently `DNA_ARRAY`-registered (checked: only - `KING_SAFETY_BY_COUNTER` and `KING_QUEEN_PROXIMITY_DANGER` are under - king-safety). Retuning them means a manual edit (optionally promoting - them to file scope + registering as DNA arrays for future tuning), - not "rerun the tuner." -6. **Absolute magnitude has to be calibrated the normal way** -- run any - change through `sd10` ECM suite comparison against `head_reference/` - and `match_play.py`'s gated head-to-head match, same as any other - hand-tuned eval constant. The regression tells you *shape* - (which defects matter more relative to each other), not the right - centipawn magnitude for a chess engine's scoring scale. -7. `CountKingSafetyDefects` (the cheap lazy-eval proxy) stays untouched - until a real change lands and is validated -- agreed early in this - project, still the right call. - -## Feature schema (read this before extending the pipeline) - -`KingSafetyDeltas` in eval.c (`-17,-16,-15,-1,+1,+15,+16,+17`) defines 8 -ring slots, indices 0-7 in that order. **Correction made mid-project**: -slots 3 and 4 (deltas -1/+1) are the two SAME-RANK side squares, not -"front of the king" -- an earlier description in conversation got this -wrong. The diagonal/vertical slots (0,1,2 and 5,6,7) are the ones with a -real front/back asymmetry, matching `EmptyAttackedSquare`'s own existing -color-dependent weighting (WHITE's front is slots 0/1/2, BLACK's is -5/6/7 -- see `_EvalKing`'s comments and `KING_RING_FRONT`-style logic). - -`extract_king_features.py` dumps full per-slot resolution (all 8 slots x -4 piece types x several state flags) since the engine-side cost of doing -so is free once you're calling `evalkingdump` anyway. `train_king_safety_model.py` -is where slots 0/1/2/5/6/7 get bucketed into "front"/"back" sums -(color-dependent per row's `mated_color_or_sampled_color`), while slots 3/4 -stay individually resolved -- this bucketing was added in this session -after the diagonal per-slot coefficients turned out to be the noisiest, -most sign-flipping entries in every full-vs-subset drift comparison. -`filestorm` is deliberately NOT used as a training feature -- its two -independent sub-components (`filestormopenfile`, `filestormpawn`) are -used instead, since they map onto two already-separate eval.c tables and -there's no reason to make the regression re-derive their relative -importance from one pre-summed number. - -## Bugs found and fixed this session (read before trusting old data) - -If you find any `.csv` file in this project OTHER than -`remine_v4/*_v4.csv` and its merged.csv, IT PREDATES ONE OR MORE OF THESE -FIXES AND SHOULD NOT BE USED. (Older files were deleted during cleanup, -but if this pipeline gets rerun and produces intermediate files again, -this is the checklist for whether they're trustworthy.) - -1. **Bare-king endgame mates leaking into "middlegame" positives.** Only - checking the attacker's material (`>= 800cp`, roughly rook+bishop) let - trivial K+Q-vs-lone-K technique mates through. Fixed by also requiring - `total_non_pawn_piece_count(board) >= 8` (mirrors `_EvalKing`'s own - `pos->uNonPawnCount[WHITE][0] + pos->uNonPawnCount[BLACK][0] < 8` - endgame boundary exactly) -- see `is_middlegame()` in - `mine_king_mates.py`. -2. **`min_ply` too low (10, i.e. move 5) for control sampling.** A large - fraction of the pool's draws are short draws-by-agreement close to - `filter_pgn.py`'s own 40-ply floor. Combined with a large end-guard - (staying clear of the game's end), the only valid sampling window on a - short draw was the early opening -- where minors are legitimately - still undeveloped for completely ordinary reasons. This alone caused - `minorsathome` to look like a massive, spurious "signal" (controls - showed `minorsathome>=2` in ~85% of samples). Verified the engine's - own computation was correct on a real developed-middlegame FEN -- - 100% a sampling-window bug, not an eval bug. Fixed: `min_ply` raised - to 20 (move 10), and a game too short to satisfy both `min_ply` and - the end-guard is now skipped entirely rather than force-sampled early. -3. **Control-sampling ply selection secretly always picked the earliest - plies in a game's valid window.** `rng.shuffle(candidates)` followed - by `for idx in sorted(candidates)` completely undid the shuffle. - Fixed in two steps (the first fix wasn't sufficient on its own -- - see the code comment in `mine_controls()` for why): randomly - subsample which plies to try, replay ascending through all of them - ONCE (required -- boards can only be walked forward), collect every - result that passes filters into a list, THEN randomly choose which - results to actually keep via `rng.sample` on that list. Verified: - sampled-ply-as-fraction-of-game-length now has a median around 0.46 - with real tail into 60%+, instead of clustering at 20-30%. -4. **Matching-radius bug treated two very differently-scaled confound - columns with one shared distance metric.** `attackerarmyscaler` - spans 0-31 (`ASSERT`-confirmed hard clamp ceiling), `minorsathome` - spans 0-4. A single Chebyshev radius meant a "tight" radius-2 match - still let `minorsathome` drift by up to 2 -- literally half its - entire range. This is why `minorsathome` was STILL the #1 "signal" - even after an initial matching attempt. Fixed: `match_controls.py` - now takes `--exact-cols` (must match exactly, no radius search -- - `minorsathome` defaults here) and `--radius-cols` (allowed to expand - outward, `attackerarmyscaler` defaults here) as separate concepts. -5. **KDTree-based nearest-neighbor matching matched almost nothing** (308 - of 43,181 positives) because the match columns are low-cardinality - integers -- huge exact-value clusters mean a KDTree's "k nearest" are - massively tied, so the first few positives processed in a cluster - exhaust the shared candidate set and everyone else in that cluster - fails to match. Replaced with exact-bucket + expanding-integer-radius - matching (see `match_controls.py`), which has no such collision - problem. -6. **`_EvalKing`'s per-square ring scan implicitly assumed all 8 slots - were geometrically equivalent for feature purposes**; slots 3/4 - (side squares) vs. the diagonal slots turned out to behave - differently enough that per-slot resolution on the diagonals was - pure noise at the sample sizes available -- see "Feature schema" - above. -7. **Board-diagram spam.** `setboard` prints a full ASCII board dump on - every call by default (`g_Options.fShouldPost`, see `DumpPosition` in - gamelist.c), which dominated per-row I/O cost during feature - extraction. Fixed by prepending `nopost` to every extraction script. -8. **Two separate concurrent-process races during interactive mining**, - both self-inflicted mid-session: (a) `$!` after a `cd ... && nohup - ...` pipeline captured the wrong PID, leaving a stale duplicate - process racing on the same output file as a supposedly-fresh rerun; - (b) killing that process didn't fully stop it before a new one - started. No lasting damage (caught via `ps aux` before trusting any - output), but: **always verify a background job is actually singular - via `ps aux | grep <script>` before trusting its output**, especially - after a kill+relaunch sequence. -9. **`sizeof(KING_DEFECT_DUMP)` name collision with the - `-DKING_DEFECT_DUMP` build flag.** The build flag is defined as - literal `1` by the compiler; naming the struct type the same thing - caused the preprocessor to mangle the struct declaration wherever the - type name appeared. Renamed the struct to `KING_DEFECT_DUMP_INFO`. - If you ever add a new `#ifdef SOME_NAME` build flag AND a same-named - type/variable in the same session, expect this failure mode. -10. **`gmake` doesn't recompile on a flag-only change.** After the struct - rename (#9) and other `chess.h` edits, a `gmake KING_DEFECT_DUMP=1` - without a preceding `gmake clean` silently reused stale `.o` files - from an earlier flag-less build, producing a binary that didn't - actually have `evalkingdump` registered, with no compile error at - all. Always `gmake clean` first when a build-flag combination - changes, not just when headers change (this is already called out - in `CLAUDE.md` for header changes, but the flag-only case is easy to - forget too). - -## Performance notes for rerunning - -- `pgn/twic_filtered.pgn` (642,268 games) mines in a few minutes when - sharded 12-24 ways via `shard_mine.py` (byte-range sharding on real - game boundaries, found via a fast raw-text `[Event ` scan). Don't run - it unsharded -- a single-process full scan took ~40 minutes. -- Feature extraction (`extract_king_features.py`) is the slow stage: - ~54 minutes for 172,735 rows sharded 18 ways on a contended 24-core - box. `nopost` (fix #7 above) is essential -- without it, board-diagram - printing dominates I/O cost. -- `match_controls.py` is pure in-memory bucket lookups, seconds even for - the full dataset -- never worth sharding. -- If `twic_filtered.pgn` (642k games, >=2400 Elo / >40 ply filtered) ever - proves insufficient for match density, `pgn/twic.pgn` is 7-8x larger - (4,099,094 games, unfiltered) and available as a fallback source -- - not needed this run (100% match rate achieved on `twic_filtered.pgn` - once the above bugs were fixed). - -## Suggested next steps - -1. Pick 2-3 concrete constant changes from Tier 1/2 above (start with - `KingFileDefects`/`KingStormingPawnDefects` -- lowest risk). -2. Land them as a normal `eval.c` diff, run `precommit_check.sh`. -3. `sd10` comparison across the three curated suites against - `head_reference/`. -4. `match_play.py` gated head-to-head match against the pre-change DNA. -5. Only keep the change if `LOWER95 >= 0.5`. -6. Once a change lands, consider whether `CountKingSafetyDefects` (lazy - eval) needs corresponding rework -- deferred by design until a real - change is validated (see project goal section above). |
