Replace hint_common_parent_position() by backwards accumulator updates

Only calls to `evaluate()` now trigger NNUE accumulator updates. To make
sure that we are likely to find parent positions from which to update
the accumulators we perform a backwards NNUE update whenever we compute
the accumulator from scratch for some position.

passed STC
LLR: 2.93 (-2.94,2.94) <0.00,2.00>
Total: 39680 W: 10474 L: 10164 D: 19042
Ptnml(0-2): 171, 4068, 11042, 4398, 161
https://tests.stockfishchess.org/tests/view/67a27f26eb183d11c65945be

passed LTC
LLR: 2.94 (-2.94,2.94) <0.50,2.50>
Total: 337308 W: 86408 L: 85550 D: 165350
Ptnml(0-2): 276, 30551, 106126, 31441, 260
https://tests.stockfishchess.org/tests/view/67a287efeb183d11c65945ee

then simplified:
STC
LLR: 2.94 (-2.94,2.94) <-1.75,0.25>
Total: 28608 W: 7641 L: 7413 D: 13554
Ptnml(0-2): 132, 3036, 7744, 3256, 136
https://tests.stockfishchess.org/tests/view/67a4703719f522d3866d3345

LTC
LLR: 2.96 (-2.94,2.94) <-1.75,0.25>
Total: 200226 W: 51026 L: 50990 D: 98210
Ptnml(0-2): 170, 18468, 62799, 18508, 168
https://tests.stockfishchess.org/tests/view/67a4f255229c1a170cc08964

The version in this PR is a bit different from the simplified version,
but it's compile-time changes only.

closes https://github.com/official-stockfish/Stockfish/pull/5870

