1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
/**
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"
// Quantized embedding table -- see chess.h's extern declaration and
// its own comment. Zero-initialized (C's static/global default) until
// NnueLoadWeights reads a real exported checkpoint.
SHORT g_rgfNnueWeight[NNUE_DIMENSIONS][NNUE_ACCUMULATOR_SIZE];
SHORT g_rgfNnueBias[NNUE_ACCUMULATOR_SIZE];
float g_fNnueFtScale = 1.0f;
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 SHORT *pRow = g_rgfNnueWeight[idx];
ULONG i;
for (i = 0; i < NNUE_ACCUMULATOR_SIZE; i++)
{
pAccum->rgiValue[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 an
exact no-op (integer arithmetic, no float rounding to worry about).
**/
{
int idx = NnueFeatureIndex(uPerspective, cKingOwn, cPieceSquare, pPiece);
const SHORT *pRow = g_rgfNnueWeight[idx];
ULONG i;
for (i = 0; i < NNUE_ACCUMULATOR_SIZE; i++)
{
pAccum->rgiValue[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->rgiValue[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);
}
}
}
|