summaryrefslogtreecommitdiff
path: root/src/command.c
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 /src/command.c
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 'src/command.c')
-rw-r--r--src/command.c196
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");
}