summaryrefslogtreecommitdiff
path: root/src/chess.h
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-15 15:21:26 -0700
committerScott Gasch <[email protected]>2026-10-15 15:21:26 -0700
commit2bf4eb27a473089b9b30726a678fbac27d0f8a69 (patch)
tree23e7271eab238cf2fa97128d902c0c828e7d1583 /src/chess.h
parentac7572166bd24868fcaf5980a91bb82fe61ba677 (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/chess.h')
-rwxr-xr-xsrc/chess.h137
1 files changed, 133 insertions, 4 deletions
diff --git a/src/chess.h b/src/chess.h
index 29833fb..d0cbd34 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -1648,6 +1648,11 @@ typedef struct _NNUE_ACCUMULATOR
}
NNUE_ACCUMULATOR;
+//
+// NNUE feature-index formula and accumulator-maintenance prototypes
+// live further down in this header (after ASSERT is defined) -- see
+// the block near ASSERT's own definition.
+
typedef struct _PLY_INFO
{
#ifdef DEBUG
@@ -2205,6 +2210,121 @@ _assert(CHAR *szFile, ULONG uLine);
#define VERIFY(x) x;
#endif // DEBUG
+// HalfKAv2_hm feature-index formula, shared verbatim between the
+// reference-only dump path (nnuefeatures.c's DumpNnueFeatures) and the
+// real, hot-path MakeMove/UnmakeMove accumulator maintenance (move.c)
+// -- defined once, here, so the two can never silently drift apart.
+// See docs/NNUE.md's "Canonical HalfKAv2_hm feature-indexing spec"
+// for the derivation; this is just the formula, no commentary
+// repeated here.
+//
+#define NNUE_SQUARE_NB (64)
+#define NNUE_PS_W_PAWN (0 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_PAWN (1 * NNUE_SQUARE_NB)
+#define NNUE_PS_W_KNIGHT (2 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_KNIGHT (3 * NNUE_SQUARE_NB)
+#define NNUE_PS_W_BISHOP (4 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_BISHOP (5 * NNUE_SQUARE_NB)
+#define NNUE_PS_W_ROOK (6 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_ROOK (7 * NNUE_SQUARE_NB)
+#define NNUE_PS_W_QUEEN (8 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_QUEEN (9 * NNUE_SQUARE_NB)
+#define NNUE_PS_KING (10 * NNUE_SQUARE_NB)
+#define NNUE_PS_NB (11 * NNUE_SQUARE_NB) // 704
+#define NNUE_DIMENSIONS ((NNUE_SQUARE_NB * NNUE_PS_NB) / 2) // 22528
+
+static int FORCEINLINE
+_NnueCoorToSq64(COOR c)
+{
+ return (int)FILE(c) + 8 * ((int)RANK(c) - 1);
+}
+
+static int FORCEINLINE
+_NnueKingBucket(ULONG uPerspective, int sq64)
+{
+ int file = sq64 & 7;
+ int rank0 = sq64 >> 3;
+ int fileQuartet = (file < 4) ? file : (7 - file);
+ int rankGroup = (uPerspective == WHITE) ? (7 - rank0) : rank0;
+ return rankGroup * 4 + fileQuartet;
+}
+
+static int FORCEINLINE
+_NnueOrientMask(ULONG uPerspective, int ksq64)
+{
+ int file = ksq64 & 7;
+ if (uPerspective == WHITE)
+ {
+ return (file < 4) ? 7 : 0;
+ }
+ return (file < 4) ? 63 : 56;
+}
+
+static int FORCEINLINE
+_NnuePiecePlane(ULONG uPerspective, ULONG uPieceColor, ULONG uPieceType)
+{
+ FLAG fOwn;
+
+ if (uPieceType == KING)
+ {
+ return NNUE_PS_KING;
+ }
+ fOwn = (uPieceColor == uPerspective);
+ switch (uPieceType)
+ {
+ case PAWN: return fOwn ? NNUE_PS_W_PAWN : NNUE_PS_B_PAWN;
+ case KNIGHT: return fOwn ? NNUE_PS_W_KNIGHT : NNUE_PS_B_KNIGHT;
+ case BISHOP: return fOwn ? NNUE_PS_W_BISHOP : NNUE_PS_B_BISHOP;
+ case ROOK: return fOwn ? NNUE_PS_W_ROOK : NNUE_PS_B_ROOK;
+ case QUEEN: return fOwn ? NNUE_PS_W_QUEEN : NNUE_PS_B_QUEEN;
+ default:
+ ASSERT(FALSE);
+ return 0;
+ }
+}
+
+// Full feature-index formula: given perspective uPerspective, that
+// perspective's OWN king square cKingOwn (unoriented, the real board
+// square), and some piece pPiece sitting on cPieceSquare, returns the
+// active HalfKAv2_hm feature index. cPieceSquare == cKingOwn is valid
+// (every king is also a feature, via NNUE_PS_KING) and so is pPiece
+// being the OPPONENT's king. __attribute__((unused)) because not
+// every TU that includes chess.h calls this directly (some only use
+// the higher-level NnueAccum* wrappers in nnueaccum.c) and it's a
+// static-in-header function, so each such TU would otherwise warn.
+static int FORCEINLINE __attribute__((unused))
+NnueFeatureIndex(ULONG uPerspective, COOR cKingOwn, COOR cPieceSquare, PIECE pPiece)
+{
+ int ksq64 = _NnueCoorToSq64(cKingOwn);
+ int orientMask = _NnueOrientMask(uPerspective, ksq64);
+ int kingBucketOffset = _NnueKingBucket(uPerspective, ksq64) * NNUE_PS_NB;
+ int sq64 = _NnueCoorToSq64(cPieceSquare);
+ int idx = (sq64 ^ orientMask) +
+ _NnuePiecePlane(uPerspective, PIECE_COLOR(pPiece), PIECE_TYPE(pPiece)) +
+ kingBucketOffset;
+ ASSERT((idx >= 0) && (idx < NNUE_DIMENSIONS));
+ return idx;
+}
+
+// Placeholder embedding table -- NOT yet loaded from a real trained
+// checkpoint (see docs/NNUE.md's "not yet built: a weight-loading
+// mechanism"). Zero-initialized by C's own static/global default, so
+// every accumulator value is 0 until real weights are loaded; this is
+// enough to prove the incremental-maintenance plumbing is correct.
+extern float g_rgfNnueWeight[NNUE_DIMENSIONS][NNUE_ACCUMULATOR_SIZE];
+extern float g_rgfNnueBias[NNUE_ACCUMULATOR_SIZE];
+
+void
+NnueAccumAddFeature(NNUE_ACCUMULATOR *pAccum, ULONG uPerspective,
+ COOR cKingOwn, COOR cPieceSquare, PIECE pPiece);
+
+void
+NnueAccumRemoveFeature(NNUE_ACCUMULATOR *pAccum, ULONG uPerspective,
+ COOR cKingOwn, COOR cPieceSquare, PIECE pPiece);
+
+void
+NnueAccumRecompute(NNUE_ACCUMULATOR *pAccum, POSITION *pos, ULONG uPerspective);
+
#ifdef PERF_COUNTERS
#define INC(x) ((x) += 1)
#else
@@ -2791,17 +2911,26 @@ SetRootToInitialPosition(void);
//
// move.c
//
+// Each takes the mover's live NNUE_ACCUMULATOR pair (normally
+// ctx->sNnueAccum) and keeps it incrementally up to date for whatever
+// square(s) this call changes, for BOTH perspectives, unconditionally
+// -- including when the piece involved is a king (never true for
+// Lift/Place, which never touch kings; see MakeMove/UnmakeMove's own
+// comments on why a king move's OWN-perspective accumulator needs a
+// full recompute/restore layered on top at the call site, not inside
+// these primitives).
+//
void
-SlidePiece(POSITION *pos, COOR cFrom, COOR cTo);
+SlidePiece(POSITION *pos, COOR cFrom, COOR cTo, NNUE_ACCUMULATOR sNnueAccum[2]);
void
-SlidePawn(POSITION *pos, COOR cFrom, COOR cTo);
+SlidePawn(POSITION *pos, COOR cFrom, COOR cTo, NNUE_ACCUMULATOR sNnueAccum[2]);
PIECE
-LiftPiece(POSITION *pos, COOR cSquare);
+LiftPiece(POSITION *pos, COOR cSquare, NNUE_ACCUMULATOR sNnueAccum[2]);
void
-PlacePiece(POSITION *pos, COOR cSquare, PIECE pPiece);
+PlacePiece(POSITION *pos, COOR cSquare, PIECE pPiece, NNUE_ACCUMULATOR sNnueAccum[2]);
FLAG
MakeMove(SEARCHER_THREAD_CONTEXT *ctx,