diff --git a/AUTHORS b/AUTHORS index ddc53ec0249..f6468c561ec 100644 --- a/AUTHORS +++ b/AUTHORS @@ -47,6 +47,7 @@ Bryan Cross (crossbr) candirufish Carlos Esparza Sánchez (ces42) Chess13234 +Chris Bao (sscg13) Chris Cain (ceebo) Ciekce clefrks diff --git a/scripts/.gitattributes b/scripts/.gitattributes new file mode 100644 index 00000000000..dfdb8b771ce --- /dev/null +++ b/scripts/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 9a523ae4475..4fce86e3a9b 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -78,7 +78,7 @@ Value Eval::evaluate(const Eval::NNUE::Networks& networks, optimism += optimism * nnueComplexity / 468; nnue -= nnue * nnueComplexity / (smallNet ? 20233 : 17879); - int material = (smallNet ? 553 : 532) * pos.count() + pos.non_pawn_material(); + int material = 535 * pos.count() + pos.non_pawn_material(); int v = (nnue * (77777 + material) + optimism * (7777 + material)) / 77777; // Damp down the evaluation linearly when shuffling diff --git a/src/history.h b/src/history.h index edf7fdc6716..15095cd0bbc 100644 --- a/src/history.h +++ b/src/history.h @@ -28,6 +28,7 @@ #include #include // IWYU pragma: keep +#include "misc.h" #include "position.h" namespace Stockfish { @@ -66,13 +67,17 @@ inline int non_pawn_index(const Position& pos) { return pos.non_pawn_key(c) & (CORRECTION_HISTORY_SIZE - 1); } -// StatsEntry stores the stat table value. It is usually a number but could -// be a move or even a nested history. We use a class instead of a naked value -// to directly call history update operator<<() on the entry so to use stats -// tables at caller sites as simple multi-dim arrays. +// StatsEntry is the container of various numerical statistics. We use a class +// instead of a naked value to directly call history update operator<<() on +// the entry. The first template parameter T is the base type of the array, +// and the second template parameter D limits the range of updates in [-D, D] +// when we update values with the << operator template class StatsEntry { + static_assert(std::is_arithmetic::value, "Not an arithmetic type"); + static_assert(D <= std::numeric_limits::max(), "D overflows T"); + T entry; public: @@ -80,13 +85,9 @@ class StatsEntry { entry = v; return *this; } - T* operator&() { return &entry; } - T* operator->() { return &entry; } operator const T&() const { return entry; } void operator<<(int bonus) { - static_assert(D <= std::numeric_limits::max(), "D overflows T"); - // Make sure that bonus is in range [-D, D] int clampedBonus = std::clamp(bonus, -D, D); entry += clampedBonus - entry * std::abs(clampedBonus) / D; @@ -95,87 +96,39 @@ class StatsEntry { } }; -template -struct StatsHelper; - -// Stats is a generic N-dimensional array used to store various statistics. -// The first template parameter T is the base type of the array, and the second -// template parameter D limits the range of updates in [-D, D] when we update -// values with the << operator, while the last parameters (Size and Sizes) -// encode the dimensions of the array. -template -class Stats { - using child_type = typename StatsHelper::child_type; - using array_type = std::array; - array_type data; - - public: - using size_type = typename array_type::size_type; - - auto& operator[](size_type index) { return data[index]; } - const auto& operator[](size_type index) const { return data[index]; } - - auto begin() { return data.begin(); } - auto end() { return data.end(); } - auto begin() const { return data.cbegin(); } - auto end() const { return data.cend(); } - auto cbegin() const { return data.cbegin(); } - auto cend() const { return data.cend(); } - - void fill(const T& v) { - for (auto& ele : data) - { - if constexpr (sizeof...(Sizes) == 0) - ele = v; - else - ele.fill(v); - } - } -}; - -template -struct StatsHelper { - using child_type = Stats; -}; - -template -struct StatsHelper { - using child_type = StatsEntry; -}; - -// In stats table, D=0 means that the template parameter is not used -enum StatsParams { - NOT_USED = 0 -}; enum StatsType { NoCaptures, Captures }; +template +using Stats = MultiArray, Sizes...>; + // ButterflyHistory records how often quiet moves have been successful or unsuccessful // during the current search, and is used for reduction and move ordering decisions. // It uses 2 tables (one for each color) indexed by the move's from and to squares, // see https://www.chessprogramming.org/Butterfly_Boards (~11 elo) -using ButterflyHistory = Stats; +using ButterflyHistory = Stats; // LowPlyHistory is adressed by play and move's from and to squares, used // to improve move ordering near the root -using LowPlyHistory = Stats; +using LowPlyHistory = + Stats; // CapturePieceToHistory is addressed by a move's [piece][to][captured piece type] -using CapturePieceToHistory = Stats; +using CapturePieceToHistory = Stats; // PieceToHistory is like ButterflyHistory but is addressed by a move's [piece][to] -using PieceToHistory = Stats; +using PieceToHistory = Stats; // ContinuationHistory is the combined history of a given pair of moves, usually // the current one given a previous one. The nested history table is based on // PieceToHistory instead of ButterflyBoards. // (~63 elo) -using ContinuationHistory = Stats; +using ContinuationHistory = MultiArray; // PawnHistory is addressed by the pawn structure and a move's [piece][to] -using PawnHistory = Stats; +using PawnHistory = Stats; // Correction histories record differences between the static evaluation of // positions and their search score. It is used to improve the static evaluation @@ -190,23 +143,27 @@ enum CorrHistType { Continuation, // Combined history of move pairs }; +namespace Detail { + template struct CorrHistTypedef { - using type = Stats; + using type = Stats; }; template<> struct CorrHistTypedef { - using type = Stats; + using type = Stats; }; template<> struct CorrHistTypedef { - using type = Stats::type, NOT_USED, PIECE_NB, SQUARE_NB>; + using type = MultiArray::type, PIECE_NB, SQUARE_NB>; }; +} + template -using CorrectionHistory = typename CorrHistTypedef::type; +using CorrectionHistory = typename Detail::CorrHistTypedef::type; } // namespace Stockfish diff --git a/src/misc.h b/src/misc.h index 48c49bafb34..81c7b17fe59 100644 --- a/src/misc.h +++ b/src/misc.h @@ -20,6 +20,7 @@ #define MISC_H_INCLUDED #include +#include #include #include #include @@ -142,6 +143,92 @@ class ValueList { }; +template +class MultiArray; + +namespace Detail { + +template +struct MultiArrayHelper { + using ChildType = MultiArray; +}; + +template +struct MultiArrayHelper { + using ChildType = T; +}; + +} + +// MultiArray is a generic N-dimensional array. +// The template parameters (Size and Sizes) encode the dimensions of the array. +template +class MultiArray { + using ChildType = typename Detail::MultiArrayHelper::ChildType; + using ArrayType = std::array; + ArrayType data_; + + public: + using value_type = typename ArrayType::value_type; + using size_type = typename ArrayType::size_type; + using difference_type = typename ArrayType::difference_type; + using reference = typename ArrayType::reference; + using const_reference = typename ArrayType::const_reference; + using pointer = typename ArrayType::pointer; + using const_pointer = typename ArrayType::const_pointer; + using iterator = typename ArrayType::iterator; + using const_iterator = typename ArrayType::const_iterator; + using reverse_iterator = typename ArrayType::reverse_iterator; + using const_reverse_iterator = typename ArrayType::const_reverse_iterator; + + constexpr auto& at(size_type index) noexcept { return data_.at(index); } + constexpr const auto& at(size_type index) const noexcept { return data_.at(index); } + + constexpr auto& operator[](size_type index) noexcept { return data_[index]; } + constexpr const auto& operator[](size_type index) const noexcept { return data_[index]; } + + constexpr auto& front() noexcept { return data_.front(); } + constexpr const auto& front() const noexcept { return data_.front(); } + constexpr auto& back() noexcept { return data_.back(); } + constexpr const auto& back() const noexcept { return data_.back(); } + + auto* data() { return data_.data(); } + const auto* data() const { return data_.data(); } + + constexpr auto begin() noexcept { return data_.begin(); } + constexpr auto end() noexcept { return data_.end(); } + constexpr auto begin() const noexcept { return data_.begin(); } + constexpr auto end() const noexcept { return data_.end(); } + constexpr auto cbegin() const noexcept { return data_.cbegin(); } + constexpr auto cend() const noexcept { return data_.cend(); } + + constexpr auto rbegin() noexcept { return data_.rbegin(); } + constexpr auto rend() noexcept { return data_.rend(); } + constexpr auto rbegin() const noexcept { return data_.rbegin(); } + constexpr auto rend() const noexcept { return data_.rend(); } + constexpr auto crbegin() const noexcept { return data_.crbegin(); } + constexpr auto crend() const noexcept { return data_.crend(); } + + constexpr bool empty() const noexcept { return data_.empty(); } + constexpr size_type size() const noexcept { return data_.size(); } + constexpr size_type max_size() const noexcept { return data_.max_size(); } + + template + void fill(const U& v) { + static_assert(std::is_assignable_v, "Cannot assign fill value to entry type"); + for (auto& ele : data_) + { + if constexpr (sizeof...(Sizes) == 0) + ele = v; + else + ele.fill(v); + } + } + + constexpr void swap(MultiArray& other) noexcept { data_.swap(other.data_); } +}; + + // xorshift64star Pseudo-Random Number Generator // This class is based on original code written and dedicated // to the public domain by Sebastiano Vigna (2014). diff --git a/src/movepick.cpp b/src/movepick.cpp index 4c5cc11dec1..8448616949d 100644 --- a/src/movepick.cpp +++ b/src/movepick.cpp @@ -22,6 +22,7 @@ #include #include "bitboard.h" +#include "misc.h" #include "position.h" namespace Stockfish { diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h index 6192cd51e32..8649d9521bb 100644 --- a/src/nnue/nnue_feature_transformer.h +++ b/src/nnue/nnue_feature_transformer.h @@ -146,10 +146,10 @@ using psqt_vec_t = int32x4_t; #endif +// Compute optimal SIMD register count for feature transformer accumulation. +template +class SIMDTiling { #ifdef VECTOR - - // Compute optimal SIMD register count for feature transformer accumulation. - // We use __m* types as template arguments, which causes GCC to emit warnings // about losing some attribute information. This is irrelevant to us as we // only take their size, so the following pragma are harmless. @@ -158,33 +158,47 @@ using psqt_vec_t = int32x4_t; #pragma GCC diagnostic ignored "-Wignored-attributes" #endif -template -static constexpr int BestRegisterCount() { - #define RegisterSize sizeof(SIMDRegisterType) - #define LaneSize sizeof(LaneType) - - static_assert(RegisterSize >= LaneSize); - static_assert(MaxRegisters <= NumRegistersSIMD); - static_assert(MaxRegisters > 0); - static_assert(NumRegistersSIMD > 0); - static_assert(RegisterSize % LaneSize == 0); - static_assert((NumLanes * LaneSize) % RegisterSize == 0); - - const int ideal = (NumLanes * LaneSize) / RegisterSize; - if (ideal <= MaxRegisters) - return ideal; - - // Look for the largest divisor of the ideal register count that is smaller than MaxRegisters - for (int divisor = MaxRegisters; divisor > 1; --divisor) - if (ideal % divisor == 0) - return divisor; - - return 1; -} + template + static constexpr int BestRegisterCount() { + constexpr std::size_t RegisterSize = sizeof(SIMDRegisterType); + constexpr std::size_t LaneSize = sizeof(LaneType); + + static_assert(RegisterSize >= LaneSize); + static_assert(MaxRegisters <= NumRegistersSIMD); + static_assert(MaxRegisters > 0); + static_assert(NumRegistersSIMD > 0); + static_assert(RegisterSize % LaneSize == 0); + static_assert((NumLanes * LaneSize) % RegisterSize == 0); + + const int ideal = (NumLanes * LaneSize) / RegisterSize; + if (ideal <= MaxRegisters) + return ideal; + + // Look for the largest divisor of the ideal register count that is smaller than MaxRegisters + for (int divisor = MaxRegisters; divisor > 1; --divisor) + if (ideal % divisor == 0) + return divisor; + + return 1; + } + #if defined(__GNUC__) #pragma GCC diagnostic pop #endif + + public: + static constexpr int NumRegs = + BestRegisterCount(); + static constexpr int NumPsqtRegs = + BestRegisterCount(); + + static constexpr IndexType TileHeight = NumRegs * sizeof(vec_t) / 2; + static constexpr IndexType PsqtTileHeight = NumPsqtRegs * sizeof(psqt_vec_t) / 4; + + static_assert(HalfDimensions % TileHeight == 0, "TileHeight must divide HalfDimensions"); + static_assert(PSQTBuckets % PsqtTileHeight == 0, "PsqtTileHeight must divide PSQTBuckets"); #endif +}; // Input feature converter @@ -196,17 +210,7 @@ class FeatureTransformer { static constexpr IndexType HalfDimensions = TransformedFeatureDimensions; private: -#ifdef VECTOR - static constexpr int NumRegs = - BestRegisterCount(); - static constexpr int NumPsqtRegs = - BestRegisterCount(); - - static constexpr IndexType TileHeight = NumRegs * sizeof(vec_t) / 2; - static constexpr IndexType PsqtTileHeight = NumPsqtRegs * sizeof(psqt_vec_t) / 4; - static_assert(HalfDimensions % TileHeight == 0, "TileHeight must divide HalfDimensions"); - static_assert(PSQTBuckets % PsqtTileHeight == 0, "PsqtTileHeight must divide PSQTBuckets"); -#endif + using Tiling = SIMDTiling; public: // Output type @@ -468,9 +472,8 @@ class FeatureTransformer { return st; } - // It computes the accumulator of the next position, or updates the - // current position's accumulator if CurrentOnly is true. - template + // Computes the accumulator of the next position. + template void update_accumulator_incremental(const Position& pos, StateInfo* computed) const { assert((computed->*accPtr).computed[Perspective]); assert(computed->next != nullptr); @@ -478,8 +481,8 @@ class FeatureTransformer { #ifdef VECTOR // Gcc-10.2 unnecessarily spills AVX2 registers if this array // is defined in the VECTOR code below, once in each branch. - vec_t acc[NumRegs]; - psqt_vec_t psqt[NumPsqtRegs]; + vec_t acc[Tiling::NumRegs]; + psqt_vec_t psqt[Tiling::NumPsqtRegs]; #endif const Square ksq = pos.square(Perspective); @@ -489,29 +492,23 @@ class FeatureTransformer { // feature set's update cost calculation to be correct and never allow // updates with more added/removed features than MaxActiveDimensions. FeatureSet::IndexList removed, added; + FeatureSet::append_changed_indices(ksq, computed->next->dirtyPiece, removed, + added); - if constexpr (CurrentOnly) - for (StateInfo* st = pos.state(); st != computed; st = st->previous) - FeatureSet::append_changed_indices(ksq, st->dirtyPiece, removed, - added); - else - FeatureSet::append_changed_indices(ksq, computed->next->dirtyPiece, - removed, added); - - StateInfo* next = CurrentOnly ? pos.state() : computed->next; + StateInfo* next = computed->next; assert(!(next->*accPtr).computed[Perspective]); #ifdef VECTOR if ((removed.size() == 1 || removed.size() == 2) && added.size() == 1) { - auto accIn = + auto* accIn = reinterpret_cast(&(computed->*accPtr).accumulation[Perspective][0]); - auto accOut = reinterpret_cast(&(next->*accPtr).accumulation[Perspective][0]); + auto* accOut = reinterpret_cast(&(next->*accPtr).accumulation[Perspective][0]); const IndexType offsetR0 = HalfDimensions * removed[0]; - auto columnR0 = reinterpret_cast(&weights[offsetR0]); + auto* columnR0 = reinterpret_cast(&weights[offsetR0]); const IndexType offsetA = HalfDimensions * added[0]; - auto columnA = reinterpret_cast(&weights[offsetA]); + auto* columnA = reinterpret_cast(&weights[offsetA]); if (removed.size() == 1) { @@ -521,22 +518,22 @@ class FeatureTransformer { else { const IndexType offsetR1 = HalfDimensions * removed[1]; - auto columnR1 = reinterpret_cast(&weights[offsetR1]); + auto* columnR1 = reinterpret_cast(&weights[offsetR1]); for (IndexType i = 0; i < HalfDimensions * sizeof(WeightType) / sizeof(vec_t); ++i) accOut[i] = vec_sub_16(vec_add_16(accIn[i], columnA[i]), vec_add_16(columnR0[i], columnR1[i])); } - auto accPsqtIn = reinterpret_cast( + auto* accPsqtIn = reinterpret_cast( &(computed->*accPtr).psqtAccumulation[Perspective][0]); - auto accPsqtOut = + auto* accPsqtOut = reinterpret_cast(&(next->*accPtr).psqtAccumulation[Perspective][0]); const IndexType offsetPsqtR0 = PSQTBuckets * removed[0]; - auto columnPsqtR0 = reinterpret_cast(&psqtWeights[offsetPsqtR0]); + auto* columnPsqtR0 = reinterpret_cast(&psqtWeights[offsetPsqtR0]); const IndexType offsetPsqtA = PSQTBuckets * added[0]; - auto columnPsqtA = reinterpret_cast(&psqtWeights[offsetPsqtA]); + auto* columnPsqtA = reinterpret_cast(&psqtWeights[offsetPsqtA]); if (removed.size() == 1) { @@ -548,7 +545,8 @@ class FeatureTransformer { else { const IndexType offsetPsqtR1 = PSQTBuckets * removed[1]; - auto columnPsqtR1 = reinterpret_cast(&psqtWeights[offsetPsqtR1]); + auto* columnPsqtR1 = + reinterpret_cast(&psqtWeights[offsetPsqtR1]); for (std::size_t i = 0; i < PSQTBuckets * sizeof(PSQTWeightType) / sizeof(psqt_vec_t); ++i) @@ -559,69 +557,69 @@ class FeatureTransformer { } else { - for (IndexType i = 0; i < HalfDimensions / TileHeight; ++i) + for (IndexType i = 0; i < HalfDimensions / Tiling::TileHeight; ++i) { // Load accumulator - auto accTileIn = reinterpret_cast( - &(computed->*accPtr).accumulation[Perspective][i * TileHeight]); - for (IndexType j = 0; j < NumRegs; ++j) + auto* accTileIn = reinterpret_cast( + &(computed->*accPtr).accumulation[Perspective][i * Tiling::TileHeight]); + for (IndexType j = 0; j < Tiling::NumRegs; ++j) acc[j] = vec_load(&accTileIn[j]); // Difference calculation for the deactivated features for (const auto index : removed) { - const IndexType offset = HalfDimensions * index + i * TileHeight; - auto column = reinterpret_cast(&weights[offset]); - for (IndexType j = 0; j < NumRegs; ++j) + const IndexType offset = HalfDimensions * index + i * Tiling::TileHeight; + auto* column = reinterpret_cast(&weights[offset]); + for (IndexType j = 0; j < Tiling::NumRegs; ++j) acc[j] = vec_sub_16(acc[j], column[j]); } // Difference calculation for the activated features for (const auto index : added) { - const IndexType offset = HalfDimensions * index + i * TileHeight; - auto column = reinterpret_cast(&weights[offset]); - for (IndexType j = 0; j < NumRegs; ++j) + const IndexType offset = HalfDimensions * index + i * Tiling::TileHeight; + auto* column = reinterpret_cast(&weights[offset]); + for (IndexType j = 0; j < Tiling::NumRegs; ++j) acc[j] = vec_add_16(acc[j], column[j]); } // Store accumulator - auto accTileOut = reinterpret_cast( - &(next->*accPtr).accumulation[Perspective][i * TileHeight]); - for (IndexType j = 0; j < NumRegs; ++j) + auto* accTileOut = reinterpret_cast( + &(next->*accPtr).accumulation[Perspective][i * Tiling::TileHeight]); + for (IndexType j = 0; j < Tiling::NumRegs; ++j) vec_store(&accTileOut[j], acc[j]); } - for (IndexType i = 0; i < PSQTBuckets / PsqtTileHeight; ++i) + for (IndexType i = 0; i < PSQTBuckets / Tiling::PsqtTileHeight; ++i) { // Load accumulator - auto accTilePsqtIn = reinterpret_cast( - &(computed->*accPtr).psqtAccumulation[Perspective][i * PsqtTileHeight]); - for (std::size_t j = 0; j < NumPsqtRegs; ++j) + auto* accTilePsqtIn = reinterpret_cast( + &(computed->*accPtr).psqtAccumulation[Perspective][i * Tiling::PsqtTileHeight]); + for (std::size_t j = 0; j < Tiling::NumPsqtRegs; ++j) psqt[j] = vec_load_psqt(&accTilePsqtIn[j]); // Difference calculation for the deactivated features for (const auto index : removed) { - const IndexType offset = PSQTBuckets * index + i * PsqtTileHeight; - auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); - for (std::size_t j = 0; j < NumPsqtRegs; ++j) + const IndexType offset = PSQTBuckets * index + i * Tiling::PsqtTileHeight; + auto* columnPsqt = reinterpret_cast(&psqtWeights[offset]); + for (std::size_t j = 0; j < Tiling::NumPsqtRegs; ++j) psqt[j] = vec_sub_psqt_32(psqt[j], columnPsqt[j]); } // Difference calculation for the activated features for (const auto index : added) { - const IndexType offset = PSQTBuckets * index + i * PsqtTileHeight; - auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); - for (std::size_t j = 0; j < NumPsqtRegs; ++j) + const IndexType offset = PSQTBuckets * index + i * Tiling::PsqtTileHeight; + auto* columnPsqt = reinterpret_cast(&psqtWeights[offset]); + for (std::size_t j = 0; j < Tiling::NumPsqtRegs; ++j) psqt[j] = vec_add_psqt_32(psqt[j], columnPsqt[j]); } // Store accumulator - auto accTilePsqtOut = reinterpret_cast( - &(next->*accPtr).psqtAccumulation[Perspective][i * PsqtTileHeight]); - for (std::size_t j = 0; j < NumPsqtRegs; ++j) + auto* accTilePsqtOut = reinterpret_cast( + &(next->*accPtr).psqtAccumulation[Perspective][i * Tiling::PsqtTileHeight]); + for (std::size_t j = 0; j < Tiling::NumPsqtRegs; ++j) vec_store_psqt(&accTilePsqtOut[j], psqt[j]); } } @@ -660,8 +658,8 @@ class FeatureTransformer { (next->*accPtr).computed[Perspective] = true; - if (!CurrentOnly && next != pos.state()) - update_accumulator_incremental(pos, next); + if (next != pos.state()) + update_accumulator_incremental(pos, next); } template @@ -700,88 +698,88 @@ class FeatureTransformer { accumulator.computed[Perspective] = true; #ifdef VECTOR - vec_t acc[NumRegs]; - psqt_vec_t psqt[NumPsqtRegs]; + vec_t acc[Tiling::NumRegs]; + psqt_vec_t psqt[Tiling::NumPsqtRegs]; - for (IndexType j = 0; j < HalfDimensions / TileHeight; ++j) + for (IndexType j = 0; j < HalfDimensions / Tiling::TileHeight; ++j) { - auto accTile = - reinterpret_cast(&accumulator.accumulation[Perspective][j * TileHeight]); - auto entryTile = reinterpret_cast(&entry.accumulation[j * TileHeight]); + auto* accTile = reinterpret_cast( + &accumulator.accumulation[Perspective][j * Tiling::TileHeight]); + auto* entryTile = reinterpret_cast(&entry.accumulation[j * Tiling::TileHeight]); - for (IndexType k = 0; k < NumRegs; ++k) + for (IndexType k = 0; k < Tiling::NumRegs; ++k) acc[k] = entryTile[k]; - int i = 0; - for (; i < int(std::min(removed.size(), added.size())); ++i) + std::size_t i = 0; + for (; i < std::min(removed.size(), added.size()); ++i) { IndexType indexR = removed[i]; - const IndexType offsetR = HalfDimensions * indexR + j * TileHeight; - auto columnR = reinterpret_cast(&weights[offsetR]); + const IndexType offsetR = HalfDimensions * indexR + j * Tiling::TileHeight; + auto* columnR = reinterpret_cast(&weights[offsetR]); IndexType indexA = added[i]; - const IndexType offsetA = HalfDimensions * indexA + j * TileHeight; - auto columnA = reinterpret_cast(&weights[offsetA]); + const IndexType offsetA = HalfDimensions * indexA + j * Tiling::TileHeight; + auto* columnA = reinterpret_cast(&weights[offsetA]); - for (unsigned k = 0; k < NumRegs; ++k) + for (IndexType k = 0; k < Tiling::NumRegs; ++k) acc[k] = vec_add_16(acc[k], vec_sub_16(columnA[k], columnR[k])); } - for (; i < int(removed.size()); ++i) + for (; i < removed.size(); ++i) { IndexType index = removed[i]; - const IndexType offset = HalfDimensions * index + j * TileHeight; - auto column = reinterpret_cast(&weights[offset]); + const IndexType offset = HalfDimensions * index + j * Tiling::TileHeight; + auto* column = reinterpret_cast(&weights[offset]); - for (unsigned k = 0; k < NumRegs; ++k) + for (IndexType k = 0; k < Tiling::NumRegs; ++k) acc[k] = vec_sub_16(acc[k], column[k]); } - for (; i < int(added.size()); ++i) + for (; i < added.size(); ++i) { IndexType index = added[i]; - const IndexType offset = HalfDimensions * index + j * TileHeight; - auto column = reinterpret_cast(&weights[offset]); + const IndexType offset = HalfDimensions * index + j * Tiling::TileHeight; + auto* column = reinterpret_cast(&weights[offset]); - for (unsigned k = 0; k < NumRegs; ++k) + for (IndexType k = 0; k < Tiling::NumRegs; ++k) acc[k] = vec_add_16(acc[k], column[k]); } - for (IndexType k = 0; k < NumRegs; k++) + for (IndexType k = 0; k < Tiling::NumRegs; k++) vec_store(&entryTile[k], acc[k]); - for (IndexType k = 0; k < NumRegs; k++) + for (IndexType k = 0; k < Tiling::NumRegs; k++) vec_store(&accTile[k], acc[k]); } - for (IndexType j = 0; j < PSQTBuckets / PsqtTileHeight; ++j) + for (IndexType j = 0; j < PSQTBuckets / Tiling::PsqtTileHeight; ++j) { - auto accTilePsqt = reinterpret_cast( - &accumulator.psqtAccumulation[Perspective][j * PsqtTileHeight]); - auto entryTilePsqt = - reinterpret_cast(&entry.psqtAccumulation[j * PsqtTileHeight]); + auto* accTilePsqt = reinterpret_cast( + &accumulator.psqtAccumulation[Perspective][j * Tiling::PsqtTileHeight]); + auto* entryTilePsqt = + reinterpret_cast(&entry.psqtAccumulation[j * Tiling::PsqtTileHeight]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = entryTilePsqt[k]; - for (int i = 0; i < int(removed.size()); ++i) + for (std::size_t i = 0; i < removed.size(); ++i) { IndexType index = removed[i]; - const IndexType offset = PSQTBuckets * index + j * PsqtTileHeight; - auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); + const IndexType offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = reinterpret_cast(&psqtWeights[offset]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = vec_sub_psqt_32(psqt[k], columnPsqt[k]); } - for (int i = 0; i < int(added.size()); ++i) + for (std::size_t i = 0; i < added.size(); ++i) { IndexType index = added[i]; - const IndexType offset = PSQTBuckets * index + j * PsqtTileHeight; - auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); + const IndexType offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = reinterpret_cast(&psqtWeights[offset]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); } - for (std::size_t k = 0; k < NumPsqtRegs; ++k) + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) vec_store_psqt(&entryTilePsqt[k], psqt[k]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) vec_store_psqt(&accTilePsqt[k], psqt[k]); } @@ -839,7 +837,7 @@ class FeatureTransformer { StateInfo* oldest = try_find_computed_accumulator(pos); if ((oldest->*accPtr).computed[Perspective] && oldest != pos.state()) - update_accumulator_incremental(pos, oldest); + update_accumulator_incremental(pos, oldest); else update_accumulator_refresh_cache(pos, cache); } @@ -853,7 +851,7 @@ class FeatureTransformer { 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(pos, oldest); + update_accumulator_incremental(pos, oldest); else update_accumulator_refresh_cache(pos, cache); } diff --git a/src/position.cpp b/src/position.cpp index 60b7d7d3f20..9d883c92b47 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -493,14 +493,23 @@ void Position::update_slider_blockers(Color c) const { // Slider attacks use the occupied bitboard to indicate occupancy. Bitboard Position::attackers_to(Square s, Bitboard occupied) const { - return (pawn_attacks_bb(BLACK, s) & pieces(WHITE, PAWN)) - | (pawn_attacks_bb(WHITE, s) & pieces(BLACK, PAWN)) - | (attacks_bb(s) & pieces(KNIGHT)) - | (attacks_bb(s, occupied) & pieces(ROOK, QUEEN)) + return (attacks_bb(s, occupied) & pieces(ROOK, QUEEN)) | (attacks_bb(s, occupied) & pieces(BISHOP, QUEEN)) - | (attacks_bb(s) & pieces(KING)); + | (pawn_attacks_bb(BLACK, s) & pieces(WHITE, PAWN)) + | (pawn_attacks_bb(WHITE, s) & pieces(BLACK, PAWN)) + | (attacks_bb(s) & pieces(KNIGHT)) | (attacks_bb(s) & pieces(KING)); } +bool Position::attackers_to_exist(Square s, Bitboard occupied, Color c) const { + + return ((attacks_bb(s) & pieces(c, ROOK, QUEEN)) + && (attacks_bb(s, occupied) & pieces(c, ROOK, QUEEN))) + || ((attacks_bb(s) & pieces(c, BISHOP, QUEEN)) + && (attacks_bb(s, occupied) & pieces(c, BISHOP, QUEEN))) + || (((pawn_attacks_bb(~c, s) & pieces(PAWN)) | (attacks_bb(s) & pieces(KNIGHT)) + | (attacks_bb(s) & pieces(KING))) + & pieces(c)); +} // Tests whether a pseudo-legal move is legal bool Position::legal(Move m) const { @@ -542,7 +551,7 @@ bool Position::legal(Move m) const { Direction step = to > from ? WEST : EAST; for (Square s = to; s != from; s += step) - if (attackers_to(s) & pieces(~us)) + if (attackers_to_exist(s, pieces(), ~us)) return false; // In case of Chess960, verify if the Rook blocks some checks. @@ -553,7 +562,7 @@ bool Position::legal(Move m) const { // If the moving piece is a king, check whether the destination square is // attacked by the opponent. if (type_of(piece_on(from)) == KING) - return !(attackers_to(to, pieces() ^ from) & pieces(~us)); + return !(attackers_to_exist(to, pieces() ^ from, ~us)); // A non-king move is legal if and only if it is not pinned or it // is moving along the ray towards or away from the king. @@ -622,7 +631,7 @@ bool Position::pseudo_legal(const Move m) const { } // In case of king moves under check we have to remove the king so as to catch // invalid moves like b1a1 when opposite queen is on c1. - else if (attackers_to(to, pieces() ^ from) & pieces(~us)) + else if (attackers_to_exist(to, pieces() ^ from, ~us)) return false; } @@ -1308,7 +1317,7 @@ bool Position::pos_is_ok() const { return true; if (pieceCount[W_KING] != 1 || pieceCount[B_KING] != 1 - || attackers_to(square(~sideToMove)) & pieces(sideToMove)) + || attackers_to_exist(square(~sideToMove), pieces(), sideToMove)) assert(0 && "pos_is_ok: Kings"); if ((pieces(PAWN) & (Rank1BB | Rank8BB)) || pieceCount[W_PAWN] > 8 || pieceCount[B_PAWN] > 8) diff --git a/src/position.h b/src/position.h index 7dc83f251be..7fdaf84d5fe 100644 --- a/src/position.h +++ b/src/position.h @@ -126,6 +126,7 @@ class Position { // Attacks to/from a given square Bitboard attackers_to(Square s) const; Bitboard attackers_to(Square s, Bitboard occupied) const; + bool attackers_to_exist(Square s, Bitboard occupied, Color c) const; void update_slider_blockers(Color c) const; template Bitboard attacks_by(Color c) const; diff --git a/src/search.cpp b/src/search.cpp index d6748c7696e..c27d93d2565 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -66,7 +66,7 @@ namespace { // Futility margin Value futility_margin(Depth d, bool noTtCutNode, bool improving, bool oppWorsening) { - Value futilityMult = 109 - 27 * noTtCutNode; + Value futilityMult = 112 - 26 * noTtCutNode; Value improvingDeduction = improving * futilityMult * 2; Value worseningDeduction = oppWorsening * futilityMult / 3; @@ -77,7 +77,7 @@ constexpr int futility_move_count(bool improving, Depth depth) { return (3 + depth * depth) / (2 - improving); } -int correction_value(const Worker& w, const Position& pos, Stack* ss) { +int correction_value(const Worker& w, const Position& pos, const Stack* ss) { const Color us = pos.side_to_move(); const auto m = (ss - 1)->currentMove; const auto pcv = w.pawnCorrectionHistory[us][pawn_structure_index(pos)]; @@ -89,7 +89,7 @@ int correction_value(const Worker& w, const Position& pos, Stack* ss) { m.is_ok() ? (*(ss - 2)->continuationCorrectionHistory)[pos.piece_on(m.to_sq())][m.to_sq()] : 0; - return (6384 * pcv + 3583 * macv + 6492 * micv + 6725 * (wnpcv + bnpcv) + 5880 * cntcv); + return (6922 * pcv + 3837 * macv + 6238 * micv + 7490 * (wnpcv + bnpcv) + 6270 * cntcv); } // Add correctionHistory value to raw staticEval and guarantee evaluation @@ -99,10 +99,10 @@ Value to_corrected_static_eval(Value v, const int cv) { } // History and stats update bonus, based on depth -int stat_bonus(Depth d) { return std::min(168 * d - 100, 1718); } +int stat_bonus(Depth d) { return std::min(154 * d - 102, 1661); } // History and stats update malus, based on depth -int stat_malus(Depth d) { return std::min(768 * d - 257, 2351); } +int stat_malus(Depth d) { return std::min(831 * d - 269, 2666); } // Add a small random component to draw evaluations to avoid 3-fold blindness Value value_draw(size_t nodes) { return VALUE_DRAW - 1 + Value(nodes & 0x2); } @@ -274,7 +274,7 @@ void Search::Worker::iterative_deepening() { int searchAgainCounter = 0; - lowPlyHistory.fill(106); + lowPlyHistory.fill(97); // Iterative deepening loop until requested to stop or the target depth is reached while (++rootDepth < MAX_PLY && !threads.stop @@ -310,13 +310,13 @@ void Search::Worker::iterative_deepening() { selDepth = 0; // Reset aspiration window starting size - delta = 5 + std::abs(rootMoves[pvIdx].meanSquaredScore) / 13461; + delta = 5 + std::abs(rootMoves[pvIdx].meanSquaredScore) / 12991; Value avg = rootMoves[pvIdx].averageScore; alpha = std::max(avg - delta, -VALUE_INFINITE); beta = std::min(avg + delta, VALUE_INFINITE); // Adjust optimism based on root move's averageScore (~4 Elo) - optimism[us] = 150 * avg / (std::abs(avg) + 85); + optimism[us] = 141 * avg / (std::abs(avg) + 83); optimism[~us] = -optimism[us]; // Start with a small aspiration window and, in the case of a fail @@ -498,10 +498,10 @@ void Search::Worker::iterative_deepening() { // Reset histories, usually before a new game void Search::Worker::clear() { - mainHistory.fill(61); - lowPlyHistory.fill(106); - captureHistory.fill(-598); - pawnHistory.fill(-1181); + mainHistory.fill(63); + lowPlyHistory.fill(108); + captureHistory.fill(-631); + pawnHistory.fill(-1210); pawnCorrectionHistory.fill(0); majorPieceCorrectionHistory.fill(0); minorPieceCorrectionHistory.fill(0); @@ -510,16 +510,16 @@ void Search::Worker::clear() { for (auto& to : continuationCorrectionHistory) for (auto& h : to) - h->fill(0); + h.fill(0); for (bool inCheck : {false, true}) for (StatsType c : {NoCaptures, Captures}) for (auto& to : continuationHistory[inCheck][c]) for (auto& h : to) - h->fill(-427); + h.fill(-479); for (size_t i = 1; i < reductions.size(); ++i) - reductions[i] = int(19.43 * std::log(i)); + reductions[i] = int(2143 / 100.0 * std::log(i)); refreshTable.clear(networks[numaAccessToken]); } @@ -636,20 +636,20 @@ Value Search::Worker::search( if (!PvNode && !excludedMove && ttData.depth > depth - (ttData.value <= beta) && is_valid(ttData.value) // Can happen when !ttHit or when access race in probe() && (ttData.bound & (ttData.value >= beta ? BOUND_LOWER : BOUND_UPPER)) - && (cutNode == (ttData.value >= beta) || depth > 8)) + && (cutNode == (ttData.value >= beta) || depth > 9)) { // If ttMove is quiet, update move sorting heuristics on TT hit (~2 Elo) if (ttData.move && ttData.value >= beta) { // Bonus for a quiet ttMove that fails high (~2 Elo) if (!ttCapture) - update_quiet_histories(pos, ss, *this, ttData.move, stat_bonus(depth) * 747 / 1024); + update_quiet_histories(pos, ss, *this, ttData.move, stat_bonus(depth) * 746 / 1024); // Extra penalty for early quiet moves of // the previous ply (~1 Elo on STC, ~2 Elo on LTC) if (prevSq != SQ_NONE && (ss - 1)->moveCount <= 2 && !priorCapture) update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, - -stat_malus(depth + 1) * 1091 / 1024); + -stat_malus(depth + 1) * 1042 / 1024); } // Partial workaround for the graph history interaction problem @@ -757,11 +757,11 @@ Value Search::Worker::search( // Use static evaluation difference to improve quiet move ordering (~9 Elo) if (((ss - 1)->currentMove).is_ok() && !(ss - 1)->inCheck && !priorCapture) { - int bonus = std::clamp(-10 * int((ss - 1)->staticEval + ss->staticEval), -1831, 1428) + 623; - thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << bonus * 1340 / 1024; + int bonus = std::clamp(-10 * int((ss - 1)->staticEval + ss->staticEval), -1881, 1413) + 616; + thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << bonus * 1151 / 1024; if (type_of(pos.piece_on(prevSq)) != PAWN && ((ss - 1)->currentMove).type_of() != PROMOTION) thisThread->pawnHistory[pawn_structure_index(pos)][pos.piece_on(prevSq)][prevSq] - << bonus * 1159 / 1024; + << bonus * 1107 / 1024; } // Set up the improving flag, which is true if current static evaluation is @@ -775,34 +775,31 @@ Value Search::Worker::search( // Step 7. Razoring (~1 Elo) // If eval is really low, check with qsearch if we can exceed alpha. If the // search suggests we cannot exceed alpha, return a speculative fail low. - if (eval < alpha - 469 - 307 * depth * depth) - { - value = qsearch(pos, ss, alpha - 1, alpha); - if (value < alpha && !is_decisive(value)) - return value; - } + // For PvNodes, we must have a guard against mates being returned. + if (!PvNode && eval < alpha - 462 - 297 * depth * depth) + return qsearch(pos, ss, alpha - 1, alpha); // Step 8. Futility pruning: child node (~40 Elo) // The depth condition is important for mate finding. if (!ss->ttPv && depth < 14 && eval - futility_margin(depth, cutNode && !ss->ttHit, improving, opponentWorsening) - - (ss - 1)->statScore / 290 + - (ss - 1)->statScore / 310 + (ss->staticEval == eval) * (40 - std::abs(correctionValue) / 131072) >= beta && eval >= beta && (!ttData.move || ttCapture) && !is_loss(beta) && !is_win(eval)) return beta + (eval - beta) / 3; - improving |= ss->staticEval >= beta + 100; + improving |= ss->staticEval >= beta + 97; // Step 9. Null move search with verification search (~35 Elo) if (cutNode && (ss - 1)->currentMove != Move::null() && eval >= beta - && ss->staticEval >= beta - 21 * depth + 421 && !excludedMove && pos.non_pawn_material(us) + && ss->staticEval >= beta - 20 * depth + 440 && !excludedMove && pos.non_pawn_material(us) && ss->ply >= thisThread->nmpMinPly && !is_loss(beta)) { assert(eval - beta >= 0); // Null move dynamic reduction based on depth and eval - Depth R = std::min(int(eval - beta) / 235, 7) + depth / 3 + 5; + Depth R = std::min(int(eval - beta) / 215, 7) + depth / 3 + 5; ss->currentMove = Move::null(); ss->continuationHistory = &thisThread->continuationHistory[0][0][NO_PIECE][0]; @@ -836,24 +833,22 @@ Value Search::Worker::search( } // Step 10. Internal iterative reductions (~9 Elo) - // For PV nodes without a ttMove, we decrease depth. - if (PvNode && !ttData.move) - depth -= 3; + // For PV nodes without a ttMove as well as for deep enough cutNodes, we decrease depth. + // This heuristic is known to scale non-linearly, current version was tested at VVLTC. + // Further improvements need to be tested at similar time control if they make IIR + // more aggressive. + if ((PvNode || (cutNode && depth >= 7)) && !ttData.move) + depth -= 2; // Use qsearch if depth <= 0 if (depth <= 0) return qsearch(pos, ss, alpha, beta); - // For cutNodes, if depth is high enough, decrease depth by 2 if there is no ttMove, - // or by 1 if there is a ttMove with an upper bound. - if (cutNode && depth >= 7 && (!ttData.move || ttData.bound == BOUND_UPPER)) - depth -= 1 + !ttData.move; - // Step 11. ProbCut (~10 Elo) // If we have a good enough capture (or queen promotion) and a reduced search // returns a value much above beta, we can (almost) safely prune the previous move. - probCutBeta = beta + 187 - 56 * improving; - if (!PvNode && depth > 3 + probCutBeta = beta + 174 - 56 * improving; + if (depth > 3 && !is_decisive(beta) // If value from transposition table is lower than probCutBeta, don't attempt // probCut there and in further interactions with transposition table cutoff @@ -864,7 +859,6 @@ Value Search::Worker::search( assert(probCutBeta < VALUE_INFINITE && probCutBeta > beta); MovePicker mp(pos, ttData.move, probCutBeta - ss->staticEval, &thisThread->captureHistory); - Piece captured; while ((move = mp.next_move()) != Move::none()) { @@ -878,10 +872,6 @@ Value Search::Worker::search( assert(pos.capture_stage(move)); - movedPiece = pos.moved_piece(move); - captured = pos.piece_on(move.to_sq()); - - // Prefetch the TT entry for the resulting position prefetch(tt.first_entry(pos.key_after(move))); @@ -906,22 +896,20 @@ Value Search::Worker::search( if (value >= probCutBeta) { - thisThread->captureHistory[movedPiece][move.to_sq()][type_of(captured)] << 1226; - // Save ProbCut data into transposition table ttWriter.write(posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER, depth - 3, move, unadjustedStaticEval, tt.generation()); - return is_decisive(value) ? value : value - (probCutBeta - beta); + + if (!is_decisive(value)) + return value - (probCutBeta - beta); } } - - Eval::NNUE::hint_common_parent_position(pos, networks[numaAccessToken], refreshTable); } moves_loop: // When in check, search starts here // Step 12. A small Probcut idea (~4 Elo) - probCutBeta = beta + 417; + probCutBeta = beta + 412; if ((ttData.bound & BOUND_LOWER) && ttData.depth >= depth - 4 && ttData.value >= probCutBeta && !is_decisive(beta) && is_valid(ttData.value) && !is_decisive(ttData.value)) return probCutBeta; @@ -1004,15 +992,15 @@ Value Search::Worker::search( // Futility pruning for captures (~2 Elo) if (!givesCheck && lmrDepth < 7 && !ss->inCheck) { - Value futilityValue = ss->staticEval + 287 + 253 * lmrDepth + Value futilityValue = ss->staticEval + 271 + 243 * lmrDepth + PieceValue[capturedPiece] + captHist / 7; if (futilityValue <= alpha) continue; } // SEE based pruning for captures and checks (~11 Elo) - int seeHist = std::clamp(captHist / 33, -161 * depth, 156 * depth); - if (!pos.see_ge(move, -162 * depth - seeHist)) + int seeHist = std::clamp(captHist / 37, -152 * depth, 141 * depth); + if (!pos.see_ge(move, -156 * depth - seeHist)) continue; } else @@ -1023,15 +1011,15 @@ Value Search::Worker::search( + thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][move.to_sq()]; // Continuation history based pruning (~2 Elo) - if (history < -3884 * depth) + if (history < -3901 * depth) continue; history += 2 * thisThread->mainHistory[us][move.from_to()]; - lmrDepth += history / 3609; + lmrDepth += history / 3459; Value futilityValue = - ss->staticEval + (bestValue < ss->staticEval - 45 ? 140 : 43) + 141 * lmrDepth; + ss->staticEval + (bestValue < ss->staticEval - 47 ? 137 : 47) + 142 * lmrDepth; // Futility pruning: parent node (~13 Elo) if (!ss->inCheck && lmrDepth < 12 && futilityValue <= alpha) @@ -1068,11 +1056,11 @@ Value Search::Worker::search( // and lower extension margins scale well. if (!rootNode && move == ttData.move && !excludedMove - && depth >= 4 - (thisThread->completedDepth > 33) + ss->ttPv + && depth >= 5 - (thisThread->completedDepth > 33) + ss->ttPv && is_valid(ttData.value) && !is_decisive(ttData.value) && (ttData.bound & BOUND_LOWER) && ttData.depth >= depth - 3) { - Value singularBeta = ttData.value - (56 + 79 * (ss->ttPv && !PvNode)) * depth / 64; + Value singularBeta = ttData.value - (52 + 74 * (ss->ttPv && !PvNode)) * depth / 64; Depth singularDepth = newDepth / 2; ss->excludedMove = move; @@ -1082,13 +1070,18 @@ Value Search::Worker::search( if (value < singularBeta) { - int doubleMargin = 249 * PvNode - 194 * !ttCapture; - int tripleMargin = 94 + 287 * PvNode - 249 * !ttCapture + 99 * ss->ttPv; + int corrValAdj = std::abs(correctionValue) / 262144; + int doubleMargin = 249 * PvNode - 194 * !ttCapture - corrValAdj; + int tripleMargin = + 94 + 287 * PvNode - 249 * !ttCapture + 99 * ss->ttPv - corrValAdj; + int quadMargin = + 394 + 287 * PvNode - 249 * !ttCapture + 99 * ss->ttPv - corrValAdj; extension = 1 + (value < singularBeta - doubleMargin) - + (value < singularBeta - tripleMargin); + + (value < singularBeta - tripleMargin) + + (value < singularBeta - quadMargin); - depth += ((!PvNode) && (depth < 14)); + depth += ((!PvNode) && (depth < 15)); } // Multi-cut pruning @@ -1121,7 +1114,7 @@ Value Search::Worker::search( else if (PvNode && move.to_sq() == prevSq && thisThread->captureHistory[movedPiece][move.to_sq()] [type_of(pos.piece_on(move.to_sq()))] - > 4321) + > 4126) extension = 1; } @@ -1150,46 +1143,46 @@ Value Search::Worker::search( // Decrease reduction if position is or has been on the PV (~7 Elo) if (ss->ttPv) - r -= 1024 + (ttData.value > alpha) * 1024 + (ttData.depth >= depth) * 1024; + r -= 1037 + (ttData.value > alpha) * 965 + (ttData.depth >= depth) * 960; // Decrease reduction for PvNodes (~0 Elo on STC, ~2 Elo on LTC) if (PvNode) - r -= 1024; + r -= 1018; // These reduction adjustments have no proven non-linear scaling - r += 330; + r += 307; - r -= std::abs(correctionValue) / 32768; + r -= std::abs(correctionValue) / 34112; // Increase reduction for cut nodes (~4 Elo) if (cutNode) - r += 2518 - (ttData.depth >= depth && ss->ttPv) * 991; + r += 2355 - (ttData.depth >= depth && ss->ttPv) * 1141; // Increase reduction if ttMove is a capture but the current move is not a capture (~3 Elo) if (ttCapture && !capture) - r += 1043 + (depth < 8) * 999; + r += 1087 + (depth < 8) * 990; // Increase reduction if next ply has a lot of fail high (~5 Elo) if ((ss + 1)->cutoffCnt > 3) - r += 938 + allNode * 960; + r += 940 + allNode * 887; // For first picked move (ttMove) reduce reduction (~3 Elo) else if (move == ttData.move) - r -= 1879; + r -= 1960; if (capture) ss->statScore = 7 * int(PieceValue[pos.captured_piece()]) + thisThread->captureHistory[movedPiece][move.to_sq()][type_of(pos.captured_piece())] - - 5000; + - 4666; else ss->statScore = 2 * thisThread->mainHistory[us][move.from_to()] + (*contHist[0])[movedPiece][move.to_sq()] - + (*contHist[1])[movedPiece][move.to_sq()] - 3996; + + (*contHist[1])[movedPiece][move.to_sq()] - 3874; // Decrease/increase reduction for moves with a good/bad history (~8 Elo) - r -= ss->statScore * 1287 / 16384; + r -= ss->statScore * 1451 / 16384; // Step 17. Late moves reduction / extension (LMR, ~117 Elo) if (depth >= 2 && moveCount > 1) @@ -1209,7 +1202,7 @@ Value Search::Worker::search( { // Adjust full-depth search based on LMR results - if the result was // good enough search deeper, if it was bad enough search shallower. - const bool doDeeperSearch = value > (bestValue + 42 + 2 * newDepth); // (~1 Elo) + const bool doDeeperSearch = value > (bestValue + 40 + 2 * newDepth); // (~1 Elo) const bool doShallowerSearch = value < bestValue + 10; // (~2 Elo) newDepth += doDeeperSearch - doShallowerSearch; @@ -1228,11 +1221,11 @@ Value Search::Worker::search( { // Increase reduction if ttMove is not present (~6 Elo) if (!ttData.move) - r += 2037; + r += 2111; // Note that if expected reduction is high, we reduce search depth by 1 here (~9 Elo) value = - -search(pos, ss + 1, -(alpha + 1), -alpha, newDepth - (r > 2983), !cutNode); + -search(pos, ss + 1, -(alpha + 1), -alpha, newDepth - (r > 3444), !cutNode); } // For PV nodes only, do a full PV search on the first move or after a fail high, @@ -1381,25 +1374,25 @@ Value Search::Worker::search( // Bonus for prior countermove that caused the fail low else if (!priorCapture && prevSq != SQ_NONE) { - int bonusScale = (117 * (depth > 5) + 39 * !allNode + 168 * ((ss - 1)->moveCount > 8) - + 115 * (!ss->inCheck && bestValue <= ss->staticEval - 108) - + 119 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 83)); + int bonusScale = (118 * (depth > 5) + 37 * !allNode + 169 * ((ss - 1)->moveCount > 8) + + 128 * (!ss->inCheck && bestValue <= ss->staticEval - 102) + + 115 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 82)); // Proportional to "how much damage we have to undo" - bonusScale += std::min(-(ss - 1)->statScore / 113, 300); + bonusScale += std::min(-(ss - 1)->statScore / 106, 318); bonusScale = std::max(bonusScale, 0); const int scaledBonus = stat_bonus(depth) * bonusScale / 32; update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, - scaledBonus * 416 / 1024); + scaledBonus * 436 / 1024); - thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << scaledBonus * 212 / 1024; + thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << scaledBonus * 207 / 1024; if (type_of(pos.piece_on(prevSq)) != PAWN && ((ss - 1)->currentMove).type_of() != PROMOTION) thisThread->pawnHistory[pawn_structure_index(pos)][pos.piece_on(prevSq)][prevSq] - << scaledBonus * 1073 / 1024; + << scaledBonus * 1195 / 1024; } else if (priorCapture && prevSq != SQ_NONE) @@ -1411,10 +1404,6 @@ Value Search::Worker::search( << stat_bonus(depth) * 2; } - // Bonus when search fails low and there is a TT move - else if (ttData.move && !allNode) - thisThread->mainHistory[us][ttData.move.from_to()] << stat_bonus(depth) * 287 / 1024; - if (PvNode) bestValue = std::min(bestValue, maxValue); @@ -1437,15 +1426,15 @@ Value Search::Worker::search( && ((bestValue < ss->staticEval && bestValue < beta) // negative correction & no fail high || (bestValue > ss->staticEval && bestMove))) // positive correction & no fail low { - const auto m = (ss - 1)->currentMove; - static const int nonPawnWeight = 154; + const auto m = (ss - 1)->currentMove; + constexpr int nonPawnWeight = 165; auto bonus = std::clamp(int(bestValue - ss->staticEval) * depth / 8, -CORRECTION_HISTORY_LIMIT / 4, CORRECTION_HISTORY_LIMIT / 4); thisThread->pawnCorrectionHistory[us][pawn_structure_index(pos)] - << bonus * 107 / 128; - thisThread->majorPieceCorrectionHistory[us][major_piece_index(pos)] << bonus * 162 / 128; - thisThread->minorPieceCorrectionHistory[us][minor_piece_index(pos)] << bonus * 148 / 128; + << bonus * 114 / 128; + thisThread->majorPieceCorrectionHistory[us][major_piece_index(pos)] << bonus * 163 / 128; + thisThread->minorPieceCorrectionHistory[us][minor_piece_index(pos)] << bonus * 146 / 128; thisThread->nonPawnCorrectionHistory[WHITE][us][non_pawn_index(pos)] << bonus * nonPawnWeight / 128; thisThread->nonPawnCorrectionHistory[BLACK][us][non_pawn_index(pos)] @@ -1577,7 +1566,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) if (bestValue > alpha) alpha = bestValue; - futilityBase = ss->staticEval + 306; + futilityBase = ss->staticEval + 301; } const PieceToHistory* contHist[] = {(ss - 1)->continuationHistory, @@ -1640,11 +1629,11 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) + (*contHist[1])[pos.moved_piece(move)][move.to_sq()] + thisThread->pawnHistory[pawn_structure_index(pos)][pos.moved_piece(move)] [move.to_sq()] - <= 5095) + <= 5228) continue; // Do not search moves with bad enough SEE values (~5 Elo) - if (!pos.see_ge(move, -83)) + if (!pos.see_ge(move, -80)) continue; } @@ -1712,7 +1701,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) Depth Search::Worker::reduction(bool i, Depth d, int mn, int delta) const { int reductionScale = reductions[d] * reductions[mn]; - return reductionScale - delta * 814 / rootDelta + !i * reductionScale / 3 + 1304; + return reductionScale - delta * 768 / rootDelta + !i * reductionScale * 108 / 300 + 1168; } // elapsed() returns the time elapsed since the search started. If the @@ -1811,30 +1800,30 @@ void update_all_stats(const Position& pos, if (!pos.capture_stage(bestMove)) { - update_quiet_histories(pos, ss, workerThread, bestMove, bonus * 1131 / 1024); + update_quiet_histories(pos, ss, workerThread, bestMove, bonus * 1216 / 1024); // Decrease stats for all non-best quiet moves for (Move move : quietsSearched) - update_quiet_histories(pos, ss, workerThread, move, -malus * 1028 / 1024); + update_quiet_histories(pos, ss, workerThread, move, -malus * 1062 / 1024); } else { // Increase stats for the best move in case it was a capture move captured = type_of(pos.piece_on(bestMove.to_sq())); - captureHistory[moved_piece][bestMove.to_sq()][captured] << bonus * 1291 / 1024; + captureHistory[moved_piece][bestMove.to_sq()][captured] << bonus * 1272 / 1024; } // Extra penalty for a quiet early move that was not a TT move in // previous ply when it gets refuted. if (prevSq != SQ_NONE && ((ss - 1)->moveCount == 1 + (ss - 1)->ttHit) && !pos.captured_piece()) - update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -malus * 919 / 1024); + update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -malus * 966 / 1024); // Decrease stats for all non-best capture moves for (Move move : capturesSearched) { moved_piece = pos.moved_piece(move); captured = type_of(pos.piece_on(move.to_sq())); - captureHistory[moved_piece][move.to_sq()][captured] << -malus * 1090 / 1024; + captureHistory[moved_piece][move.to_sq()][captured] << -malus * 1205 / 1024; } } @@ -1843,7 +1832,7 @@ void update_all_stats(const Position& pos, // at ply -1, -2, -3, -4, and -6 with current move. void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus) { static constexpr std::array conthist_bonuses = { - {{1, 1024}, {2, 571}, {3, 339}, {4, 500}, {6, 592}}}; + {{1, 1025}, {2, 621}, {3, 325}, {4, 512}, {6, 534}}}; for (const auto [i, weight] : conthist_bonuses) { @@ -1864,12 +1853,12 @@ void update_quiet_histories( workerThread.mainHistory[us][move.from_to()] << bonus; // Untuned to prevent duplicate effort if (ss->ply < LOW_PLY_HISTORY_SIZE) - workerThread.lowPlyHistory[ss->ply][move.from_to()] << bonus * 874 / 1024; + workerThread.lowPlyHistory[ss->ply][move.from_to()] << bonus * 879 / 1024; - update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus * 853 / 1024); + update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus * 888 / 1024); int pIndex = pawn_structure_index(pos); - workerThread.pawnHistory[pIndex][pos.moved_piece(move)][move.to_sq()] << bonus * 628 / 1024; + workerThread.pawnHistory[pIndex][pos.moved_piece(move)][move.to_sq()] << bonus * 634 / 1024; } } diff --git a/tests/.gitattributes b/tests/.gitattributes new file mode 100644 index 00000000000..dfdb8b771ce --- /dev/null +++ b/tests/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/tests/testing.py b/tests/testing.py index d51ca89ac92..bc1f6b15bf9 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -150,6 +150,7 @@ def __init__(self): self.failed_test_suites = 0 self.passed_tests = 0 self.failed_tests = 0 + self.stop_on_failure = True def has_failed(self) -> bool: return self.failed_test_suites > 0 @@ -167,6 +168,9 @@ def run(self, classes: List[type]) -> bool: self.failed_test_suites += 1 else: self.passed_test_suites += 1 + except Exception as e: + self.failed_test_suites += 1 + print(f"\n{RED_COLOR}Error: {e}{RESET_COLOR}") finally: os.chdir(original_cwd) @@ -226,6 +230,10 @@ def __run_test_method(self, test_instance, method: str) -> int: if isinstance(e, AssertionError): self.__handle_assertion_error(t0, method) + if self.stop_on_failure: + self.__print_buffer_output(buffer) + raise e + fails += 1 finally: self.__print_buffer_output(buffer) @@ -286,6 +294,11 @@ def __init__( self.start() + def _check_process_alive(self): + if not self.process or self.process.poll() is not None: + print("\n".join(self.output)) + raise RuntimeError("Stockfish process has terminated") + def start(self): if self.cli: self.process = subprocess.run( @@ -314,6 +327,8 @@ def send_command(self, command: str): if not self.process: raise RuntimeError("Stockfish process is not started") + self._check_process_alive() + self.process.stdin.write(command + "\n") self.process.stdin.flush() @@ -355,6 +370,7 @@ def readline(self): raise RuntimeError("Stockfish process is not started") while True: + self._check_process_alive() line = self.process.stdout.readline().strip() self.output.append(line)