diff options
| author | Scott Gasch <[email protected]> | 2026-10-15 16:26:09 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-15 16:26:09 -0700 |
| commit | 1ea81bae240912efb8cebabae35fe58bc5d12afb (patch) | |
| tree | e43b3bbb179601595a47f7c5c4661a297a8b4664 /src/command.c | |
| parent | 3d55dc94497661b1577109b42a94cc3dba1860fb (diff) | |
Wire NNUE eval into real search path; fix critical perf regression
Load-and-call infra: export_nnue_weights.py-produced weight files can
now be loaded at runtime (nnueeval.c's NnueLoadWeights, "nnue load"
command, --nnueweights CLI flag for scripted match-play), and the FC
head (NnueForward) runs the real forward pass against the live
accumulator.
Real integration: Eval() now short-circuits to NnueForward when
g_fUseNnueEval && g_fNnueWeightsLoaded, so ./typhoon can actually play
on the NNUE eval -- ./eval_tune/test_vs_head.sh works unmodified to
compare it against the old HCE. ReInitializeSearcherContext (root.c)
gained a defensive accumulator recompute hook covering setboard's
downstream effect on the next go/ponder; it only fires when both
kings are validly placed, since testrecogn.c's synthetic positions
don't always have one at the moment this is called.
Critical fix: the NNUE accumulator maintenance added to move.c's 8
primitives (and MakeMove/UnmakeMove's king-move backup/restore) was
running unconditionally, on the mistaken assumption this was cheap.
It cost ~50% of engine throughput even with NNUE off (673k nps
measured vs the expected 1.4-1.5M baseline). Gated all of it behind
g_fUseNnueEval; classic HCE is back to ~1.52M nps, NNUE eval runs at
a fair ~409k nps (expected for an unquantized float32 forward pass).
This gating also exposed that testnnue.c's TestNnueAccumulator never
set g_fUseNnueEval, so its own direct NnueAccumRecompute calls raced
ahead of now-gated-off incremental maintenance; fixed by having the
test set/restore the flag around its run.
board.c: added en passant legality check to IsBoardLegal (rank vs
side-to-move, origin-square occupancy) -- a real, previously-latent
bug surfaced while building the NNUE test fixtures, unrelated to
NNUE itself.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/command.c')
| -rw-r--r-- | src/command.c | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/src/command.c b/src/command.c index 715c989..6e11d90 100644 --- a/src/command.c +++ b/src/command.c @@ -28,6 +28,7 @@ Revision History: **/ #include "chess.h" +#include <math.h> // NnueCommand's sigmoid (exp) #define MAX_ARGS 32 int g_eModeBeforeAnalyze = -1; @@ -771,6 +772,141 @@ Return value: } +COMMAND(NnueCommand) +/** + +Routine description: + + This function implements the 'nnue' engine command. + + Usage: + + nnue load <filename> + nnue eval + + 'load' reads a weights file exported by annotate_pgns/ + export_nnue_weights.py (NnueLoadWeights, nnueeval.c). 'eval' + recomputes both color's accumulators from scratch against the + current position -- not relying on any incremental-maintenance + history, since this command exists to validate weight loading + and the forward pass (NnueForward), not to exercise the hot + MakeMove/UnmakeMove path -- and prints the raw score plus a + sigmoid-derived win probability (same K=1.3123 pawns as + annotate_pgns/fit_k_and_label.py's Phase A fit). + + Reference-only: not wired into Eval()/search. See docs/NNUE.md. + +Parameters: + + The COMMAND macro hides four arguments from the input parser: + + CHAR *szInput : the full line of input + ULONG argc : number of argument chunks + CHAR *argv[] : array of ptrs to each argument chunk + POSITION *pos : a POSITION pointer to operate on + +Return value: + + void + +**/ +{ + static SEARCHER_THREAD_CONTEXT *ctx = NULL; + static const double K_PAWNS = 1.3123; + SCORE iScore; + double dWinProb; + + if (argc < 2) + { + Trace("Usage: nnue load <filename>\n" + " nnue eval\n" + " nnue on\n" + " nnue off\n"); + return; + } + + if (!STRCMPI(argv[1], "load")) + { + if (argc < 3) + { + Trace("Error (missing argument)\n"); + return; + } + if (!NnueLoadWeights(argv[2])) + { + Trace("Error loading NNUE weights file.\n"); + } + else + { + Trace("Loaded NNUE weights from \"%s\"\n", argv[2]); + } + return; + } + + if (!STRCMPI(argv[1], "eval")) + { + if (!g_fNnueWeightsLoaded) + { + Trace("Error: no NNUE weights loaded -- " + "try 'nnue load <file>' first.\n"); + return; + } + + // Same persistent-context pattern as EvalCommand -- avoid + // paying SEARCHER_THREAD_CONTEXT's full multi-MB + // allocation/init cost on every call. + if (NULL == ctx) + { + ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT)); + if (NULL == ctx) + { + Trace("Out of memory.\n"); + return; + } + InitializeSearcherContext(pos, ctx); + } + else + { + // Already recomputes both accumulators from scratch + // internally (root.c's ReInitializeSearcherContext) -- + // no separate NnueAccumRecompute call needed here. + ReInitializeSearcherContext(pos, ctx); + } + + iScore = NnueForward(ctx); + dWinProb = 1.0 / (1.0 + exp(-((double)iScore / 100.0) / K_PAWNS)); + Trace("NNUE static eval: %s (win prob for side to move: %.4f)\n", + ScoreToString(iScore), dWinProb); + return; + } + + if (!STRCMPI(argv[1], "on")) + { + if (!g_fNnueWeightsLoaded) + { + Trace("Error: no NNUE weights loaded -- " + "try 'nnue load <file>' first.\n"); + return; + } + g_fUseNnueEval = TRUE; + Trace("NNUE eval enabled.\n"); + return; + } + + if (!STRCMPI(argv[1], "off")) + { + g_fUseNnueEval = FALSE; + Trace("NNUE eval disabled (back to the hand-crafted Eval()).\n"); + return; + } + + Trace("Usage: nnue load <filename>\n" + " nnue eval\n" + " nnue on\n" + " nnue off\n"); +} + + COMMAND(CalibrateCommand) /** @@ -2494,6 +2630,13 @@ COMMAND_PARSER_ENTRY g_ParserTable[] = FALSE, FALSE, "Read or write the DNA from the eval code." }, + { "nnue", + NnueCommand, + FALSE, + FALSE, + FALSE, + "Load NNUE weights / run the NNUE forward pass (reference-only, " + "not wired into search)." }, { "exit", ExitCommand, FALSE, |
