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/root.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/root.c')
| -rw-r--r-- | src/root.c | 39 |
1 files changed, 39 insertions, 0 deletions
@@ -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)); |
