summaryrefslogtreecommitdiff
path: root/src/command.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-15 16:26:09 -0700
committerScott Gasch <[email protected]>2026-10-15 16:26:09 -0700
commit1ea81bae240912efb8cebabae35fe58bc5d12afb (patch)
treee43b3bbb179601595a47f7c5c4661a297a8b4664 /src/command.c
parent3d55dc94497661b1577109b42a94cc3dba1860fb (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.c143
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,