diff --git a/src/Makefile b/src/Makefile index 3622b5898..7faac5196 100644 --- a/src/Makefile +++ b/src/Makefile @@ -80,14 +80,14 @@ SRCS = attacks.cpp benchmark.cpp bitboard.cpp evaluate.cpp main.cpp \ misc.cpp movegen.cpp movepick.cpp position.cpp \ search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp \ nnue/nnue_accumulator.cpp nnue/nnue_misc.cpp nnue/network.cpp \ - nnue/features/half_ka_v2_hm.cpp nnue/features/full_threats.cpp \ + nnue/features/half_ka_v2_hm.cpp nnue/features/full_threats.cpp nnue/features/pp_3wide.cpp \ engine.cpp score.cpp memory.cpp OTHER_SRCS = universal/entry_x86.cpp universal/entry_arm64.cpp universal/entry_riscv64.cpp universal/nnue_embed.cpp HEADERS = attacks.h benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h history.h \ nnue/nnue_misc.h nnue/features/half_ka_v2_hm.h nnue/features/full_threats.h \ - nnue/layers/affine_transform.h nnue/layers/affine_transform_sparse_input.h \ + nnue/features/pp_3wide.h nnue/layers/affine_transform.h nnue/layers/affine_transform_sparse_input.h \ nnue/layers/clipped_relu.h nnue/layers/sqr_clipped_relu.h nnue/nnue_accumulator.h \ nnue/nnue_architecture.h nnue/nnue_common.h nnue/nnue_feature_transformer.h nnue/simd.h \ nnue/nnz_helper.h position.h search.h syzygy/tbprobe.h thread.h thread_native.h timeman.h \ diff --git a/src/bitboard.h b/src/bitboard.h index 49989d2e3..66b1a8e8a 100644 --- a/src/bitboard.h +++ b/src/bitboard.h @@ -20,6 +20,7 @@ #define BITBOARD_H_INCLUDED #include +#include #include #include #include @@ -127,8 +128,23 @@ constexpr Bitboard pawn_single_push_bb(Color c, Bitboard b) { return c == WHITE ? shift(b) : shift(b); } -constexpr int edge_distance(File f) { return std::min(f, File(FILE_H - f)); } +inline constexpr auto PawnPairBB = []() { + std::array result{}; + for (Square s = SQ_A1; s <= SQ_H8; ++s) + { + Bitboard file = file_bb(s); + Bitboard files = file | shift(file) | shift(file); + result[s] = files & ~(Rank1BB | Rank8BB) & ~square_bb(s); + } + return result; +}(); +// Returns the squares that can host a pawn forming a "pawn pair" with a pawn +// on s: own file plus adjacent files, restricted to ranks 2-7, excluding s. +// The geometry is color-independent. +constexpr Bitboard pawn_pair_bb(Square s) { return PawnPairBB[s]; } + +constexpr int edge_distance(File f) { return std::min(f, File(FILE_H - f)); } template constexpr int constexpr_popcount(T v) { diff --git a/src/evaluate.h b/src/evaluate.h index e62cec409..02f37fff8 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -33,7 +33,7 @@ namespace Eval { // for the build process (profile-build and fishtest) to work. Do not change the // name of the macro or the location where this macro is defined, as it is used // in the Makefile/Fishtest. -#define EvalFileDefaultName "nn-0ee0657fb25e.nnue" +#define EvalFileDefaultName "nn-89cb98a217f7.nnue" namespace NNUE { class Network; diff --git a/src/nnue/features/full_threats.cpp b/src/nnue/features/full_threats.cpp index 468c168a8..7fd3ca7b7 100644 --- a/src/nnue/features/full_threats.cpp +++ b/src/nnue/features/full_threats.cpp @@ -21,7 +21,6 @@ #include "full_threats.h" #include -#include #include #include #include @@ -73,7 +72,7 @@ constexpr auto make_piece_indices_piece() { for (Square from = SQ_A1; from <= SQ_H8; ++from) { - Bitboard attacks = Attacks::PawnPushOrAttacks[C][from]; + Bitboard attacks = Attacks::PseudoAttacks[C][from]; for (Square to = SQ_A1; to <= SQ_H8; ++to) { @@ -136,8 +135,8 @@ constexpr auto init_threat_offsets() { else if (from >= SQ_A2 && from <= SQ_H7) { - Bitboard attacks = (pieceIdx < 8) ? Attacks::PawnPushOrAttacks[WHITE][from] - : Attacks::PawnPushOrAttacks[BLACK][from]; + Bitboard attacks = (pieceIdx < 8) ? Attacks::PseudoAttacks[WHITE][from] + : Attacks::PseudoAttacks[BLACK][from]; cumulativePieceOffset += constexpr_popcount(attacks); } } @@ -208,11 +207,9 @@ inline sf_always_inline IndexType FullThreats::make_index( // Get a list of indices for active features in ascending order void FullThreats::append_active_indices(Color perspective, const Position& pos, IndexList& active) { - const Square ksq = pos.square(perspective); - const Bitboard occupied = pos.pieces(); - const Bitboard pawns = pos.pieces(PAWN); - - const Bitboard pawnTargets = pos.pieces(PAWN, KNIGHT, ROOK); + const Square ksq = pos.square(perspective); + const Bitboard occupied = pos.pieces(); + const Bitboard pawnTargets = pos.pieces(KNIGHT, ROOK); const Bitboard minorSliderTargets = pos.pieces(PAWN, KNIGHT, BISHOP, ROOK); const Bitboard queenTargets = pos.pieces(PAWN, KNIGHT, BISHOP, ROOK, QUEEN); @@ -221,18 +218,14 @@ void FullThreats::append_active_indices(Color perspective, const Position& pos, const Color c = Color(perspective ^ color); { - const Piece attacker = make_piece(c, PAWN); - const Bitboard cPawns = pos.pieces(c, PAWN); - // Set of pawns which are prevented from movement by a pawn in front of them - const Bitboard pushers = pawn_single_push_bb(~c, pawns) & cPawns; - - auto process_pawn_attacks = [&](Bitboard attacks, Direction attkDir) { + const Piece attacker = make_piece(c, PAWN); + const Bitboard cPawns = pos.pieces(c, PAWN); + auto process_pawn_attacks = [&](Bitboard attacks, Direction attkDir) { while (attacks) { - Square to = pop_lsb(attacks); - Square from = to - attkDir; - Piece attacked = pos.piece_on(to); - assert(file_of(from) != file_of(to) || type_of(attacked) == PAWN); + Square to = pop_lsb(attacks); + Square from = to - attkDir; + Piece attacked = pos.piece_on(to); IndexType index = make_index(perspective, attacker, from, to, attacked, ksq); active.push_back_if_lt(index, Dimensions); } @@ -242,13 +235,11 @@ void FullThreats::append_active_indices(Color perspective, const Position& pos, { process_pawn_attacks(shift(cPawns) & pawnTargets, NORTH_EAST); process_pawn_attacks(shift(cPawns) & pawnTargets, NORTH_WEST); - process_pawn_attacks(shift(pushers), NORTH); } else { process_pawn_attacks(shift(cPawns) & pawnTargets, SOUTH_WEST); process_pawn_attacks(shift(cPawns) & pawnTargets, SOUTH_EAST); - process_pawn_attacks(shift(pushers), SOUTH); } } diff --git a/src/nnue/features/full_threats.h b/src/nnue/features/full_threats.h index 5cc4d977b..1ef5e7fc9 100644 --- a/src/nnue/features/full_threats.h +++ b/src/nnue/features/full_threats.h @@ -29,16 +29,18 @@ class Position; namespace Stockfish::Eval::NNUE::Features { -static constexpr int numValidTargets[PIECE_NB] = {0, 6, 10, 8, 8, 10, 0, 0, - 0, 6, 10, 8, 8, 10, 0, 0}; +// Pawn diagonal threats only target knights and rooks (pawn-pawn relationships +// are handled by the PP_3Wide feature set), so pawns have 4 valid targets. +static constexpr int numValidTargets[PIECE_NB] = {0, 4, 10, 8, 8, 10, 0, 0, + 0, 4, 10, 8, 8, 10, 0, 0}; class FullThreats { public: // Hash value embedded in the evaluation file - static constexpr u32 HashValue = 0x8f234cb8u; + static constexpr u32 HashValue = 0x2e6b9d04u; // Number of feature dimensions - static constexpr IndexType Dimensions = 60720; + static constexpr IndexType Dimensions = 59808; // clang-format off // Orient a square according to perspective (rotates by 180 for black) @@ -54,7 +56,7 @@ class FullThreats { }; static constexpr int map[PIECE_TYPE_NB-2][PIECE_TYPE_NB-2] = { - { 0, 1, -1, 2, -1, -1}, + {-1, 0, -1, 1, -1, -1}, { 0, 1, 2, 3, 4, -1}, { 0, 1, 2, 3, -1, -1}, { 0, 1, 2, 3, -1, -1}, @@ -64,9 +66,8 @@ class FullThreats { // clang-format on // Maximum number of simultaneously active features. - static constexpr IndexType MaxActiveDimensions = 128; - using IndexList = ValueList; - using DiffType = DirtyThreats; + using IndexList = ValueList; + using DiffType = DirtyThreats; static IndexType make_index(Color perspective, Piece attkr, Square from, Square to, Piece attkd, Square ksq); diff --git a/src/nnue/features/pp_3wide.cpp b/src/nnue/features/pp_3wide.cpp new file mode 100644 index 000000000..ec129ebf3 --- /dev/null +++ b/src/nnue/features/pp_3wide.cpp @@ -0,0 +1,171 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2026 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 "pp_3wide.h" + +#include +#include +#include + +#include "full_threats.h" +#include "../../bitboard.h" +#include "../../misc.h" +#include "../../position.h" +#include "../../types.h" +#include "../nnue_common.h" + +namespace Stockfish::Eval::NNUE::Features { + +constexpr IndexType make_pawn_id(Color color, Square square) { + assert(square >= SQ_A2 && square <= SQ_H7); + return 48 * int(color) + square - SQ_A2; +} + +#ifdef USE_AVX512ICL +static inline __m256i pp_idx_epi16(__m256i a, __m256i b) { + const __m256i hi = _mm256_max_epu16(a, b); + const __m256i lo = _mm256_min_epu16(a, b); + const __m256i prod = _mm256_mullo_epi16(hi, _mm256_sub_epi16(hi, _mm256_set1_epi16(1))); + return _mm256_add_epi16(_mm256_add_epi16(_mm256_srli_epi16(prod, 1), lo), + _mm256_set1_epi16(i16(PP_3Wide::IndexBase))); +} +#endif + +inline sf_always_inline IndexType PP_3Wide::make_index( + Color perspective, Color color, Square from, Square to, Color pairedColor, Square ksq) { + const i8 orientation = FullThreats::OrientTBL[ksq] ^ (56 * perspective); + unsigned from_oriented = u8(from) ^ orientation; + unsigned to_oriented = u8(to) ^ orientation; + + Color color_oriented = Color(color ^ perspective); + Color pairedColor_oriented = Color(pairedColor ^ perspective); + + assert(from_oriented >= SQ_A2 && from_oriented <= SQ_H7); + assert(to_oriented >= SQ_A2 && to_oriented <= SQ_H7); + + const IndexType idA = make_pawn_id(color_oriented, Square(from_oriented)); + const IndexType idB = make_pawn_id(pairedColor_oriented, Square(to_oriented)); + const IndexType hi = std::max(idA, idB); + const IndexType lo = std::min(idA, idB); + + return hi * (hi - 1) / 2 + lo + IndexBase; +} + +void PP_3Wide::append_active_indices(Color perspective, const Position& pos, IndexList& active) { + const Square ksq = pos.square(perspective); + const Bitboard white = pos.pieces(WHITE, PAWN); + const Bitboard black = pos.pieces(BLACK, PAWN); + + Bitboard bb = white; + while (bb) + { + Square from = pop_lsb(bb); + const Bitboard band = pawn_pair_bb(from); + for (Bitboard ww = band & bb; ww;) + active.push_back(make_index(perspective, WHITE, from, pop_lsb(ww), WHITE, ksq)); + for (Bitboard wb = band & black; wb;) + active.push_back(make_index(perspective, WHITE, from, pop_lsb(wb), BLACK, ksq)); + } + + bb = black; + while (bb) + { + Square from = pop_lsb(bb); + const Bitboard band = pawn_pair_bb(from); + for (Bitboard bbk = band & bb; bbk;) + active.push_back(make_index(perspective, BLACK, from, pop_lsb(bbk), BLACK, ksq)); + } +} + +void PP_3Wide::append_changed_indices(Color perspective, + Square ksq, + const DiffType& diff, + IndexList& removed, + IndexList& added, + [[maybe_unused]] const ThreatWeightType* prefetchBase, + [[maybe_unused]] IndexType prefetchStride) { + + const Bitboard whiteBefore = diff.before[WHITE]; + const Bitboard blackBefore = diff.before[BLACK]; + const Bitboard whiteAfter = diff.after[WHITE]; + const Bitboard blackAfter = diff.after[BLACK]; + + if (whiteBefore == whiteAfter && blackBefore == blackAfter) + return; + +#ifdef USE_AVX512ICL + const u8 orientation = u8(FullThreats::OrientTBL[ksq]) ^ u8(56 * perspective); + const __m512i iota = AllSquares; + const __m512i adjusted = + _mm512_sub_epi8(_mm512_xor_si512(iota, _mm512_set1_epi8(orientation)), _mm512_set1_epi8(8)); + + auto generate = [&](Bitboard updatedW, Bitboard updatedB, Bitboard pawnsW, Bitboard pawnsB, + IndexList& out) { + const Bitboard friendly = perspective == WHITE ? pawnsW : pawnsB; + const Bitboard enemy = perspective == WHITE ? pawnsB : pawnsW; + const __m512i ids = _mm512_mask_blend_epi8( + friendly, _mm512_add_epi8(adjusted, _mm512_set1_epi8(48)), adjusted); + + const Bitboard unchanged = (pawnsW | pawnsB) & ~(updatedW | updatedB); + for (Bitboard u = updatedW | updatedB; u;) + { + const Square a = pop_lsb(u); + const Bitboard partners = pawn_pair_bb(a) & (unchanged | u); + const int n = popcount(partners); + if (!n) + continue; + + const u16 colorOff = (enemy & a) ? 48 : 0; + const u16 aId = u16(((u8(a) ^ orientation) - 8) + colorOff); + const __m256i pids = _mm256_cvtepu8_epi16( + _mm512_castsi512_si128(_mm512_maskz_compress_epi8(partners, ids))); + const __m256i feats = pp_idx_epi16(_mm256_set1_epi16(aId), pids); + + u16* w = out.make_space(n); + _mm256_storeu_epi16(w, feats); + } + }; +#else + auto generate = [&](Bitboard updatedW, Bitboard updatedB, Bitboard pawnsW, Bitboard pawnsB, + IndexList& out) { + auto push = [&](IndexType index) { + if (prefetchBase) + prefetch(reinterpret_cast( + reinterpret_cast(prefetchBase) + index * prefetchStride)); + out.push_back(index); + }; + const Bitboard unchanged = (pawnsW | pawnsB) & ~(updatedW | updatedB); + for (Bitboard u = updatedW | updatedB; u;) + { + const Square a = pop_lsb(u); + const Bitboard mask = pawn_pair_bb(a) & (unchanged | u); + const Color aCol = (pawnsB & a) ? BLACK : WHITE; + for (Bitboard pb = pawnsB & mask; pb;) + push(make_index(perspective, aCol, a, pop_lsb(pb), BLACK, ksq)); + for (Bitboard pw = pawnsW & mask; pw;) + push(make_index(perspective, aCol, a, pop_lsb(pw), WHITE, ksq)); + } + }; +#endif + + generate(whiteAfter & ~whiteBefore, blackAfter & ~blackBefore, whiteAfter, blackAfter, added); + generate(whiteBefore & ~whiteAfter, blackBefore & ~blackAfter, whiteBefore, blackBefore, + removed); +} + +} // namespace Stockfish::Eval::NNUE::Features diff --git a/src/nnue/features/pp_3wide.h b/src/nnue/features/pp_3wide.h new file mode 100644 index 000000000..fda913376 --- /dev/null +++ b/src/nnue/features/pp_3wide.h @@ -0,0 +1,61 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2026 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 . +*/ + +#ifndef NNUE_FEATURES_PP_3WIDE_INCLUDED +#define NNUE_FEATURES_PP_3WIDE_INCLUDED + +#include "../../misc.h" +#include "../../types.h" +#include "../nnue_common.h" + +namespace Stockfish { +class Position; +} + +namespace Stockfish::Eval::NNUE::Features { + +class PP_3Wide { + public: + static constexpr u32 HashValue = 0x86f2b1ddu; + + static constexpr IndexType PawnIds = COLOR_NB * 48; + static constexpr IndexType Dimensions = PawnIds * (PawnIds - 1) / 2; + + // Pawn pair feature indices are concatenated to threats, so this must equal ThreatFeatureSet::Dimensions; + // see nnue_feature_transformer.h + static constexpr IndexType IndexBase = 59808; + using IndexList = ValueList; + using DiffType = DirtyPawnPairs; + + static IndexType make_index( + Color perspective, Color color, Square from, Square to, Color pairedColor, Square ksq); + + static void append_active_indices(Color perspective, const Position& pos, IndexList& active); + + static void append_changed_indices(Color perspective, + Square ksq, + const DiffType& diff, + IndexList& removed, + IndexList& added, + const ThreatWeightType* prefetchBase = nullptr, + IndexType prefetchStride = 0); +}; + +} // namespace Stockfish::Eval::NNUE::Features + +#endif // #ifndef NNUE_FEATURES_PP_3WIDE_INCLUDED diff --git a/src/nnue/nnue_accumulator.cpp b/src/nnue/nnue_accumulator.cpp index ec3f2022e..74b45fc40 100644 --- a/src/nnue/nnue_accumulator.cpp +++ b/src/nnue/nnue_accumulator.cpp @@ -57,17 +57,19 @@ AccumulatorState& AccumulatorStack::mut_latest() noexcept { return accumulators[ void AccumulatorStack::reset() noexcept { accumulators[0].dirtyPiece = {}; new (&accumulators[0].dirtyThreats) DirtyThreats; + new (&accumulators[0].dirtyPawnPairs) DirtyPawnPairs; accumulators[0].computed.fill(false); size = 1; } -std::pair AccumulatorStack::push() noexcept { +Dirties& AccumulatorStack::push() noexcept { assert(size < MaxSize); auto& st = accumulators[size]; st.computed.fill(false); new (&st.dirtyThreats) DirtyThreats; + new (&st.dirtyPawnPairs) DirtyPawnPairs; size++; - return {st.dirtyPiece, st.dirtyThreats}; + return st; } void AccumulatorStack::pop() noexcept { @@ -177,8 +179,8 @@ void apply_combined(Color perspective, vec_t acc[Tiling::NumRegs]; psqt_vec_t psqt[Tiling::NumPsqtRegs]; - const auto* psqWeights = &featureTransformer.weights[0]; - const auto* threatWeights = &featureTransformer.threatWeights[0]; + const auto* psqWeights = &featureTransformer.weights[0]; + const auto* threatAndPpWeights = &featureTransformer.threatAndPpWeights[0]; for (IndexType j = 0; j < Dimensions / Tiling::TileHeight; ++j) { @@ -208,7 +210,7 @@ void apply_combined(Color perspective, for (int i = 0; i < thrRemoved.ssize(); ++i) { auto* column = reinterpret_cast( - &threatWeights[thrRemoved[i] * Dimensions + tileOff]); + &threatAndPpWeights[thrRemoved[i] * Dimensions + tileOff]); #ifdef USE_NEON for (IndexType k = 0; k < Tiling::NumRegs; k += 2) @@ -224,8 +226,8 @@ void apply_combined(Color perspective, for (int i = 0; i < thrAdded.ssize(); ++i) { - auto* column = - reinterpret_cast(&threatWeights[thrAdded[i] * Dimensions + tileOff]); + auto* column = reinterpret_cast( + &threatAndPpWeights[thrAdded[i] * Dimensions + tileOff]); #ifdef USE_NEON for (IndexType k = 0; k < Tiling::NumRegs; k += 2) @@ -271,7 +273,8 @@ void apply_combined(Color perspective, for (int i = 0; i < thrRemoved.ssize(); ++i) { auto* columnPsqt = reinterpret_cast( - &featureTransformer.threatPsqtWeights[thrRemoved[i] * PSQTBuckets + psqtTileOff]); + &featureTransformer + .threatAndPpPsqtWeights[thrRemoved[i] * PSQTBuckets + psqtTileOff]); for (usize k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = vec_sub_psqt_32(psqt[k], columnPsqt[k]); } @@ -279,7 +282,7 @@ void apply_combined(Color perspective, for (int i = 0; i < thrAdded.ssize(); ++i) { auto* columnPsqt = reinterpret_cast( - &featureTransformer.threatPsqtWeights[thrAdded[i] * PSQTBuckets + psqtTileOff]); + &featureTransformer.threatAndPpPsqtWeights[thrAdded[i] * PSQTBuckets + psqtTileOff]); for (usize k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); } @@ -293,9 +296,9 @@ void apply_combined(Color perspective, usize tileOffset = 0; const auto* psqWeights = &featureTransformer.weights[0]; - const auto* threatWeights = &featureTransformer.threatWeights[0]; + const auto* threatWeights = &featureTransformer.threatAndPpWeights[0]; const auto* psqtWeights = &featureTransformer.psqtWeights[0]; - const auto* threatPsqtWeights = &featureTransformer.threatPsqtWeights[0]; + const auto* threatPsqtWeights = &featureTransformer.threatAndPpPsqtWeights[0]; while (tileOffset < Dimensions) { @@ -373,18 +376,18 @@ void apply_combined(Color perspective, { const IndexType offset = Dimensions * index; for (IndexType j = 0; j < Dimensions; ++j) - toAcc[j] -= featureTransformer.threatWeights[offset + j]; + toAcc[j] -= featureTransformer.threatAndPpWeights[offset + j]; for (usize k = 0; k < PSQTBuckets; ++k) - toPsqtAcc[k] -= featureTransformer.threatPsqtWeights[index * PSQTBuckets + k]; + toPsqtAcc[k] -= featureTransformer.threatAndPpPsqtWeights[index * PSQTBuckets + k]; } for (const auto index : thrAdded) { const IndexType offset = Dimensions * index; for (IndexType j = 0; j < Dimensions; ++j) - toAcc[j] += featureTransformer.threatWeights[offset + j]; + toAcc[j] += featureTransformer.threatAndPpWeights[offset + j]; for (usize k = 0; k < PSQTBuckets; ++k) - toPsqtAcc[k] += featureTransformer.threatPsqtWeights[index * PSQTBuckets + k]; + toPsqtAcc[k] += featureTransformer.threatAndPpPsqtWeights[index * PSQTBuckets + k]; } #endif @@ -407,22 +410,28 @@ void update_accumulator_incremental(Color perspective, PSQFeatureSet::IndexList psqRemoved, psqAdded; ThreatFeatureSet::IndexList thrRemoved, thrAdded; - const auto& dirtyPiece = Forward ? target_state.dirtyPiece : computed.dirtyPiece; - const auto& dirtyThreats = Forward ? target_state.dirtyThreats : computed.dirtyThreats; + const auto& dirtyPiece = Forward ? target_state.dirtyPiece : computed.dirtyPiece; + const auto& dirtyThreats = Forward ? target_state.dirtyThreats : computed.dirtyThreats; + const auto& dirtyPawnPairs = Forward ? target_state.dirtyPawnPairs : computed.dirtyPawnPairs; - const auto* pfBase = &featureTransformer.threatWeights[0]; - IndexType pfStride = FeatureTransformer::OutputDimensions; + // Used solely for prefetching + const auto* threatPpBase = &featureTransformer.threatAndPpWeights[0]; + IndexType pfStride = FeatureTransformer::OutputDimensions; if constexpr (Forward) { ThreatFeatureSet::append_changed_indices(perspective, ksq, dirtyThreats, thrRemoved, - thrAdded, pfBase, pfStride); + thrAdded, threatPpBase, pfStride); + PairFeatureSet::append_changed_indices(perspective, ksq, dirtyPawnPairs, thrRemoved, + thrAdded, threatPpBase, pfStride); PSQFeatureSet::append_changed_indices(perspective, ksq, dirtyPiece, psqRemoved, psqAdded); } else { ThreatFeatureSet::append_changed_indices(perspective, ksq, dirtyThreats, thrAdded, - thrRemoved, pfBase, pfStride); + thrRemoved, threatPpBase, pfStride); + PairFeatureSet::append_changed_indices(perspective, ksq, dirtyPawnPairs, thrAdded, + thrRemoved, threatPpBase, pfStride); PSQFeatureSet::append_changed_indices(perspective, ksq, dirtyPiece, psqAdded, psqRemoved); } @@ -575,6 +584,7 @@ void update_accumulator_refresh_cache(Color perspective, ThreatFeatureSet::IndexList active; ThreatFeatureSet::append_active_indices(perspective, pos, active); + PairFeatureSet::append_active_indices(perspective, pos, active); accumulator.computed[perspective] = true; @@ -582,8 +592,8 @@ void update_accumulator_refresh_cache(Color perspective, vec_t acc[Tiling::NumRegs]; psqt_vec_t psqt[Tiling::NumPsqtRegs]; - const auto* weights = &featureTransformer.weights[0]; - const auto* threatWeights = &featureTransformer.threatWeights[0]; + const auto* weights = &featureTransformer.weights[0]; + const auto* threatAndPpWeights = &featureTransformer.threatAndPpWeights[0]; for (IndexType j = 0; j < Dimensions / Tiling::TileHeight; ++j) { @@ -614,8 +624,8 @@ void update_accumulator_refresh_cache(Color perspective, for (int i = 0; i < active.ssize(); ++i) { - auto* column = - reinterpret_cast(&threatWeights[active[i] * Dimensions + tileOff]); + auto* column = reinterpret_cast( + &threatAndPpWeights[active[i] * Dimensions + tileOff]); #ifdef USE_NEON for (IndexType k = 0; k < Tiling::NumRegs; k += 2) @@ -664,7 +674,7 @@ void update_accumulator_refresh_cache(Color perspective, for (int i = 0; i < active.ssize(); ++i) { auto* columnPsqt = reinterpret_cast( - &featureTransformer.threatPsqtWeights[active[i] * PSQTBuckets + psqtTileOff]); + &featureTransformer.threatAndPpPsqtWeights[active[i] * PSQTBuckets + psqtTileOff]); for (usize k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); } @@ -676,9 +686,9 @@ void update_accumulator_refresh_cache(Color perspective, #elif defined(USE_RVV) const auto* weights = &featureTransformer.weights[0]; - const auto* threatWeights = &featureTransformer.threatWeights[0]; + const auto* threatWeights = &featureTransformer.threatAndPpWeights[0]; const auto* psqtWeights = &featureTransformer.psqtWeights[0]; - const auto* threatPsqtWeights = &featureTransformer.threatPsqtWeights[0]; + const auto* threatPsqtWeights = &featureTransformer.threatAndPpPsqtWeights[0]; usize tileOffset = 0; @@ -763,11 +773,11 @@ void update_accumulator_refresh_cache(Color perspective, for (IndexType j = 0; j < Dimensions; ++j) accumulator.accumulation[perspective][j] += - featureTransformer.threatWeights[offset + j]; + featureTransformer.threatAndPpWeights[offset + j]; for (usize k = 0; k < PSQTBuckets; ++k) accumulator.psqtAccumulation[perspective][k] += - featureTransformer.threatPsqtWeights[index * PSQTBuckets + k]; + featureTransformer.threatAndPpPsqtWeights[index * PSQTBuckets + k]; } #endif diff --git a/src/nnue/nnue_accumulator.h b/src/nnue/nnue_accumulator.h index 10c1b5cd2..5e7e719da 100644 --- a/src/nnue/nnue_accumulator.h +++ b/src/nnue/nnue_accumulator.h @@ -24,7 +24,6 @@ #include #include #include -#include #include "../types.h" #include "../misc.h" @@ -90,10 +89,7 @@ struct AccumulatorCaches { }; -struct AccumulatorState: public Accumulator { - DirtyPiece dirtyPiece; - DirtyThreats dirtyThreats; -}; +struct AccumulatorState: public Accumulator, Dirties {}; class AccumulatorStack { public: @@ -101,9 +97,9 @@ class AccumulatorStack { [[nodiscard]] const AccumulatorState& latest() const noexcept; - void reset() noexcept; - std::pair push() noexcept; - void pop() noexcept; + void reset() noexcept; + Dirties& push() noexcept; + void pop() noexcept; void evaluate(const Position& pos, const FeatureTransformer& featureTransformer, diff --git a/src/nnue/nnue_architecture.h b/src/nnue/nnue_architecture.h index f3ccfdabd..f9ea70211 100644 --- a/src/nnue/nnue_architecture.h +++ b/src/nnue/nnue_architecture.h @@ -26,6 +26,7 @@ #include "features/half_ka_v2_hm.h" #include "features/full_threats.h" +#include "features/pp_3wide.h" #include "layers/affine_transform.h" #include "layers/affine_transform_sparse_input.h" #include "layers/clipped_relu.h" @@ -37,6 +38,7 @@ namespace Stockfish::Eval::NNUE { // Input features used in evaluation function using ThreatFeatureSet = Features::FullThreats; +using PairFeatureSet = Features::PP_3Wide; using PSQFeatureSet = Features::HalfKAv2_hm; // Number of input feature dimensions after conversion diff --git a/src/nnue/nnue_common.h b/src/nnue/nnue_common.h index 14bdd8783..abba88805 100644 --- a/src/nnue/nnue_common.h +++ b/src/nnue/nnue_common.h @@ -187,12 +187,9 @@ inline void write_little_endian(std::ostream& stream, const IntType* values, usi // Read N signed integers from the stream s, putting them in the array out. // The stream is assumed to be compressed using the signed LEB128 format. // See https://en.wikipedia.org/wiki/LEB128 for a description of the compression scheme. -template -inline void read_leb_128_detail(std::istream& stream, - std::array& out, - u32& bytes_left, - BufType& buf, - u32& buf_pos) { +template +inline void read_leb_128_detail( + std::istream& stream, IntType* out, usize Count, u32& bytes_left, BufType& buf, u32& buf_pos) { static_assert(std::is_signed_v, "Not implemented for unsigned types"); static_assert(sizeof(IntType) <= 4, "Not implemented for types larger than 32 bit"); @@ -233,7 +230,24 @@ inline void read_leb_128(std::istream& stream, Arrays&... outs) { std::array buf; u32 buf_pos = u32(buf.size()); - (read_leb_128_detail(stream, outs, bytes_left, buf, buf_pos), ...); + (read_leb_128_detail(stream, outs.data(), outs.size(), bytes_left, buf, buf_pos), ...); + + assert(bytes_left == 0); +} + + +template +inline void read_leb_128(std::istream& stream, IntType* out, usize expected) { + // Check the presence of our LEB128 magic string + char leb128MagicString[Leb128MagicStringSize]; + stream.read(leb128MagicString, Leb128MagicStringSize); + assert(strncmp(Leb128MagicString, leb128MagicString, Leb128MagicStringSize) == 0); + + auto bytes_left = read_little_endian(stream); + std::array buf; + u32 buf_pos = u32(buf.size()); + + read_leb_128_detail(stream, out, expected, bytes_left, buf, buf_pos); assert(bytes_left == 0); } @@ -243,8 +257,8 @@ inline void read_leb_128(std::istream& stream, Arrays&... outs) { // This takes N integers from array values, compresses them with // the LEB128 algorithm and writes the result on the stream s. // See https://en.wikipedia.org/wiki/LEB128 for a description of the compression scheme. -template -inline void write_leb_128(std::ostream& stream, const std::array& values) { +template +inline void write_leb_128(std::ostream& stream, const IntType* values, usize Count) { // Write our LEB128 magic string stream.write(Leb128MagicString, Leb128MagicStringSize); @@ -303,6 +317,11 @@ inline void write_leb_128(std::ostream& stream, const std::array flush(); } +template +inline void write_leb_128(std::ostream& stream, const std::array& values) { + write_leb_128(stream, values.data(), Count); +} + } // namespace Stockfish::Eval::NNUE #endif // #ifndef NNUE_COMMON_H_INCLUDED diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h index 7edf2b6c8..fa5c76b03 100644 --- a/src/nnue/nnue_feature_transformer.h +++ b/src/nnue/nnue_feature_transformer.h @@ -87,7 +87,9 @@ class FeatureTransformer { // Number of input/output dimensions static constexpr IndexType ThreatInputDimensions = ThreatFeatureSet::Dimensions; - static constexpr IndexType InputDimensions = PSQFeatureSet::Dimensions + ThreatInputDimensions; + static constexpr IndexType PairInputDimensions = PairFeatureSet::Dimensions; + static constexpr IndexType InputDimensions = + PSQFeatureSet::Dimensions + ThreatInputDimensions + PairInputDimensions; static constexpr IndexType OutputDimensions = HalfDimensions; // Size of forward propagation buffer @@ -128,7 +130,8 @@ class FeatureTransformer { // Hash value embedded in the evaluation file static constexpr u32 get_hash_value() { - return combine_hash({ThreatFeatureSet::HashValue, PSQFeatureSet::HashValue}) + return combine_hash( + {ThreatFeatureSet::HashValue, PairFeatureSet::HashValue, PSQFeatureSet::HashValue}) ^ (OutputDimensions * 2); } @@ -136,22 +139,42 @@ class FeatureTransformer { permute<16>(biases, PackusEpi16Order); permute<16>(weights, PackusEpi16Order); - permute<8>(threatWeights, PackusEpi16Order); + permute<8>(threatAndPpWeights, PackusEpi16Order); } void unpermute_weights() { permute<16>(biases, InversePackusEpi16Order); permute<16>(weights, InversePackusEpi16Order); - permute<8>(threatWeights, InversePackusEpi16Order); + permute<8>(threatAndPpWeights, InversePackusEpi16Order); } + auto threatWeights() { return threatAndPpWeights.data(); } + auto threatWeights() const { return threatAndPpWeights.data(); } + auto ppWeights() { return &threatAndPpWeights[ThreatFeatureSet::Dimensions * HalfDimensions]; } + auto ppWeights() const { + return &threatAndPpWeights[ThreatFeatureSet::Dimensions * HalfDimensions]; + } + + auto threatPsqtWeights() { return threatAndPpPsqtWeights.data(); } + auto threatPsqtWeights() const { return threatAndPpPsqtWeights.data(); } + auto ppPsqtWeights() { + return &threatAndPpPsqtWeights[ThreatFeatureSet::Dimensions * PSQTBuckets]; + } + auto ppPsqtWeights() const { + return &threatAndPpPsqtWeights[ThreatFeatureSet::Dimensions * PSQTBuckets]; + } + + // Read network parameters bool read_parameters(std::istream& stream) { read_leb_128(stream, biases); - read_little_endian(stream, threatWeights.data(), + read_little_endian(stream, threatWeights(), ThreatInputDimensions * HalfDimensions); - read_leb_128(stream, threatPsqtWeights); + read_leb_128(stream, threatPsqtWeights(), ThreatFeatureSet::Dimensions * PSQTBuckets); + read_little_endian(stream, ppWeights(), + PairInputDimensions * HalfDimensions); + read_leb_128(stream, ppPsqtWeights(), PairFeatureSet::Dimensions * PSQTBuckets); read_leb_128(stream, weights); read_leb_128(stream, psqtWeights); @@ -170,9 +193,14 @@ class FeatureTransformer { write_leb_128(stream, copy->biases); - write_little_endian(stream, copy->threatWeights.data(), + write_little_endian(stream, copy->threatWeights(), ThreatInputDimensions * HalfDimensions); - write_leb_128(stream, copy->threatPsqtWeights); + write_leb_128(stream, copy->threatPsqtWeights(), + ThreatFeatureSet::Dimensions * PSQTBuckets); + write_little_endian(stream, copy->ppWeights(), + PairInputDimensions * HalfDimensions); + write_leb_128(stream, copy->ppPsqtWeights(), + PairFeatureSet::Dimensions * PSQTBuckets); write_leb_128(stream, copy->weights); write_leb_128(stream, copy->psqtWeights); @@ -187,8 +215,8 @@ class FeatureTransformer { hash_combine(h, get_raw_data_hash(weights)); hash_combine(h, get_raw_data_hash(psqtWeights)); - hash_combine(h, get_raw_data_hash(threatWeights)); - hash_combine(h, get_raw_data_hash(threatPsqtWeights)); + hash_combine(h, get_raw_data_hash(threatAndPpWeights)); + hash_combine(h, get_raw_data_hash(threatAndPpPsqtWeights)); hash_combine(h, get_hash_value()); @@ -399,12 +427,20 @@ class FeatureTransformer { alignas(CacheLineSize) std::array biases; alignas( CacheLineSize) std::array weights; + + // Threats and pawn-pair features are concatenated into one array to allow for a single index to address either. + // The first pawn-pair feature is at index ThreatFeatureSet::Dimensions. + static_assert(PairFeatureSet::IndexBase == ThreatFeatureSet::Dimensions); + + alignas(CacheLineSize) std::array threatAndPpWeights; alignas(CacheLineSize) - std::array threatWeights; - alignas(CacheLineSize) - std::array psqtWeights; - alignas(CacheLineSize) - std::array threatPsqtWeights; + std::array psqtWeights; + // As above + alignas(CacheLineSize) std::array threatAndPpPsqtWeights; }; } // namespace Stockfish::Eval::NNUE diff --git a/src/position.cpp b/src/position.cpp index 69de6267e..a8585bc2f 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -817,8 +817,7 @@ bool Position::gives_check(Move m) const { void Position::do_move(Move m, StateInfo& newSt, bool givesCheck, - DirtyPiece& dp, - DirtyThreats& dts, + Dirties& dirties, const TranspositionTable* tt = nullptr, const SharedHistories* history = nullptr) { @@ -840,6 +839,13 @@ void Position::do_move(Move m, ++st->rule50; ++st->pliesFromNull; + auto& dpps = dirties.dirtyPawnPairs; + auto& dts = dirties.dirtyThreats; + auto& dp = dirties.dirtyPiece; + + dpps.before[WHITE] = pieces(WHITE, PAWN); + dpps.before[BLACK] = pieces(BLACK, PAWN); + Color us = sideToMove; Color them = ~us; Square from = m.from_sq(); @@ -1065,6 +1071,9 @@ void Position::do_move(Move m, assert(pos_is_ok()); + dpps.after[WHITE] = pieces(WHITE, PAWN); + dpps.after[BLACK] = pieces(BLACK, PAWN); + assert(dp.pc != NO_PIECE); assert(!(bool(captured) || m.type_of() == CASTLING) ^ (dp.remove_sq != SQ_NONE)); assert(dp.from != SQ_NONE); @@ -1238,32 +1247,14 @@ void Position::update_piece_threats(Piece pc, Bitboard threatened = attacks_bb(pc, s, occupied) & occupiedNoK; Bitboard incoming_threats = PseudoAttacks[KNIGHT][s] & knights; - // Compute both incoming and outgoing pawn threats. Incoming pawn pushers are only - // added if 'pc' is a pawn. - Bitboard pawnThreats = 0; - if (type_of(pc) == PAWN) - { - Bitboard whiteAttacks = PawnPushOrAttacks[WHITE][s]; - Bitboard blackAttacks = PawnPushOrAttacks[BLACK][s]; - - threatened |= (color_of(pc) == WHITE ? whiteAttacks : blackAttacks) & pieces(PAWN); - - pawnThreats = whiteAttacks & blackPawns; - pawnThreats |= blackAttacks & whitePawns; - } - else - { - pawnThreats = + if (type_of(pc) == KNIGHT || type_of(pc) == ROOK) + incoming_threats |= (attacks_bb(s, WHITE) & blackPawns) | (attacks_bb(s, BLACK) & whitePawns); - } - - if (type_of(pc) == PAWN || type_of(pc) == KNIGHT || type_of(pc) == ROOK) - incoming_threats |= pawnThreats; switch (type_of(pc)) { case PAWN : - threatened &= pieces(PAWN, KNIGHT, ROOK); + threatened &= pieces(KNIGHT, ROOK); break; case BISHOP : case ROOK : diff --git a/src/position.h b/src/position.h index f216be4fa..04336b370 100644 --- a/src/position.h +++ b/src/position.h @@ -147,8 +147,7 @@ class Position { void do_move(Move m, StateInfo& newSt, bool givesCheck, - DirtyPiece& dp, - DirtyThreats& dts, + Dirties& dirties, const TranspositionTable* tt, const SharedHistories* worker); void undo_move(Move m); @@ -221,16 +220,15 @@ class Position { std::array byTypeBB; std::array byColorBB; - int pieceCount[PIECE_NB]; - int castlingRightsMask[SQUARE_NB]; - Square castlingRookSquare[CASTLING_RIGHT_NB]; - Bitboard castlingPath[CASTLING_RIGHT_NB]; - StateInfo* st; - int gamePly; - Color sideToMove; - bool chess960; - DirtyPiece scratch_dp; - DirtyThreats scratch_dts; + int pieceCount[PIECE_NB]; + int castlingRightsMask[SQUARE_NB]; + Square castlingRookSquare[CASTLING_RIGHT_NB]; + Bitboard castlingPath[CASTLING_RIGHT_NB]; + StateInfo* st; + int gamePly; + Color sideToMove; + bool chess960; + Dirties scratchDirties; }; std::ostream& operator<<(std::ostream& os, const Position& pos); @@ -437,8 +435,9 @@ inline void Position::swap_piece(Square s, Piece pc, DirtyThreats* const dts) { } inline void Position::do_move(Move m, StateInfo& newSt, const TranspositionTable* tt = nullptr) { - new (&scratch_dts) DirtyThreats; - do_move(m, newSt, gives_check(m), scratch_dp, scratch_dts, tt, nullptr); + new (&scratchDirties.dirtyThreats) DirtyThreats; + new (&scratchDirties.dirtyPawnPairs) DirtyPawnPairs; + do_move(m, newSt, gives_check(m), scratchDirties, tt, nullptr); } inline StateInfo* Position::state() const { return st; } diff --git a/src/search.cpp b/src/search.cpp index 6f1fa20db..a3db28f27 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -644,12 +644,13 @@ void Search::Worker::do_move( bool capture = pos.capture_stage(move); ++nodes; - auto [dirtyPiece, dirtyThreats] = accumulatorStack.push(); - pos.do_move(move, st, givesCheck, dirtyPiece, dirtyThreats, &tt, &sharedHistory); + Dirties& dirties = accumulatorStack.push(); + pos.do_move(move, st, givesCheck, dirties, &tt, &sharedHistory); if (ss != nullptr) { - ss->currentMove = move; + auto& dirtyPiece = dirties.dirtyPiece; + ss->currentMove = move; ss->continuationHistory = &continuationHistory[ss->inCheck][capture][dirtyPiece.pc][move.to_sq()]; ss->continuationCorrectionHistory = diff --git a/src/types.h b/src/types.h index 6a2fcc694..f51bdb89f 100644 --- a/src/types.h +++ b/src/types.h @@ -344,6 +344,17 @@ struct DirtyThreats { DirtyThreatList list; }; +struct DirtyPawnPairs { + Bitboard before[COLOR_NB]; + Bitboard after[COLOR_NB]; +}; + +struct Dirties { + DirtyPiece dirtyPiece; + DirtyThreats dirtyThreats; + DirtyPawnPairs dirtyPawnPairs; +}; + #define ENABLE_INCR_OPERATORS_ON(T) \ constexpr T& operator++(T& d) { return d = T(int(d) + 1); } \ constexpr T& operator--(T& d) { return d = T(int(d) - 1); }