<feed xmlns='http://www.w3.org/2005/Atom'>
<title>typhoon.git/src/testnnue.c, branch master</title>
<subtitle>The typhoon chess playing engine.
</subtitle>
<id>https://git.acknak.org/cgit/typhoon.git/atom?h=master</id>
<link rel='self' href='https://git.acknak.org/cgit/typhoon.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/'/>
<updated>2026-10-20T16:26:44Z</updated>
<entry>
<title>Wire NNUE eval into search, build the training pipeline, and add a fast</title>
<updated>2026-10-20T16:26:44Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-20T16:26:44Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=c8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80'/>
<id>urn:sha1:c8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Wire NNUE eval into real search path; fix critical perf regression</title>
<updated>2026-10-15T23:26:09Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-15T23:26:09Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=1ea81bae240912efb8cebabae35fe58bc5d12afb'/>
<id>urn:sha1:1ea81bae240912efb8cebabae35fe58bc5d12afb</id>
<content type='text'>
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 &amp;&amp; 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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>NNUE accumulator correctness test; fix king-capture backup-ordering bug</title>
<updated>2026-10-15T22:35:54Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-15T22:35:54Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=3d55dc94497661b1577109b42a94cc3dba1860fb'/>
<id>urn:sha1:3d55dc94497661b1577109b42a94cc3dba1860fb</id>
<content type='text'>
testnnue.c (new, TEST=1): TestNnueAccumulator proves the one property
nothing else checked -- that SEARCHER_THREAD_CONTEXT.sNnueAccum stays
equal to a from-scratch NnueAccumRecompute through real move
sequences. Fills the (otherwise all-zero placeholder) embedding table
with distinctive, deterministic values first: an all-zero table would
let a broken feature-index formula "pass" trivially, since every row
sums to zero regardless of which squares/pieces are involved. Plays
real games 2 plies deep from curated FENs chosen to reach captures,
promotions, en passant, castling, and plain king moves quickly, and
compares after every MakeMove and every UnmakeMove -- 2+ plies of
recursion is what exercises PLY_INFO.sNnueAccumBackup's per-ply
indexing (two king moves at different, not-yet-unmade plies must not
collide), which a single-ply test could not have caught.

It caught a real bug on its very first run: move.c's MakeMove backed
up the king's own-perspective accumulator *after* a capture's
LiftPiece call instead of before it. For a king capturing a piece
(e.g. Kxd7), LiftPiece already removes the captured piece's feature
from both perspectives -- including the king's own -- before the
backup snapshot was taken, so the backup captured a state already
missing the captured piece. UnmakeMove correctly resurrects the
captured piece via PlacePieceWithoutSigs, but then immediately
overwrote that correct state with the stale backup. Fixed by moving
the backup to the top of the ordinary (non-castling) king-move
handling, before any mutation for that move happens at all -- the
castling branch was already backing up before its own moves and
needed no change.

Verified via the full precommit_check.sh (all 4 gates, including the
combined DEBUG=1 TEST=1 self-test, which now runs this test too).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
