summaryrefslogtreecommitdiff
path: root/annotate_pgns/parse_stockfish_net.py
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-20 09:26:44 -0700
committerScott Gasch <[email protected]>2026-10-20 09:26:44 -0700
commitc8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80 (patch)
treeb6c2137c9ac577fa83b1e948fa0311d53dfe445d /annotate_pgns/parse_stockfish_net.py
parent3b5d3677bb14ae14728a1c4b221176bb83667d92 (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 'annotate_pgns/parse_stockfish_net.py')
-rw-r--r--annotate_pgns/parse_stockfish_net.py117
1 files changed, 117 insertions, 0 deletions
diff --git a/annotate_pgns/parse_stockfish_net.py b/annotate_pgns/parse_stockfish_net.py
new file mode 100644
index 0000000..ad70eb9
--- /dev/null
+++ b/annotate_pgns/parse_stockfish_net.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+"""Extracts the FeatureTransformer's psqtWeights table from a real
+Stockfish .nnue file (nn-fdc1d0fe6455.nnue, added to Stockfish 2023-06-12,
+rev 38e61663d836e062af0bc002814ad5149c4b7729 -- see
+https://github.com/official-stockfish/networks).
+
+Parsed against that exact commit's header-reading code (fetched and
+read directly, not guessed/reverse-engineered by eye -- see
+src/nnue/evaluate_nnue.cpp:94-125, src/nnue/nnue_feature_transformer.h:254-258,
+src/nnue/nnue_architecture.h:44, src/nnue/features/half_ka_v2_hm.h):
+
+Top-level header (evaluate_nnue.cpp read_header):
+ version uint32 LE
+ hashValue uint32 LE
+ size uint32 LE (length of description string)
+ description `size` bytes
+
+FeatureTransformer section (Detail::read_parameters + its own
+read_parameters):
+ ft_header_hash uint32 LE, must equal
+ HalfKAv2_hm::HashValue (0x7f234cb8) XOR (2*L1)
+ biases L1 int16 values
+ weights L1 * InputDimensions int16 values
+ psqtWeights PSQTBuckets * InputDimensions int32 values,
+ laid out psqtWeights[index*PSQTBuckets + k]
+
+InputDimensions = SQUARE_NB*PS_NB/2 = 22528, IDENTICAL formula/indexing
+to typhoon's own HalfKAv2_hm spec (docs/NNUE.md) -- same king buckets,
+same orientation table, same piece-square planes. PSQTBuckets=8
+(nnue_architecture.h). L1 for this net is NOT assumed -- derived below
+from the stored ft hash, since Stockfish's own loader treats that
+hash as the authoritative check, not file size or guessed offsets.
+
+This script ONLY extracts psqtWeights (a literal per-feature-index
+scalar linear value, one per material-count bucket) for comparison/
+diagnostic purposes against our own training coverage gaps -- NOT for
+copying into our net's weights. See the conversation: the goal is a
+from-scratch net, this is a sanity check on whether our dusty corners
+are in the right ballpark, not a transplant.
+
+Usage:
+ python3 parse_stockfish_net.py nn-fdc1d0fe6455.nnue --out sf_psqt.npy
+"""
+import argparse
+import struct
+import sys
+
+import numpy as np
+
+HALFKAV2_HM_HASH = 0x7F234CB8
+PS_NB = 11 * 64 # 704
+INPUT_DIMENSIONS = (64 * PS_NB) // 2 # 22528
+PSQT_BUCKETS = 8
+
+
+def read_u32(f):
+ return struct.unpack("<I", f.read(4))[0]
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("nnue_file")
+ ap.add_argument("--out", required=True,
+ help="output .npy, shape [INPUT_DIMENSIONS, PSQT_BUCKETS] int32")
+ args = ap.parse_args()
+
+ with open(args.nnue_file, "rb") as f:
+ version = read_u32(f)
+ top_hash = read_u32(f)
+ desc_len = read_u32(f)
+ desc = f.read(desc_len).decode("utf-8", errors="replace")
+ print(f"version=0x{version:08x} top_hash=0x{top_hash:08x}")
+ print(f"description: {desc!r}")
+
+ ft_hash = read_u32(f)
+ print(f"ft_hash=0x{ft_hash:08x}")
+
+ # ft_hash = FeatureSet::HashValue ^ (OutputDimensions * 2)
+ # (feature_transformer.h:250) -- solve for L1 (OutputDimensions).
+ l1_times_2 = ft_hash ^ HALFKAV2_HM_HASH
+ if l1_times_2 % 2 != 0:
+ sys.exit(f"ft_hash XOR HalfKAv2_hm hash gave an odd value "
+ f"({l1_times_2}) -- this net's feature set isn't "
+ f"HalfKAv2_hm, or the hash constant is wrong for "
+ f"this Stockfish version. Refusing to guess further.")
+ l1 = l1_times_2 // 2
+ print(f"Derived L1 (TransformedFeatureDimensions) = {l1}")
+
+ n_biases = l1
+ n_weights = l1 * INPUT_DIMENSIONS
+ n_psqt = PSQT_BUCKETS * INPUT_DIMENSIONS
+
+ biases = np.frombuffer(f.read(2 * n_biases), dtype="<i2")
+ assert len(biases) == n_biases, "short read on biases -- bad offset math"
+ print(f"biases: read {len(biases)}, "
+ f"range [{biases.min()}, {biases.max()}]")
+
+ # Skip the weights array -- not needed for this diagnostic, and
+ # it's ~66MB (l1 * 22528 * 2 bytes), the bulk of the file.
+ f.seek(2 * n_weights, 1)
+
+ psqt_raw = np.frombuffer(f.read(4 * n_psqt), dtype="<i4")
+ if len(psqt_raw) != n_psqt:
+ sys.exit(f"short read on psqtWeights: got {len(psqt_raw)}, "
+ f"expected {n_psqt} -- offset math above must be "
+ f"wrong (L1 derivation or file truncated)")
+
+ psqt = psqt_raw.reshape(INPUT_DIMENSIONS, PSQT_BUCKETS)
+ print(f"psqtWeights: shape {psqt.shape}, "
+ f"range [{psqt.min()}, {psqt.max()}]")
+ np.save(args.out, psqt)
+ print(f"Wrote {args.out}")
+
+
+if __name__ == "__main__":
+ sys.exit(main())