diff options
| author | Scott Gasch <[email protected]> | 2026-10-15 13:11:22 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-15 13:11:22 -0700 |
| commit | 993b6eeb2a9ac7b659c473b6883fa53af1ad3a0b (patch) | |
| tree | c5388824dec15a7691ce9952a6eb4af66e837127 /src/nnuefeatures.c | |
| parent | 53b380c658f828c4c0e096be2763e6b012c23abd (diff) | |
Phase A NNUE: feature-indexing spec, annotation/training pipeline, dump nnuefeatures
Builds out the NNUE Phase A prototype per docs/NNUE.md:
- src/nnuefeatures.c: standalone HalfKAv2_hm feature-index reference
implementation (new `dump nnuefeatures` command), sourced from
Stockfish's half_ka_v2_hm.{h,cpp} (sf_16 tag), including the 0x88->64
square conversion typhoon's own board representation needs.
- annotate_pgns/: sharded TWIC/self-play annotation harness
(annotate.py), position extraction, quiet-position filtering (flat
and game-shaped, plus an upstream --prefilter-quiet option),
train/holdout game-level stratified split, WDL-sigmoid K fitting,
the Python HalfKAv2_hm reference encoder (nnue_feature_spec.py) and
its bit-for-bit cross-checker against the C side
(cross_check_features.py), and a first PyTorch training pipeline
(nnue_model.py, nnue_train.py, build_training_tensors.py) -- a
256x2-32-32-1-ish net trained locally on CPU against ~1.85M quiet
TWIC positions as a pipeline/framework validation pass.
- Finish an in-progress InitMagic/InitLMRTable/InitEval ->
InitializeBitboardMagic/InitializeLMRTable/InitializeEval rename:
chess.h's prototypes hadn't been updated to match the already-renamed
definitions/call sites, leaving 4 implicit-function-declaration
warnings.
- tests/arasan.epd: new curated EPD suite.
- docs/: add the project's design docs (previously untracked).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/nnuefeatures.c')
| -rw-r--r-- | src/nnuefeatures.c | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/src/nnuefeatures.c b/src/nnuefeatures.c new file mode 100644 index 0000000..c43e267 --- /dev/null +++ b/src/nnuefeatures.c @@ -0,0 +1,248 @@ +/** + +Copyright (c) Scott Gasch + +Module Name: + + nnuefeatures.c + +Abstract: + + Standalone reference implementation of the HalfKAv2_hm feature + index formula, used ONLY to cross-check against the Python + reference encoder (annotate_pgns/nnue_feature_spec.py) -- see + docs/NNUE.md's "Canonical HalfKAv2_hm feature-indexing spec" + section for the formulas this file implements, sourced verbatim + from Stockfish's src/nnue/features/half_ka_v2_hm.{h,cpp} (git tag + sf_16). + + This is deliberately NOT wired into search/eval -- no accumulator, + no incremental maintenance, nothing performance-sensitive. It + exists purely so `dump nnuefeatures` can print the exact same + active-feature-index set the eventual real C inference code must + produce, so a Python/C cross-checker has a C-side ground truth to + diff against before any of that real code exists. The *real* + MakeMove/UnmakeMove accumulator plumbing (per NNUE.md's + sequencing, gated behind a dedicated branch, not started) will + reuse this same index math, not this file's dump-command + plumbing. + +Author: + + Scott Gasch ([email protected]) 15 Oct 2026 + +**/ + +#include "chess.h" + +// +// HalfKAv2_hm constants, verbatim from Stockfish's half_ka_v2_hm.h. +// PS_NB counts 5 non-king piece types x 2 "us/them" roles x 64 +// squares, plus one shared 64-square king plane (own king and enemy +// king features both land in this one plane, distinguished only by +// which square is set). +// +#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 + +// +// Convert a typhoon 0x88 COOR to Stockfish's flat 0-63 square index +// (file + 8*(rank-1), rank/file both derived from the engine's own +// RANK()/FILE() macros -- see docs/NNUE.md's spec for why this +// one-line conversion is the single highest-risk spot in the whole +// scheme). +// +static int +CoorToSq64(COOR c) +{ + return (int)FILE(c) + 8 * ((int)RANK(c) - 1); +} + +// +// King bucket (0..31) for a king on Stockfish-square sq64, from +// color uPerspective's (WHITE/BLACK) point of view. Verified against +// Stockfish's literal KingBuckets[][] table -- see docs/NNUE.md. +// +static int +KingBucket(ULONG uPerspective, int sq64) +{ + int file = sq64 & 7; + int rank0 = sq64 >> 3; // 0..7, rank-1 + int fileQuartet = (file < 4) ? file : (7 - file); // 0..3, mirrored + int rankGroup = (uPerspective == WHITE) ? (7 - rank0) : rank0; + return rankGroup * 4 + fileQuartet; +} + +// +// Orientation XOR mask for perspective uPerspective whose own king +// sits on Stockfish-square ksq64. Only ksq64's file selects the +// branch (queenside half a-d vs. kingside half e-h); any rank. +// +static int +OrientMask(ULONG uPerspective, int ksq64) +{ + int file = ksq64 & 7; + if (uPerspective == WHITE) + { + return (file < 4) ? 7 : 0; // SQ_H1 : SQ_A1 + } + return (file < 4) ? 63 : 56; // SQ_H8 : SQ_A8 +} + +// +// Piece-square plane offset for a piece of color uPieceColor and +// type uPieceType (chess.h's PAWN..KING values), as seen from +// perspective uPerspective. Both kings (own and enemy) share +// NNUE_PS_KING. +// +static int +PiecePlane(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; + } +} + +static int +CompareInts(const void *a, const void *b) +{ + int x = *(const int *)a; + int y = *(const int *)b; + return (x > y) - (x < y); +} + +// +// Fill pIndices (caller-allocated, at least 32 ints) with the sorted +// set of active HalfKAv2_hm feature indices for perspective +// uPerspective at position pos. Returns the count (<=32). +// +static int +ActiveFeatureIndices(POSITION *pos, ULONG uPerspective, int *pIndices) +{ + int f, r; + COOR cKing = ILLEGAL_COOR; + int ksq64; + int orientMask; + int kingBucketOffset; + int n = 0; + + // + // Find this perspective's own king square. + // + 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) && + (PIECE_TYPE(p) == KING) && + (PIECE_COLOR(p) == uPerspective)) + { + cKing = c; + } + } + } + ASSERT(cKing != ILLEGAL_COOR); + ksq64 = CoorToSq64(cKing); + orientMask = OrientMask(uPerspective, ksq64); + kingBucketOffset = KingBucket(uPerspective, ksq64) * NNUE_PS_NB; + + 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; + int sq64; + int idx; + + if (IS_EMPTY(p)) + { + continue; + } + sq64 = CoorToSq64(c); + idx = (sq64 ^ orientMask) + + PiecePlane(uPerspective, PIECE_COLOR(p), PIECE_TYPE(p)) + + kingBucketOffset; + ASSERT((idx >= 0) && (idx < NNUE_DIMENSIONS)); + ASSERT(n < 32); + pIndices[n++] = idx; + } + } + qsort(pIndices, n, sizeof(int), CompareInts); + return n; +} + +void +DumpNnueFeatures(POSITION *pos) +/** + +Routine description: + + Prints the sorted set of active HalfKAv2_hm feature indices for + both perspectives at the current position, one line per + perspective, in a format trivial for a Python cross-checker to + parse: + + NNUE_FEATURES white <idx> <idx> ... + NNUE_FEATURES black <idx> <idx> ... + +Parameters: + + POSITION *pos : position to dump + +Return value: + + void + +**/ +{ + int rgIndices[32]; + int n; + int i; + + n = ActiveFeatureIndices(pos, WHITE, rgIndices); + Trace("NNUE_FEATURES white"); + for (i = 0; i < n; i++) + { + Trace(" %d", rgIndices[i]); + } + Trace("\n"); + + n = ActiveFeatureIndices(pos, BLACK, rgIndices); + Trace("NNUE_FEATURES black"); + for (i = 0; i < n; i++) + { + Trace(" %d", rgIndices[i]); + } + Trace("\n"); +} |
