/* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Stockfish is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "nnue_accumulator.h" #include #include #include #include #include "../bitboard.h" #include "../misc.h" #include "../position.h" #include "../types.h" #include "network.h" #include "nnue_architecture.h" #include "nnue_feature_transformer.h" namespace Stockfish::Eval::NNUE { #if defined(__GNUC__) && !defined(__clang__) #define sf_assume(cond) \ do \ { \ if (!(cond)) \ __builtin_unreachable(); \ } while (0) #else // do nothing for other compilers #define sf_assume(cond) #endif namespace { template void update_accumulator_incremental( const FeatureTransformer& featureTransformer, const Square ksq, AccumulatorState& target_state, const AccumulatorState& computed); template void update_accumulator_refresh_cache(const FeatureTransformer& featureTransformer, const Position& pos, AccumulatorState& accumulatorState, AccumulatorCaches::Cache& cache); } void AccumulatorState::reset(const DirtyPiece& dp) noexcept { dirtyPiece = dp; accumulatorBig.computed.fill(false); accumulatorSmall.computed.fill(false); } const AccumulatorState& AccumulatorStack::latest() const noexcept { return m_accumulators[m_current_idx - 1]; } AccumulatorState& AccumulatorStack::mut_latest() noexcept { return m_accumulators[m_current_idx - 1]; } void AccumulatorStack::reset(const Position& rootPos, const Networks& networks, AccumulatorCaches& caches) noexcept { m_current_idx = 1; update_accumulator_refresh_cache( *networks.big.featureTransformer, rootPos, m_accumulators[0], caches.big); update_accumulator_refresh_cache( *networks.big.featureTransformer, rootPos, m_accumulators[0], caches.big); update_accumulator_refresh_cache( *networks.small.featureTransformer, rootPos, m_accumulators[0], caches.small); update_accumulator_refresh_cache( *networks.small.featureTransformer, rootPos, m_accumulators[0], caches.small); } void AccumulatorStack::push(const DirtyPiece& dirtyPiece) noexcept { assert(m_current_idx + 1 < m_accumulators.size()); m_accumulators[m_current_idx].reset(dirtyPiece); m_current_idx++; } void AccumulatorStack::pop() noexcept { assert(m_current_idx > 1); m_current_idx--; } template void AccumulatorStack::evaluate(const Position& pos, const FeatureTransformer& featureTransformer, AccumulatorCaches::Cache& cache) noexcept { evaluate_side(pos, featureTransformer, cache); evaluate_side(pos, featureTransformer, cache); } template void AccumulatorStack::evaluate_side(const Position& pos, const FeatureTransformer& featureTransformer, AccumulatorCaches::Cache& cache) noexcept { const auto last_usable_accum = find_last_usable_accumulator(); if ((m_accumulators[last_usable_accum].template acc()).computed[Perspective]) forward_update_incremental(pos, featureTransformer, last_usable_accum); else { update_accumulator_refresh_cache(featureTransformer, pos, mut_latest(), cache); backward_update_incremental(pos, featureTransformer, last_usable_accum); } } // Find the earliest usable accumulator, this can either be a computed accumulator or the accumulator // state just before a change that requires full refresh. template std::size_t AccumulatorStack::find_last_usable_accumulator() const noexcept { for (std::size_t curr_idx = m_current_idx - 1; curr_idx > 0; curr_idx--) { if ((m_accumulators[curr_idx].template acc()).computed[Perspective]) return curr_idx; if (FeatureSet::requires_refresh(m_accumulators[curr_idx].dirtyPiece, Perspective)) return curr_idx; } return 0; } template void AccumulatorStack::forward_update_incremental( const Position& pos, const FeatureTransformer& featureTransformer, const std::size_t begin) noexcept { assert(begin < m_accumulators.size()); assert((m_accumulators[begin].acc()).computed[Perspective]); const Square ksq = pos.square(Perspective); for (std::size_t next = begin + 1; next < m_current_idx; next++) update_accumulator_incremental( featureTransformer, ksq, m_accumulators[next], m_accumulators[next - 1]); assert((latest().acc()).computed[Perspective]); } template void AccumulatorStack::backward_update_incremental( const Position& pos, const FeatureTransformer& featureTransformer, const std::size_t end) noexcept { assert(end < m_accumulators.size()); assert(end < m_current_idx); assert((latest().acc()).computed[Perspective]); const Square ksq = pos.square(Perspective); for (std::size_t next = m_current_idx - 2; next >= end; next--) update_accumulator_incremental( featureTransformer, ksq, m_accumulators[next], m_accumulators[next + 1]); assert((m_accumulators[end].acc()).computed[Perspective]); } // Explicit template instantiations template void AccumulatorStack::evaluate( const Position& pos, const FeatureTransformer& featureTransformer, AccumulatorCaches::Cache& cache) noexcept; template void AccumulatorStack::evaluate( const Position& pos, const FeatureTransformer& featureTransformer, AccumulatorCaches::Cache& cache) noexcept; namespace { template, bool> = true> void fused_row_reduce(const ElementType* in, ElementType* out, const Ts* const... rows) { constexpr IndexType size = Width * sizeof(ElementType) / sizeof(typename VectorWrapper::type); auto* vecIn = reinterpret_cast(in); auto* vecOut = reinterpret_cast(out); for (IndexType i = 0; i < size; ++i) vecOut[i] = fused( vecIn[i], reinterpret_cast(rows)[i]...); } template struct AccumulatorUpdateContext { const FeatureTransformer& featureTransformer; const AccumulatorState& from; AccumulatorState& to; AccumulatorUpdateContext(const FeatureTransformer& ft, const AccumulatorState& accF, AccumulatorState& accT) noexcept : featureTransformer{ft}, from{accF}, to{accT} {} template, bool> = true> void apply(const Ts... indices) { auto to_weight_vector = [&](const IndexType index) { return &featureTransformer.weights[index * Dimensions]; }; auto to_psqt_weight_vector = [&](const IndexType index) { return &featureTransformer.psqtWeights[index * PSQTBuckets]; }; fused_row_reduce( (from.acc()).accumulation[Perspective], (to.acc()).accumulation[Perspective], to_weight_vector(indices)...); fused_row_reduce( (from.acc()).psqtAccumulation[Perspective], (to.acc()).psqtAccumulation[Perspective], to_psqt_weight_vector(indices)...); } }; template auto make_accumulator_update_context(const FeatureTransformer& featureTransformer, const AccumulatorState& accumulatorFrom, AccumulatorState& accumulatorTo) noexcept { return AccumulatorUpdateContext{featureTransformer, accumulatorFrom, accumulatorTo}; } template void update_accumulator_incremental( const FeatureTransformer& featureTransformer, const Square ksq, AccumulatorState& target_state, const AccumulatorState& computed) { assert((computed.acc()).computed[Perspective]); assert(!(target_state.acc()).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 // feature set's update cost calculation to be correct and never allow // updates with more added/removed features than MaxActiveDimensions. // 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; if constexpr (Forward) FeatureSet::append_changed_indices(ksq, target_state.dirtyPiece, removed, added); else FeatureSet::append_changed_indices(ksq, computed.dirtyPiece, added, removed); assert(added.size() == 1 || added.size() == 2); assert(removed.size() == 1 || removed.size() == 2); assert((Forward && added.size() <= removed.size()) || (!Forward && added.size() >= removed.size())); // Workaround compiler warning for uninitialized variables, replicated on // profile builds on windows with gcc 14.2.0. // TODO remove once unneeded sf_assume(added.size() == 1 || added.size() == 2); sf_assume(removed.size() == 1 || removed.size() == 2); auto updateContext = make_accumulator_update_context(featureTransformer, computed, target_state); if ((Forward && removed.size() == 1) || (!Forward && added.size() == 1)) { assert(added.size() == 1 && removed.size() == 1); updateContext.template apply(added[0], removed[0]); } else if (Forward && added.size() == 1) { assert(removed.size() == 2); updateContext.template apply(added[0], removed[0], removed[1]); } else if (!Forward && removed.size() == 1) { assert(added.size() == 2); updateContext.template apply(added[0], added[1], removed[0]); } else { assert(added.size() == 2 && removed.size() == 2); updateContext.template apply(added[0], added[1], removed[0], removed[1]); } (target_state.acc()).computed[Perspective] = true; } template void update_accumulator_refresh_cache(const FeatureTransformer& featureTransformer, const Position& pos, AccumulatorState& accumulatorState, AccumulatorCaches::Cache& cache) { using Tiling [[maybe_unused]] = SIMDTiling; const Square ksq = pos.square(Perspective); auto& entry = cache[ksq][Perspective]; FeatureSet::IndexList removed, added; for (Color c : {WHITE, BLACK}) { for (PieceType pt = PAWN; pt <= KING; ++pt) { const Piece piece = make_piece(c, pt); const Bitboard oldBB = entry.byColorBB[c] & entry.byTypeBB[pt]; const Bitboard newBB = pos.pieces(c, pt); Bitboard toRemove = oldBB & ~newBB; Bitboard toAdd = newBB & ~oldBB; while (toRemove) { Square sq = pop_lsb(toRemove); removed.push_back(FeatureSet::make_index(sq, piece, ksq)); } while (toAdd) { Square sq = pop_lsb(toAdd); added.push_back(FeatureSet::make_index(sq, piece, ksq)); } } } auto& accumulator = accumulatorState.acc(); accumulator.computed[Perspective] = true; #ifdef VECTOR vec_t acc[Tiling::NumRegs]; psqt_vec_t psqt[Tiling::NumPsqtRegs]; for (IndexType j = 0; j < Dimensions / Tiling::TileHeight; ++j) { auto* accTile = reinterpret_cast(&accumulator.accumulation[Perspective][j * Tiling::TileHeight]); auto* entryTile = reinterpret_cast(&entry.accumulation[j * Tiling::TileHeight]); for (IndexType k = 0; k < Tiling::NumRegs; ++k) acc[k] = entryTile[k]; IndexType i = 0; for (; i < std::min(removed.size(), added.size()); ++i) { IndexType indexR = removed[i]; const IndexType offsetR = Dimensions * indexR + j * Tiling::TileHeight; auto* columnR = reinterpret_cast(&featureTransformer.weights[offsetR]); IndexType indexA = added[i]; const IndexType offsetA = Dimensions * indexA + j * Tiling::TileHeight; auto* columnA = reinterpret_cast(&featureTransformer.weights[offsetA]); for (IndexType k = 0; k < Tiling::NumRegs; ++k) acc[k] = fused(acc[k], columnA[k], columnR[k]); } for (; i < removed.size(); ++i) { IndexType index = removed[i]; const IndexType offset = Dimensions * index + j * Tiling::TileHeight; auto* column = reinterpret_cast(&featureTransformer.weights[offset]); for (IndexType k = 0; k < Tiling::NumRegs; ++k) acc[k] = vec_sub_16(acc[k], column[k]); } for (; i < added.size(); ++i) { IndexType index = added[i]; const IndexType offset = Dimensions * index + j * Tiling::TileHeight; auto* column = reinterpret_cast(&featureTransformer.weights[offset]); for (IndexType k = 0; k < Tiling::NumRegs; ++k) acc[k] = vec_add_16(acc[k], column[k]); } for (IndexType k = 0; k < Tiling::NumRegs; k++) vec_store(&entryTile[k], acc[k]); for (IndexType k = 0; k < Tiling::NumRegs; k++) vec_store(&accTile[k], acc[k]); } for (IndexType j = 0; j < PSQTBuckets / Tiling::PsqtTileHeight; ++j) { auto* accTilePsqt = reinterpret_cast( &accumulator.psqtAccumulation[Perspective][j * Tiling::PsqtTileHeight]); auto* entryTilePsqt = reinterpret_cast(&entry.psqtAccumulation[j * Tiling::PsqtTileHeight]); for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = entryTilePsqt[k]; for (IndexType i = 0; i < removed.size(); ++i) { IndexType index = removed[i]; const IndexType offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; auto* columnPsqt = reinterpret_cast(&featureTransformer.psqtWeights[offset]); for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = vec_sub_psqt_32(psqt[k], columnPsqt[k]); } for (IndexType i = 0; i < added.size(); ++i) { IndexType index = added[i]; const IndexType offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; auto* columnPsqt = reinterpret_cast(&featureTransformer.psqtWeights[offset]); for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); } for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) vec_store_psqt(&entryTilePsqt[k], psqt[k]); for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) vec_store_psqt(&accTilePsqt[k], psqt[k]); } #else for (const auto index : removed) { const IndexType offset = Dimensions * index; for (IndexType j = 0; j < Dimensions; ++j) entry.accumulation[j] -= featureTransformer.weights[offset + j]; for (std::size_t k = 0; k < PSQTBuckets; ++k) entry.psqtAccumulation[k] -= featureTransformer.psqtWeights[index * PSQTBuckets + k]; } for (const auto index : added) { const IndexType offset = Dimensions * index; for (IndexType j = 0; j < Dimensions; ++j) entry.accumulation[j] += featureTransformer.weights[offset + j]; for (std::size_t k = 0; k < PSQTBuckets; ++k) entry.psqtAccumulation[k] += featureTransformer.psqtWeights[index * PSQTBuckets + k]; } // The accumulator of the refresh entry has been updated. // Now copy its content to the actual accumulator we were refreshing. std::memcpy(accumulator.accumulation[Perspective], entry.accumulation, sizeof(BiasType) * Dimensions); std::memcpy(accumulator.psqtAccumulation[Perspective], entry.psqtAccumulation, sizeof(int32_t) * PSQTBuckets); #endif for (Color c : {WHITE, BLACK}) entry.byColorBB[c] = pos.pieces(c); for (PieceType pt = PAWN; pt <= KING; ++pt) entry.byTypeBB[pt] = pos.pieces(pt); } } }