No functional change
This commit is contained in:
Carlos Esparza
2025-02-24 19:01:23 +01:00
committed by Disservin
parent fa6c30af81
commit e9997afb1c
7 changed files with 92 additions and 77 deletions
-6
View File
@@ -278,12 +278,6 @@ void Network<Arch, Transformer>::verify(std::string
}
template<typename Arch, typename Transformer>
void Network<Arch, Transformer>::hint_common_access(
const Position& pos, AccumulatorCaches::Cache<FTDimensions>* cache) const {
featureTransformer->hint_common_access(pos, cache);
}
template<typename Arch, typename Transformer>
NnueEvalTrace
Network<Arch, Transformer>::trace_evaluate(const Position& pos,
-3
View File
@@ -67,9 +67,6 @@ class Network {
AccumulatorCaches::Cache<FTDimensions>* cache) const;
void hint_common_access(const Position& pos,
AccumulatorCaches::Cache<FTDimensions>* cache) const;
void verify(std::string evalfilePath, const std::function<void(std::string_view)>&) const;
NnueEvalTrace trace_evaluate(const Position& pos,
AccumulatorCaches::Cache<FTDimensions>* cache) const;
+91 -50
View File
@@ -41,6 +41,11 @@ using BiasType = std::int16_t;
using WeightType = std::int16_t;
using PSQTWeightType = std::int32_t;
enum IncUpdateDirection {
FORWARD,
BACKWARDS
};
// If vector instructions are enabled, we update and refresh the
// accumulator tile by tile such that each tile fits in the CPU's
// vector registers.
@@ -249,6 +254,7 @@ class FeatureTransformer {
// Number of output dimensions for one side
static constexpr IndexType HalfDimensions = TransformedFeatureDimensions;
static constexpr bool Big = TransformedFeatureDimensions == TransformedFeatureDimensionsBig;
private:
using Tiling = SIMDTiling<TransformedFeatureDimensions, HalfDimensions>;
@@ -468,38 +474,20 @@ class FeatureTransformer {
return psqt;
} // end of function transform()
void hint_common_access(const Position& pos,
AccumulatorCaches::Cache<HalfDimensions>* cache) const {
update_accumulator<WHITE>(pos, cache);
update_accumulator<BLACK>(pos, cache);
}
private:
template<Color Perspective>
StateInfo* try_find_computed_accumulator(const Position& pos) const {
// Look for a usable accumulator of an earlier position. We keep track
// of the estimated gain in terms of features to be added/subtracted.
StateInfo* st = pos.state();
int gain = FeatureSet::refresh_cost(pos);
while (st->previous && !(st->*accPtr).computed[Perspective])
{
// This governs when a full feature refresh is needed and how many
// updates are better than just one full refresh.
if (FeatureSet::requires_refresh(st, Perspective)
|| (gain -= FeatureSet::update_cost(st) + 1) < 0)
break;
st = st->previous;
}
return st;
}
// Given a computed accumulator, computes the accumulator of the next position.
template<Color Perspective>
void update_accumulator_incremental(const Position& pos, StateInfo* computed) const {
// Given a computed accumulator, computes the accumulator of another position.
template<Color Perspective, IncUpdateDirection Direction = FORWARD>
void update_accumulator_incremental(const Square ksq,
StateInfo* target_state,
const StateInfo* computed) const {
[[maybe_unused]] constexpr bool Forward = Direction == FORWARD;
[[maybe_unused]] constexpr bool Backwards = Direction == BACKWARDS;
assert((computed->*accPtr).computed[Perspective]);
assert(computed->next != nullptr);
const Square ksq = pos.square<KING>(Perspective);
StateInfo* next = Forward ? computed->next : computed->previous;
assert(next != nullptr);
assert(!(next->*accPtr).computed[Perspective]);
// The size must be enough to contain the largest possible update.
// That might depend on the feature set and generally relies on the
@@ -508,11 +496,11 @@ class FeatureTransformer {
// In this case, the maximum size of both feature addition and removal
// is 2, since we are incrementally updating one move at a time.
FeatureSet::IndexList removed, added;
FeatureSet::append_changed_indices<Perspective>(ksq, computed->next->dirtyPiece, removed,
added);
StateInfo* next = computed->next;
assert(!(next->*accPtr).computed[Perspective]);
if constexpr (Forward)
FeatureSet::append_changed_indices<Perspective>(ksq, next->dirtyPiece, removed, added);
else
FeatureSet::append_changed_indices<Perspective>(ksq, computed->dirtyPiece, added,
removed);
if (removed.size() == 0 && added.size() == 0)
{
@@ -527,7 +515,10 @@ class FeatureTransformer {
{
assert(added.size() == 1 || added.size() == 2);
assert(removed.size() == 1 || removed.size() == 2);
assert(added.size() <= removed.size());
if (Forward)
assert(added.size() <= removed.size());
else
assert(removed.size() <= added.size());
#ifdef VECTOR
auto* accIn =
@@ -539,13 +530,15 @@ class FeatureTransformer {
const IndexType offsetR0 = HalfDimensions * removed[0];
auto* columnR0 = reinterpret_cast<const vec_t*>(&weights[offsetR0]);
if (removed.size() == 1)
if ((Forward && removed.size() == 1) || (Backwards && added.size() == 1))
{
assert(added.size() == 1 && removed.size() == 1);
for (IndexType i = 0; i < HalfDimensions * sizeof(WeightType) / sizeof(vec_t); ++i)
accOut[i] = vec_add_16(vec_sub_16(accIn[i], columnR0[i]), columnA0[i]);
}
else if (added.size() == 1)
else if (Forward && added.size() == 1)
{
assert(removed.size() == 2);
const IndexType offsetR1 = HalfDimensions * removed[1];
auto* columnR1 = reinterpret_cast<const vec_t*>(&weights[offsetR1]);
@@ -553,8 +546,19 @@ class FeatureTransformer {
accOut[i] = vec_sub_16(vec_add_16(accIn[i], columnA0[i]),
vec_add_16(columnR0[i], columnR1[i]));
}
else if (Backwards && removed.size() == 1)
{
assert(added.size() == 2);
const IndexType offsetA1 = HalfDimensions * added[1];
auto* columnA1 = reinterpret_cast<const vec_t*>(&weights[offsetA1]);
for (IndexType i = 0; i < HalfDimensions * sizeof(WeightType) / sizeof(vec_t); ++i)
accOut[i] = vec_add_16(vec_add_16(accIn[i], columnA0[i]),
vec_sub_16(columnA1[i], columnR0[i]));
}
else
{
assert(added.size() == 2 && removed.size() == 2);
const IndexType offsetA1 = HalfDimensions * added[1];
auto* columnA1 = reinterpret_cast<const vec_t*>(&weights[offsetA1]);
const IndexType offsetR1 = HalfDimensions * removed[1];
@@ -576,14 +580,15 @@ class FeatureTransformer {
const IndexType offsetPsqtR0 = PSQTBuckets * removed[0];
auto* columnPsqtR0 = reinterpret_cast<const psqt_vec_t*>(&psqtWeights[offsetPsqtR0]);
if (removed.size() == 1)
if ((Forward && removed.size() == 1)
|| (Backwards && added.size() == 1)) // added.size() == removed.size() == 1
{
for (std::size_t i = 0;
i < PSQTBuckets * sizeof(PSQTWeightType) / sizeof(psqt_vec_t); ++i)
accPsqtOut[i] = vec_add_psqt_32(vec_sub_psqt_32(accPsqtIn[i], columnPsqtR0[i]),
columnPsqtA0[i]);
}
else if (added.size() == 1)
else if (Forward && added.size() == 1)
{
const IndexType offsetPsqtR1 = PSQTBuckets * removed[1];
auto* columnPsqtR1 =
@@ -595,6 +600,18 @@ class FeatureTransformer {
vec_sub_psqt_32(vec_add_psqt_32(accPsqtIn[i], columnPsqtA0[i]),
vec_add_psqt_32(columnPsqtR0[i], columnPsqtR1[i]));
}
else if (Backwards && removed.size() == 1)
{
const IndexType offsetPsqtA1 = PSQTBuckets * added[1];
auto* columnPsqtA1 =
reinterpret_cast<const psqt_vec_t*>(&psqtWeights[offsetPsqtA1]);
for (std::size_t i = 0;
i < PSQTBuckets * sizeof(PSQTWeightType) / sizeof(psqt_vec_t); ++i)
accPsqtOut[i] =
vec_add_psqt_32(vec_add_psqt_32(accPsqtIn[i], columnPsqtA0[i]),
vec_sub_psqt_32(columnPsqtA1[i], columnPsqtR0[i]));
}
else
{
const IndexType offsetPsqtA1 = PSQTBuckets * added[1];
@@ -647,8 +664,8 @@ class FeatureTransformer {
(next->*accPtr).computed[Perspective] = true;
if (next != pos.state())
update_accumulator_incremental<Perspective>(pos, next);
if (next != target_state)
update_accumulator_incremental<Perspective, Direction>(ksq, target_state, next);
}
@@ -815,16 +832,40 @@ class FeatureTransformer {
template<Color Perspective>
void update_accumulator(const Position& pos,
AccumulatorCaches::Cache<HalfDimensions>* cache) const {
if ((pos.state()->*accPtr).computed[Perspective])
return;
StateInfo* oldest = try_find_computed_accumulator<Perspective>(pos);
StateInfo* st = pos.state();
if ((st->*accPtr).computed[Perspective])
return; // nothing to do
if ((oldest->*accPtr).computed[Perspective] && oldest != pos.state())
// Start from the oldest computed accumulator, update all the
// accumulators up to the current position.
update_accumulator_incremental<Perspective>(pos, oldest);
else
update_accumulator_refresh_cache<Perspective>(pos, cache);
[[maybe_unused]] // only used when !Big
int gain = FeatureSet::refresh_cost(pos);
// Look for a usable already computed accumulator of an earlier position.
// When computing the small accumulator, we keep track of the estimated gain in
// terms of features to be added/subtracted.
// When computing the big accumulator, we expect to be able to reuse any
// accumulators, so we always try to do an incremental update.
do
{
if (FeatureSet::requires_refresh(st, Perspective)
|| (!Big && (gain -= FeatureSet::update_cost(st) < 0)) || !st->previous
|| st->previous->next != st)
{
// compute accumulator from scratch for this position
update_accumulator_refresh_cache<Perspective>(pos, cache);
if (Big && st != pos.state())
// when computing a big accumulator from scratch we can use it to
// efficiently compute the accumulator backwards, until we get to a king
// move. We expect that we will need these accumulators later anyway, so
// computing them now will save some work.
update_accumulator_incremental<Perspective, BACKWARDS>(
pos.square<KING>(Perspective), st, pos.state());
return;
}
st = st->previous;
} while (!(st->*accPtr).computed[Perspective]);
// Start from the oldest computed accumulator, update all the
// accumulators up to the current position.
update_accumulator_incremental<Perspective>(pos.square<KING>(Perspective), pos.state(), st);
}
template<IndexType Size>
-10
View File
@@ -30,7 +30,6 @@
#include <string_view>
#include <tuple>
#include "../evaluate.h"
#include "../position.h"
#include "../types.h"
#include "../uci.h"
@@ -43,15 +42,6 @@ namespace Stockfish::Eval::NNUE {
constexpr std::string_view PieceToChar(" PNBRQK pnbrqk");
void hint_common_parent_position(const Position& pos,
const Networks& networks,
AccumulatorCaches& caches) {
if (Eval::use_smallnet(pos))
networks.small.hint_common_access(pos, &caches.small);
else
networks.big.hint_common_access(pos, &caches.big);
}
namespace {
// Converts a Value into (centi)pawns and writes it in a buffer.
// The buffer must have capacity for at least 5 chars.
-3
View File
@@ -54,9 +54,6 @@ struct Networks;
struct AccumulatorCaches;
std::string trace(Position& pos, const Networks& networks, AccumulatorCaches& caches);
void hint_common_parent_position(const Position& pos,
const Networks& networks,
AccumulatorCaches& caches);
} // namespace Stockfish::Eval::NNUE
} // namespace Stockfish
+1 -1
View File
@@ -63,9 +63,9 @@ struct StateInfo {
int repetition;
// Used by NNUE
DirtyPiece dirtyPiece;
Eval::NNUE::Accumulator<Eval::NNUE::TransformedFeatureDimensionsBig> accumulatorBig;
Eval::NNUE::Accumulator<Eval::NNUE::TransformedFeatureDimensionsSmall> accumulatorSmall;
DirtyPiece dirtyPiece;
};
-4
View File
@@ -41,7 +41,6 @@
#include "nnue/network.h"
#include "nnue/nnue_accumulator.h"
#include "nnue/nnue_common.h"
#include "nnue/nnue_misc.h"
#include "position.h"
#include "syzygy/tbprobe.h"
#include "thread.h"
@@ -759,7 +758,6 @@ Value Search::Worker::search(
else if (excludedMove)
{
// Providing the hint that this node's accumulator will be used often
Eval::NNUE::hint_common_parent_position(pos, networks[numaAccessToken], refreshTable);
unadjustedStaticEval = eval = ss->staticEval;
}
else if (ss->ttHit)
@@ -768,8 +766,6 @@ Value Search::Worker::search(
unadjustedStaticEval = ttData.eval;
if (!is_valid(unadjustedStaticEval))
unadjustedStaticEval = evaluate(pos);
else if (PvNode)
Eval::NNUE::hint_common_parent_position(pos, networks[numaAccessToken], refreshTable);
ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, correctionValue);