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/command.c | |
| 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/command.c')
| -rw-r--r-- | src/command.c | 196 |
1 files changed, 194 insertions, 2 deletions
diff --git a/src/command.c b/src/command.c index 6e11d90..1767e98 100644 --- a/src/command.c +++ b/src/command.c @@ -819,9 +819,13 @@ Return value: if (argc < 2) { Trace("Usage: nnue load <filename>\n" + " nnue probeload <filename>\n" " nnue eval\n" + " nnue stats\n" " nnue on\n" - " nnue off\n"); + " nnue off\n" + " nnue lazyprobeon\n" + " nnue lazyprobeoff\n"); return; } @@ -843,6 +847,28 @@ Return value: return; } + if (!STRCMPI(argv[1], "probeload")) + { + // Loads the experimental lazy-eval probe's weights (docs/ + // NNUE.md's "Lazy-evaluation tiering plan") -- a separate, + // calibration-only weight file, not the production .nnue + // format NnueLoadWeights reads. + if (argc < 3) + { + Trace("Error (missing argument)\n"); + return; + } + if (!NnueProbeLoadWeights(argv[2])) + { + Trace("Error loading NNUE probe weights file.\n"); + } + else + { + Trace("Loaded NNUE probe weights from \"%s\"\n", argv[2]); + } + return; + } + if (!STRCMPI(argv[1], "eval")) { if (!g_fNnueWeightsLoaded) @@ -880,6 +906,34 @@ Return value: return; } + if (!STRCMPI(argv[1], "stats")) + { + // Diagnostic: per-layer activation saturation report. See + // NnueDumpActivationStats's routine description (nnueeval.c). + if (!g_fNnueWeightsLoaded) + { + Trace("Error: no NNUE weights loaded -- " + "try 'nnue load <file>' first.\n"); + return; + } + if (NULL == ctx) + { + ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT)); + if (NULL == ctx) + { + Trace("Out of memory.\n"); + return; + } + InitializeSearcherContext(pos, ctx); + } + else + { + ReInitializeSearcherContext(pos, ctx); + } + NnueDumpActivationStats(ctx); + return; + } + if (!STRCMPI(argv[1], "on")) { if (!g_fNnueWeightsLoaded) @@ -900,10 +954,148 @@ Return value: return; } + if (!STRCMPI(argv[1], "lazyprobeon")) + { + if (!g_fNnueProbeLoaded) + { + Trace("Error: no NNUE probe weights loaded -- " + "try 'nnue probeload <file>' first.\n"); + return; + } + g_fUseNnueLazyProbe = TRUE; + Trace("NNUE lazy probe gate enabled.\n"); + return; + } + + if (!STRCMPI(argv[1], "lazyprobeoff")) + { + g_fUseNnueLazyProbe = FALSE; + Trace("NNUE lazy probe gate disabled.\n"); + return; + } + + if (!STRCMPI(argv[1], "lazymargin")) + { + // Flat override for every material bucket's lazy-probe margin + // -- experimentation knob, see nnueeval.c. Pass a negative + // number (or omit) to go back to the calibrated table. + if (argc < 3) + { + g_iNnueLazyMarginOverride = -1; + Trace("NNUE lazy margin override cleared (back to calibrated table).\n"); + return; + } + g_iNnueLazyMarginOverride = atoi(argv[2]); + Trace("NNUE lazy margin override set to %d cp.\n", g_iNnueLazyMarginOverride); + return; + } + + if (!STRCMPI(argv[1], "superlazyon")) + { + if (argc < 3) + { + Trace("Usage: nnue superlazyon <margin cp>\n"); + return; + } + g_iNnueSuperLazyMargin = (SCORE)atoi(argv[2]); + g_fUseNnueSuperLazy = TRUE; + Trace("NNUE super-lazy (material-only) pre-filter enabled, margin=%d cp.\n", + g_iNnueSuperLazyMargin); + return; + } + + if (!STRCMPI(argv[1], "superlazyoff")) + { + g_fUseNnueSuperLazy = FALSE; + Trace("NNUE super-lazy pre-filter disabled.\n"); + return; + } + + if (!STRCMPI(argv[1], "bench")) + { + // Scratch diagnostic, not a shipped feature -- times + // NnueAccumAddFeature/RemoveFeature (the per-MakeMove hot + // path) against NnueForward/NnueProbeScore (the per-eval hot + // path) so we know which one actually dominates before + // spending effort hand-vectorizing either. See docs/NNUE.md. + const ULONG N = 2000000; + ULONG i; + UINT64 u64Start, u64AddRemove, u64Forward, u64Probe; + NNUE_ACCUMULATOR acc; + COOR cKing = pos->cKingSq[WHITE]; + COOR cSq = FILE_RANK_TO_COOR(3, 4); + PIECE p = pos->rgSquare[FILE_RANK_TO_COOR(0, 1)].pPiece; + + memset(&acc, 0, sizeof(acc)); + u64Start = SystemReadTimeStampCounter(); + for (i = 0; i < N; i++) + { + NnueAccumAddFeature(&acc, WHITE, cKing, cSq, p); + NnueAccumRemoveFeature(&acc, WHITE, cKing, cSq, p); + } + u64AddRemove = SystemReadTimeStampCounter() - u64Start; + + if (!g_fNnueWeightsLoaded) + { + Trace("nnue bench: load weights first ('nnue load <file>') " + "to also time the forward pass.\n"); + Trace("Add+Remove (x%lu): %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " cycles total, %.1f cycles/call\n", + N, u64AddRemove, (double)u64AddRemove / (2.0 * N)); + return; + } + + if (NULL == ctx) + { + ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT)); + InitializeSearcherContext(pos, ctx); + } + else + { + ReInitializeSearcherContext(pos, ctx); + } + + u64Start = SystemReadTimeStampCounter(); + for (i = 0; i < N; i++) + { + volatile SCORE s = NnueForward(ctx); + (void)s; + } + u64Forward = SystemReadTimeStampCounter() - u64Start; + + u64Probe = 0; + if (g_fNnueProbeLoaded) + { + u64Start = SystemReadTimeStampCounter(); + for (i = 0; i < N; i++) + { + volatile SCORE s = NnueProbeScore(ctx); + (void)s; + } + u64Probe = SystemReadTimeStampCounter() - u64Start; + } + + Trace("nnue bench (N=%lu):\n", N); + Trace(" Add+Remove pair : %10.1f cycles/call\n", + (double)u64AddRemove / (2.0 * N)); + Trace(" NnueForward : %10.1f cycles/call\n", + (double)u64Forward / (double)N); + if (g_fNnueProbeLoaded) + { + Trace(" NnueProbeScore : %10.1f cycles/call\n", + (double)u64Probe / (double)N); + } + return; + } + Trace("Usage: nnue load <filename>\n" + " nnue probeload <filename>\n" " nnue eval\n" " nnue on\n" - " nnue off\n"); + " nnue off\n" + " nnue lazyprobeon\n" + " nnue lazyprobeoff\n" + " nnue bench\n"); } |
