diff options
| author | Scott Gasch <[email protected]> | 2026-10-15 15:21:26 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-15 15:21:26 -0700 |
| commit | 2bf4eb27a473089b9b30726a678fbac27d0f8a69 (patch) | |
| tree | 23e7271eab238cf2fa97128d902c0c828e7d1583 /src/nnueaccum.c | |
| parent | ac7572166bd24868fcaf5980a91bb82fe61ba677 (diff) | |
NNUE accumulator incremental maintenance: MakeMove/UnmakeMove hookup
Wires the live NNUE accumulator (SEARCHER_THREAD_CONTEXT.sNnueAccum[2],
added in the previous commit) into real incremental maintenance,
hooked directly into the piece-movement primitives rather than
re-deriving move type from MOVE in a separate pass:
- chess.h: shared HalfKAv2_hm feature-index formula/constants
(NnueFeatureIndex and friends), moved here from nnuefeatures.c so
the reference dump and the real hot-path code can never drift
apart. Prototypes for NnueAccumAddFeature/RemoveFeature/Recompute
and the (still placeholder, zero-initialized) embedding table.
LiftPiece/PlacePiece/SlidePiece/SlidePawn/SlidePieceWithoutSigs
signatures gain an NNUE_ACCUMULATOR sNnueAccum[2] parameter.
- nnueaccum.c (new): the embedding table storage and the three
accumulator operations. Written as straight-line 256-float loops,
structured for -O3 -ffast-math auto-vectorization on this box's
AVX2+FMA target; explicit SIMD intrinsics and real weight loading
are separate, deliberately deferred follow-ups.
- move.c: all 8 Lift/Place/Slide(WithoutSigs) primitives now keep
sNnueAccum incrementally correct for both perspectives on every
call -- except when the piece is a king, where each primitive skips
its own-color perspective entirely (that perspective needs a full
recompute, not a delta, since every HalfKAv2_hm feature in it is
own-king-relative; computing an incremental update for it only to
discard it unread would be wasted work). MakeMove backs up that
perspective into the current ply's PLY_INFO.sNnueAccumBackup before
a king moves (castling or a plain step) and overwrites it via
NnueAccumRecompute once the board reflects the final position;
UnmakeMove restores it from the same backup slot. DEBUG builds tag
the backup with the color it belongs to and poison it after
consumption, release builds carry no overhead for either.
- nnuefeatures.c: now a thin board-walking wrapper around the shared
chess.h formula instead of its own duplicated copy.
- testmove.c: TestLiftPlaceSlidePiece passes a local (unread) dummy
accumulator pair -- the only caller of these primitives outside
move.c, and it never needed NNUE awareness, just a real argument.
Verified via the full precommit_check.sh (all 4 gates, including the
new combined DEBUG=1 TEST=1 self-test, which exercises MakeMove/
UnmakeMove heavily via TestMoveGenerator's perft counts and
TestLegalMoveGenerator) -- confirms nothing crashes and no existing
invariant breaks. Does NOT yet confirm the actual property that
matters (incrementally-maintained accumulator == recompute-from-
scratch) -- no test exercises that yet; next step.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/nnueaccum.c')
| -rw-r--r-- | src/nnueaccum.c | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/src/nnueaccum.c b/src/nnueaccum.c new file mode 100644 index 0000000..8f01692 --- /dev/null +++ b/src/nnueaccum.c @@ -0,0 +1,148 @@ +/** + +Copyright (c) Scott Gasch + +Module Name: + + nnueaccum.c + +Abstract: + + Real NNUE accumulator maintenance: the embedding-table storage and + the three operations (add one feature's row, remove one feature's + row, recompute an entire perspective from scratch) that move.c's + MakeMove/UnmakeMove hook into LiftPiece/PlacePiece/SlidePiece/ + SlidePawn/SlidePieceWithoutSigs use to keep + SEARCHER_THREAD_CONTEXT.sNnueAccum[2] incrementally up to date. + + Hot path -- called from the piece-movement primitives, which run + millions of times/sec in a real search. Kept branch-light: Add/ + Remove are a single feature-index computation (chess.h's + NnueFeatureIndex, shared with nnuefeatures.c's reference dump) and + a straight-line 256-float add/subtract loop, written so -O3 + -ffast-math (this project's release profile) has the best shot at + auto-vectorizing it on this box's confirmed AVX2+FMA target. + Explicit SIMD intrinsics are an explicitly deferred follow-up (see + docs/NNUE.md) once there's a correct baseline to profile against, + not something to reach for pre-emptively here. + +Author: + + Scott Gasch ([email protected]) 15 Oct 2026 + +**/ + +#include "chess.h" + +// Placeholder embedding table -- see chess.h's extern declaration and +// its own comment. Zero-initialized (C's static/global default), so +// every accumulator is currently all-zero regardless of position; +// real weight loading (reading annotate_pgns/nnue_train.py's trained +// checkpoint) is separate, not-yet-built follow-up work. This is +// sufficient to prove incremental-vs-recompute-from-scratch +// correctness, which only depends on both paths reading the SAME +// table consistently, not on its values being meaningful yet. +float g_rgfNnueWeight[NNUE_DIMENSIONS][NNUE_ACCUMULATOR_SIZE]; +float g_rgfNnueBias[NNUE_ACCUMULATOR_SIZE]; + +void +NnueAccumAddFeature(NNUE_ACCUMULATOR *pAccum, ULONG uPerspective, + COOR cKingOwn, COOR cPieceSquare, PIECE pPiece) +/** + +Routine description: + + Adds one feature's weight row into *pAccum (in place). Hot path. + +**/ +{ + int idx = NnueFeatureIndex(uPerspective, cKingOwn, cPieceSquare, pPiece); + const float *pRow = g_rgfNnueWeight[idx]; + ULONG i; + + for (i = 0; i < NNUE_ACCUMULATOR_SIZE; i++) + { + pAccum->rgfValue[i] += pRow[i]; + } +} + + +void +NnueAccumRemoveFeature(NNUE_ACCUMULATOR *pAccum, ULONG uPerspective, + COOR cKingOwn, COOR cPieceSquare, PIECE pPiece) +/** + +Routine description: + + Subtracts one feature's weight row from *pAccum (in place). Hot + path -- the exact inverse of NnueAccumAddFeature, so that applying + Add then Remove (or Remove then Add) for the same arguments is a + no-op up to float rounding. + +**/ +{ + int idx = NnueFeatureIndex(uPerspective, cKingOwn, cPieceSquare, pPiece); + const float *pRow = g_rgfNnueWeight[idx]; + ULONG i; + + for (i = 0; i < NNUE_ACCUMULATOR_SIZE; i++) + { + pAccum->rgfValue[i] -= pRow[i]; + } +} + + +void +NnueAccumRecompute(NNUE_ACCUMULATOR *pAccum, POSITION *pos, ULONG uPerspective) +/** + +Routine description: + + Fully recomputes *pAccum from scratch against the CURRENT board in + *pos, for perspective uPerspective -- the only correct way to + update a perspective after its OWN king moves (every feature's + king-bucket/orientation changes, not just the king's own feature; + see chess.h's NNUE_ACCUMULATOR comment). Not hot in the same sense + as Add/Remove -- only called on king moves, a small fraction of + all moves in a tree -- so this walks the whole board rather than + needing a piece list. + +Parameters: + + NNUE_ACCUMULATOR *pAccum : accumulator to overwrite + POSITION *pos : current board (already reflects the move) + ULONG uPerspective : which color's accumulator this is + +**/ +{ + COOR cKing = pos->cKingSq[uPerspective]; + int f, r; + ULONG i; + + ASSERT(IS_ON_BOARD(cKing)); + ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); + ASSERT(PIECE_COLOR(pos->rgSquare[cKing].pPiece) == uPerspective); + + for (i = 0; i < NNUE_ACCUMULATOR_SIZE; i++) + { + pAccum->rgfValue[i] = g_rgfNnueBias[i]; + } + + // 0x88 COORs aren't a simple monotonic range (A1=0x70 > H8=0x07), + // so walk file/rank explicitly -- same idiom nnuefeatures.c's + // ActiveFeatureIndices already uses. + for (f = 0; f < 8; f++) + { + for (r = 1; r <= 8; r++) + { + COOR c = FILE_RANK_TO_COOR(f, r); + PIECE p = pos->rgSquare[c].pPiece; + + if (IS_EMPTY(p)) + { + continue; + } + NnueAccumAddFeature(pAccum, uPerspective, cKing, c, p); + } + } +} |
