summaryrefslogtreecommitdiff
path: root/src/root.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/root.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/root.c')
-rw-r--r--src/root.c39
1 files changed, 39 insertions, 0 deletions
diff --git a/src/root.c b/src/root.c
index fda77d5..47fd103 100644
--- a/src/root.c
+++ b/src/root.c
@@ -294,6 +294,45 @@ Return value:
if (NULL != pos)
{
memmove(&(ctx->sPosition), pos, sizeof(POSITION));
+
+ // Establish a correct NNUE accumulator baseline for whatever
+ // position was just bound in -- MakeMove/UnmakeMove only
+ // maintain sNnueAccum *incrementally* from here on; nothing
+ // else ever (re)establishes it from scratch. This is the one
+ // choke point every real "go"/"ponder" (root.c's own two
+ // InitializeSearcherContext call sites), every MP=1 split
+ // worker (split.c), and setboard's downstream effect (the
+ // position SetRootPosition commits to g_RootPosition only
+ // actually reaches a context, and therefore an accumulator,
+ // via this function at the next go) all pass through.
+ // Unconditional, not gated on whether NNUE eval is currently
+ // toggled on (EvalCommand's g_fUseNnueEval) -- cheap (32
+ // active features x 2 colors, not per-node), and keeps
+ // toggling NNUE on mid-game correct for free instead of
+ // needing its own separate re-sync path.
+ //
+ // Defensive, though: this function is also called from many
+ // test-only sites (testrecogn.c especially) that construct a
+ // POSITION by hand, sometimes mid-construction, not
+ // guaranteed to have valid king placement for both colors at
+ // the exact moment they call this. NnueAccumRecompute's own
+ // ASSERTs are deliberately strict for its direct, deliberate
+ // callers (the "nnue eval" command, testnnue.c) -- this one
+ // call site instead checks first and silently skips (leaving
+ // sNnueAccum stale) rather than crashing every caller that
+ // doesn't care about NNUE correctness at all. Found exactly
+ // this way: TestRecogn tripped the ASSERT on its first run
+ // after this hook was added.
+ if (IS_ON_BOARD(ctx->sPosition.cKingSq[WHITE]) &&
+ IS_KING(ctx->sPosition.rgSquare[ctx->sPosition.cKingSq[WHITE]].pPiece) &&
+ (PIECE_COLOR(ctx->sPosition.rgSquare[ctx->sPosition.cKingSq[WHITE]].pPiece) == WHITE) &&
+ IS_ON_BOARD(ctx->sPosition.cKingSq[BLACK]) &&
+ IS_KING(ctx->sPosition.rgSquare[ctx->sPosition.cKingSq[BLACK]].pPiece) &&
+ (PIECE_COLOR(ctx->sPosition.rgSquare[ctx->sPosition.cKingSq[BLACK]].pPiece) == BLACK))
+ {
+ NnueAccumRecompute(&ctx->sNnueAccum[WHITE], &ctx->sPosition, WHITE);
+ NnueAccumRecompute(&ctx->sNnueAccum[BLACK], &ctx->sPosition, BLACK);
+ }
}
ctx->uPly = 0;
memset(&(ctx->sCounters), 0, sizeof(COUNTERS));