diff options
| author | Scott Gasch <[email protected]> | 2026-10-20 09:26:44 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-20 09:26:44 -0700 |
| commit | c8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80 (patch) | |
| tree | b6c2137c9ac577fa83b1e948fa0311d53dfe445d /src/eval_tune/relabel_selfplay_pgn.py | |
| parent | 3b5d3677bb14ae14728a1c4b221176bb83667d92 (diff) | |
Wire NNUE eval into search, build the training pipeline, and add a fast
move/eval-agreement benchmark against Stockfish (tests/lop)
Implements the architecture decisions in src/docs/NNUE.md: HalfKAv2_hm
feature set, int16/int8 quantization, per-ply accumulator backup for
king moves, and a lazy-eval-ready integration into search.c/eval.c.
Adds the full annotate_pgns/ training pipeline (extraction, K-fit/
labeling, quiet filtering, tensor building, training, export) plus an
end-to-end orchestrator (run_nnue_pipeline.py) and validation tooling
(sanity_check_net.py, compare_vs_stockfish_net.py). Adds tests/lop:
a 10k-position list-of-positions benchmark comparing engine eval rank
and move choice against pre-computed Stockfish/lc0 references, much
cheaper than a full self-play match for day-to-day progress checks.
First sd10-matched self-play result against classic HCE: LLR +2.28
(H1=+2.94), -72=119+169 over 360 games, confirming the net is
"smarter per node" at matched depth. A real-time (--st 4) match is
currently weaker (candidate loses ~3 plies of depth to a ~3.3-3.7x
nps gap against an unrecalibrated search), consistent with
NNUE.md's adoption-criterion section -- pruning/reduction constants
still need retuning for the new eval's score scale before a fair
real-time comparison is possible.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/eval_tune/relabel_selfplay_pgn.py')
| -rw-r--r-- | src/eval_tune/relabel_selfplay_pgn.py | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/src/eval_tune/relabel_selfplay_pgn.py b/src/eval_tune/relabel_selfplay_pgn.py new file mode 100644 index 0000000..be14e22 --- /dev/null +++ b/src/eval_tune/relabel_selfplay_pgn.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +One-off (but reusable) maintenance tool: retroactively rename the +White/Black PGN header value for games matching a given Event + Date +list, in place, without touching anything else in the file (moves, +comments, other headers byte-for-byte unchanged). + +Why this exists, 2026-10-16: self_play_games.pgn is reused as NNUE +training data (see docs/NNUE.md). match_play.py recorded every +candidate engine as literally "candidate" regardless of what that +candidate actually was, so the 2026-10-15/2026-10-16 batches (an NNUE +candidate vs the HCE baseline, scores on the NNUE side compressed/ +uncalibrated -- see the engine-vs-engine eval discussion that same +day) are indistinguishable from any other "candidate" batch by header +alone. match_play.py now takes --candidate-name/--baseline-name to +avoid this going forward; this script fixes the games written before +that existed. + +Deliberately stream-edits the file line by line rather than round- +tripping through python-chess's PGN writer -- re-serializing ~190k +games risks reformatting move/comment text (whitespace, line wrapping) +that downstream positional parsers (extract_positions.py) may depend +on being byte-identical to how it was originally written. This script +only ever rewrites a `[White "..."]`/`[Black "..."]` header line, and +only when that game's own `[Date ...]`/`[Event ...]` headers already +matched the target. + +Usage: + python3 relabel_selfplay_pgn.py <pgn-file> \\ + --dates 2026-10-15 2026-10-16 \\ + --event "typhoon self-play match" \\ + --old-name candidate --new-name candidate_nnue_loose \\ + [--dry-run] + +Writes to a sibling temp file and atomically replaces the original +only after a full pass succeeds -- a failure partway through never +leaves the original file half-rewritten. +""" +import argparse +import os +import re +import sys + + +HEADER_RE = re.compile(r'^\[(\w+)\s+"((?:[^"\\]|\\.)*)"\]\s*$') + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("pgn_file") + ap.add_argument("--dates", nargs="+", required=True, + help="Date header values (as PGN prints them, e.g. " + "2026-10-15) whose games are in scope") + ap.add_argument("--event", required=True, + help="Event header value a game must also match") + ap.add_argument("--old-name", required=True, + help="White/Black header value to rename") + ap.add_argument("--new-name", required=True, + help="replacement header value") + ap.add_argument("--dry-run", action="store_true", + help="report what would change, write nothing") + args = ap.parse_args() + + target_dates = set(args.dates) + tmp_path = args.pgn_file + ".relabel_tmp" + + cur_date = None + cur_event = None + in_scope = False # true once we know this game's headers match + games_seen = 0 + games_touched = 0 + renames = 0 + + with open(args.pgn_file, "r", encoding="utf-8", errors="surrogateescape") as fin, \ + open(tmp_path, "w", encoding="utf-8", errors="surrogateescape") as fout: + for line in fin: + m = HEADER_RE.match(line) + if m: + tag, value = m.group(1), m.group(2) + if tag == "Event": + # New header block -> new game starting. + games_seen += 1 + cur_event = value + cur_date = None + in_scope = False + elif tag == "Date": + cur_date = value + in_scope = (cur_event == args.event and cur_date in target_dates) + elif tag in ("White", "Black") and in_scope and value == args.old_name: + renames += 1 + games_touched += 1 if tag == "White" else 0 # count once/game is enough + if not args.dry_run: + line = f'[{tag} "{args.new_name}"]\n' + fout.write(line) + + if args.dry_run: + os.remove(tmp_path) + print(f"[dry run] {games_seen} games scanned, {renames} header " + f"values would be renamed ({args.old_name!r} -> " + f"{args.new_name!r}) for Event={args.event!r} Date in " + f"{sorted(target_dates)}") + return + + os.replace(tmp_path, args.pgn_file) + print(f"{games_seen} games scanned, {renames} header values renamed " + f"({args.old_name!r} -> {args.new_name!r}) for Event={args.event!r} " + f"Date in {sorted(target_dates)}") + + +if __name__ == "__main__": + sys.exit(main()) |
