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/split_ep.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/split_ep.py')
| -rwxr-xr-x | src/split_ep.py | 69 |
1 files changed, 69 insertions, 0 deletions
diff --git a/src/split_ep.py b/src/split_ep.py new file mode 100755 index 0000000..557dbec --- /dev/null +++ b/src/split_ep.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Split a script-format .ep_ file (setboard/solution/id/go blocks, as +used by typhoon's `script` command) into two disjoint, shuffled halves +for a train/holdout threshold sweep. + +Usage: split_ep.py <input.ep_> <out_a.ep_> <out_b.ep_> [--seed N] + +Splits by whole position blocks (never mid-position), shuffled with a +fixed seed so the split is reproducible but not biased by the file's +original ordering (ECM.ep_ is not randomly ordered). +""" +import argparse +import random +import sys + + +def read_blocks(path): + """Each block is the lines from one 'setboard' up to and including + the following 'go' line.""" + blocks = [] + current = [] + with open(path) as f: + for line in f: + stripped = line.rstrip("\n") + if stripped.startswith("setboard") and current: + blocks.append(current) + current = [] + current.append(stripped) + if current: + blocks.append(current) + return blocks + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("input") + ap.add_argument("out_a") + ap.add_argument("out_b") + ap.add_argument("--seed", type=int, default=0) + args = ap.parse_args() + + blocks = read_blocks(args.input) + if not blocks: + print(f"ERROR: no position blocks found in {args.input}", file=sys.stderr) + sys.exit(1) + + rng = random.Random(args.seed) + order = list(range(len(blocks))) + rng.shuffle(order) + + mid = len(order) // 2 + half_a = sorted(order[:mid]) + half_b = sorted(order[mid:]) + + with open(args.out_a, "w") as f: + for i in half_a: + f.write("\n".join(blocks[i]) + "\n") + + with open(args.out_b, "w") as f: + for i in half_b: + f.write("\n".join(blocks[i]) + "\n") + + print(f"{len(blocks)} positions in {args.input} -> " + f"{len(half_a)} in {args.out_a}, {len(half_b)} in {args.out_b} " + f"(seed={args.seed})") + + +if __name__ == "__main__": + main() |
