diff --git a/.github/workflows/games.yml b/.github/workflows/games.yml index f0bca442f..a50ca594b 100644 --- a/.github/workflows/games.yml +++ b/.github/workflows/games.yml @@ -19,22 +19,22 @@ jobs: working-directory: Stockfish/src run: make -j build debug=yes - - name: Checkout fast-chess repo + - name: Checkout fastchess repo uses: actions/checkout@v4 with: - repository: Disservin/fast-chess - path: fast-chess - ref: d54af1910d5479c669dc731f1f54f9108a251951 + repository: Disservin/fastchess + path: fastchess + ref: 894616028492ae6114835195f14a899f6fa237d3 persist-credentials: false - - name: fast-chess build - working-directory: fast-chess + - name: fastchess build + working-directory: fastchess run: make -j - name: Run games - working-directory: fast-chess + working-directory: fastchess run: | - ./fast-chess -rounds 4 -games 2 -repeat -concurrency 4 -openings file=app/tests/data/openings.epd format=epd order=random -srand $RANDOM\ + ./fastchess -rounds 4 -games 2 -repeat -concurrency 4 -openings file=app/tests/data/openings.epd format=epd order=random -srand $RANDOM\ -engine name=sf1 cmd=/home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish\ -engine name=sf2 cmd=/home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish\ -ratinginterval 1 -report penta=true -each proto=uci tc=4+0.04 -log file=fast.log | tee fast.out diff --git a/.github/workflows/matetrack.yml b/.github/workflows/matetrack.yml index dc8dff8d5..85c2be3e7 100644 --- a/.github/workflows/matetrack.yml +++ b/.github/workflows/matetrack.yml @@ -24,7 +24,7 @@ jobs: with: repository: vondele/matetrack path: matetrack - ref: 814160f82e6428ed2f6522dc06c2a6fa539cd413 + ref: 4f8a80860ed8f3607f05a9195df8b40203bdc360 persist-credentials: false - name: matetrack install deps @@ -50,5 +50,22 @@ jobs: - name: Run matetrack working-directory: matetrack run: | - python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 | tee matecheckout.out + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 | tee matecheckout.out ! grep "issues were detected" matecheckout.out > /dev/null + + - name: Run matetrack with --syzygy50MoveRule false + working-directory: matetrack + run: | + grep 5men cursed.epd > cursed5.epd + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile cursed5.epd --nodes 100000 --syzygy50MoveRule false | tee matecheckcursed.out + ! grep "issues were detected" matecheckcursed.out > /dev/null + + - name: Verify mate and TB win count for matecheckcursed.out + working-directory: matetrack + run: | + mates=$(grep "Found mates:" matecheckcursed.out | awk '{print $3}') + tbwins=$(grep "Found TB wins:" matecheckcursed.out | awk '{print $4}') + if [ $(($mates + $tbwins)) -ne 32 ]; then + echo "Sum of mates and TB wins is not 32 in matecheckcursed.out" >&2 + exit 1 + fi diff --git a/AUTHORS b/AUTHORS index f6468c561..a345bb69f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -125,10 +125,12 @@ jundery Justin Blanchard (UncombedCoconut) Kelly Wilson Ken Takusagawa +Kenneth Lee (kennethlee33) Kian E (KJE-98) kinderchocolate Kiran Panditrao (Krgp) Kojirion +Krisztián Peőcz Krystian Kuzniarek (kuzkry) Leonardo Ljubičić (ICCF World Champion) Leonid Pechenik (lp--) diff --git a/src/bitboard.h b/src/bitboard.h index 6f9cca0bd..df15113bd 100644 --- a/src/bitboard.h +++ b/src/bitboard.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -268,11 +269,10 @@ inline int popcount(Bitboard b) { #ifndef USE_POPCNT - union { - Bitboard bb; - uint16_t u[4]; - } v = {b}; - return PopCnt16[v.u[0]] + PopCnt16[v.u[1]] + PopCnt16[v.u[2]] + PopCnt16[v.u[3]]; + std::uint16_t indices[4]; + std::memcpy(indices, &b, sizeof(b)); + return PopCnt16[indices[0]] + PopCnt16[indices[1]] + PopCnt16[indices[2]] + + PopCnt16[indices[3]]; #elif defined(_MSC_VER) diff --git a/src/engine.cpp b/src/engine.cpp index aba0ae993..02e60ee47 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -59,57 +59,83 @@ Engine::Engine(std::optional path) : NN::NetworkSmall({EvalFileDefaultNameSmall, "None", ""}, NN::EmbeddedNNUEType::SMALL))) { pos.set(StartFEN, false, &states->back()); - options["Debug Log File"] << Option("", [](const Option& o) { - start_logger(o); - return std::nullopt; - }); - options["NumaPolicy"] << Option("auto", [this](const Option& o) { - set_numa_config_from_option(o); - return numa_config_information_as_string() + "\n" - + thread_allocation_information_as_string(); - }); + options.add( // + "Debug Log File", Option("", [](const Option& o) { + start_logger(o); + return std::nullopt; + })); - options["Threads"] << Option(1, 1, 1024, [this](const Option&) { - resize_threads(); - return thread_allocation_information_as_string(); - }); + options.add( // + "NumaPolicy", Option("auto", [this](const Option& o) { + set_numa_config_from_option(o); + return numa_config_information_as_string() + "\n" + + thread_allocation_information_as_string(); + })); - options["Hash"] << Option(16, 1, MaxHashMB, [this](const Option& o) { - set_tt_size(o); - return std::nullopt; - }); + options.add( // + "Threads", Option(1, 1, 1024, [this](const Option&) { + resize_threads(); + return thread_allocation_information_as_string(); + })); - options["Clear Hash"] << Option([this](const Option&) { - search_clear(); - return std::nullopt; - }); - options["Ponder"] << Option(false); - options["MultiPV"] << Option(1, 1, MAX_MOVES); - options["Skill Level"] << Option(20, 0, 20); - options["Move Overhead"] << Option(10, 0, 5000); - options["nodestime"] << Option(0, 0, 10000); - options["UCI_Chess960"] << Option(false); - options["UCI_LimitStrength"] << Option(false); - options["UCI_Elo"] << Option(Stockfish::Search::Skill::LowestElo, - Stockfish::Search::Skill::LowestElo, - Stockfish::Search::Skill::HighestElo); - options["UCI_ShowWDL"] << Option(false); - options["SyzygyPath"] << Option("", [](const Option& o) { - Tablebases::init(o); - return std::nullopt; - }); - options["SyzygyProbeDepth"] << Option(1, 1, 100); - options["Syzygy50MoveRule"] << Option(true); - options["SyzygyProbeLimit"] << Option(7, 0, 7); - options["EvalFile"] << Option(EvalFileDefaultNameBig, [this](const Option& o) { - load_big_network(o); - return std::nullopt; - }); - options["EvalFileSmall"] << Option(EvalFileDefaultNameSmall, [this](const Option& o) { - load_small_network(o); - return std::nullopt; - }); + options.add( // + "Hash", Option(16, 1, MaxHashMB, [this](const Option& o) { + set_tt_size(o); + return std::nullopt; + })); + + options.add( // + "Clear Hash", Option([this](const Option&) { + search_clear(); + return std::nullopt; + })); + + options.add( // + "Ponder", Option(false)); + + options.add( // + "MultiPV", Option(1, 1, MAX_MOVES)); + + options.add("Skill Level", Option(20, 0, 20)); + + options.add("Move Overhead", Option(10, 0, 5000)); + + options.add("nodestime", Option(0, 0, 10000)); + + options.add("UCI_Chess960", Option(false)); + + options.add("UCI_LimitStrength", Option(false)); + + options.add("UCI_Elo", + Option(Stockfish::Search::Skill::LowestElo, Stockfish::Search::Skill::LowestElo, + Stockfish::Search::Skill::HighestElo)); + + options.add("UCI_ShowWDL", Option(false)); + + options.add( // + "SyzygyPath", Option("", [](const Option& o) { + Tablebases::init(o); + return std::nullopt; + })); + + options.add("SyzygyProbeDepth", Option(1, 1, 100)); + + options.add("Syzygy50MoveRule", Option(true)); + + options.add("SyzygyProbeLimit", Option(7, 0, 7)); + + options.add( // + "EvalFile", Option(EvalFileDefaultNameBig, [this](const Option& o) { + load_big_network(o); + return std::nullopt; + })); + + options.add( // + "EvalFileSmall", Option(EvalFileDefaultNameSmall, [this](const Option& o) { + load_small_network(o); + return std::nullopt; + })); load_networks(); resize_threads(); @@ -343,5 +369,4 @@ std::string Engine::thread_allocation_information_as_string() const { return ss.str(); } - } diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 4fce86e3a..dddb56860 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -76,7 +76,7 @@ Value Eval::evaluate(const Eval::NNUE::Networks& networks, // Blend optimism and eval with nnue complexity int nnueComplexity = std::abs(psqt - positional); optimism += optimism * nnueComplexity / 468; - nnue -= nnue * nnueComplexity / (smallNet ? 20233 : 17879); + nnue -= nnue * nnueComplexity / 18000; int material = 535 * pos.count() + pos.non_pawn_material(); int v = (nnue * (77777 + material) + optimism * (7777 + material)) / 77777; diff --git a/src/history.h b/src/history.h index 15095cd0b..fd9b98b98 100644 --- a/src/history.h +++ b/src/history.h @@ -54,10 +54,6 @@ inline int pawn_structure_index(const Position& pos) { return pos.pawn_key() & ((T == Normal ? PAWN_HISTORY_SIZE : CORRECTION_HISTORY_SIZE) - 1); } -inline int major_piece_index(const Position& pos) { - return pos.major_piece_key() & (CORRECTION_HISTORY_SIZE - 1); -} - inline int minor_piece_index(const Position& pos) { return pos.minor_piece_key() & (CORRECTION_HISTORY_SIZE - 1); } @@ -75,7 +71,7 @@ inline int non_pawn_index(const Position& pos) { template class StatsEntry { - static_assert(std::is_arithmetic::value, "Not an arithmetic type"); + static_assert(std::is_arithmetic_v, "Not an arithmetic type"); static_assert(D <= std::numeric_limits::max(), "D overflows T"); T entry; @@ -136,18 +132,17 @@ using PawnHistory = Stats +template struct CorrHistTypedef { - using type = Stats; + using type = Stats; }; template<> diff --git a/src/misc.h b/src/misc.h index 81c7b17fe..d2cbb699d 100644 --- a/src/misc.h +++ b/src/misc.h @@ -120,11 +120,8 @@ void sync_cout_start(); void sync_cout_end(); // True if and only if the binary is compiled on a little-endian machine -static inline const union { - uint32_t i; - char c[4]; -} Le = {0x01020304}; -static inline const bool IsLittleEndian = (Le.c[0] == 4); +static inline const std::uint16_t Le = 1; +static inline const bool IsLittleEndian = *reinterpret_cast(&Le) == 1; template @@ -158,6 +155,10 @@ struct MultiArrayHelper { using ChildType = T; }; +template +constexpr bool is_strictly_assignable_v = + std::is_assignable_v && (std::is_same_v || !std::is_convertible_v); + } // MultiArray is a generic N-dimensional array. @@ -215,7 +216,8 @@ class MultiArray { template void fill(const U& v) { - static_assert(std::is_assignable_v, "Cannot assign fill value to entry type"); + static_assert(Detail::is_strictly_assignable_v, + "Cannot assign fill value to entry type"); for (auto& ele : data_) { if constexpr (sizeof...(Sizes) == 0) diff --git a/src/nnue/layers/affine_transform.h b/src/nnue/layers/affine_transform.h index f5c640fb9..dac727e23 100644 --- a/src/nnue/layers/affine_transform.h +++ b/src/nnue/layers/affine_transform.h @@ -102,7 +102,7 @@ static void affine_transform_non_ssse3(std::int32_t* output, product = vmlal_s8(product, inputVector[j * 2 + 1], row[j * 2 + 1]); sum = vpadalq_s16(sum, product); } - output[i] = sum[0] + sum[1] + sum[2] + sum[3]; + output[i] = Simd::neon_m128_reduce_add_epi32(sum); #endif } diff --git a/src/nnue/layers/affine_transform_sparse_input.h b/src/nnue/layers/affine_transform_sparse_input.h index 248e19dd0..be5e30b5e 100644 --- a/src/nnue/layers/affine_transform_sparse_input.h +++ b/src/nnue/layers/affine_transform_sparse_input.h @@ -38,99 +38,41 @@ namespace Stockfish::Eval::NNUE::Layers { #if (USE_SSSE3 | (USE_NEON >= 8)) +static constexpr int lsb_index64[64] = { + 0, 47, 1, 56, 48, 27, 2, 60, 57, 49, 41, 37, 28, 16, 3, 61, 54, 58, 35, 52, 50, 42, + 21, 44, 38, 32, 29, 23, 17, 11, 4, 62, 46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, + 31, 22, 10, 45, 25, 39, 14, 33, 19, 30, 9, 24, 13, 18, 8, 12, 7, 6, 5, 63}; + +constexpr int constexpr_lsb(uint64_t bb) { + assert(bb != 0); + constexpr uint64_t debruijn64 = 0x03F79D71B4CB0A89ULL; + return lsb_index64[((bb ^ (bb - 1)) * debruijn64) >> 58]; +} + +alignas(CacheLineSize) static constexpr struct OffsetIndices { #if (USE_SSE41) -alignas(CacheLineSize) static constexpr std::uint8_t + std::uint8_t offset_indices[256][8]; #else -alignas(CacheLineSize) static constexpr std::uint16_t + std::uint16_t offset_indices[256][8]; #endif - lookup_indices[256][8] = { - {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}, {1, 0, 0, 0, 0, 0, 0, 0}, - {0, 1, 0, 0, 0, 0, 0, 0}, {2, 0, 0, 0, 0, 0, 0, 0}, {0, 2, 0, 0, 0, 0, 0, 0}, - {1, 2, 0, 0, 0, 0, 0, 0}, {0, 1, 2, 0, 0, 0, 0, 0}, {3, 0, 0, 0, 0, 0, 0, 0}, - {0, 3, 0, 0, 0, 0, 0, 0}, {1, 3, 0, 0, 0, 0, 0, 0}, {0, 1, 3, 0, 0, 0, 0, 0}, - {2, 3, 0, 0, 0, 0, 0, 0}, {0, 2, 3, 0, 0, 0, 0, 0}, {1, 2, 3, 0, 0, 0, 0, 0}, - {0, 1, 2, 3, 0, 0, 0, 0}, {4, 0, 0, 0, 0, 0, 0, 0}, {0, 4, 0, 0, 0, 0, 0, 0}, - {1, 4, 0, 0, 0, 0, 0, 0}, {0, 1, 4, 0, 0, 0, 0, 0}, {2, 4, 0, 0, 0, 0, 0, 0}, - {0, 2, 4, 0, 0, 0, 0, 0}, {1, 2, 4, 0, 0, 0, 0, 0}, {0, 1, 2, 4, 0, 0, 0, 0}, - {3, 4, 0, 0, 0, 0, 0, 0}, {0, 3, 4, 0, 0, 0, 0, 0}, {1, 3, 4, 0, 0, 0, 0, 0}, - {0, 1, 3, 4, 0, 0, 0, 0}, {2, 3, 4, 0, 0, 0, 0, 0}, {0, 2, 3, 4, 0, 0, 0, 0}, - {1, 2, 3, 4, 0, 0, 0, 0}, {0, 1, 2, 3, 4, 0, 0, 0}, {5, 0, 0, 0, 0, 0, 0, 0}, - {0, 5, 0, 0, 0, 0, 0, 0}, {1, 5, 0, 0, 0, 0, 0, 0}, {0, 1, 5, 0, 0, 0, 0, 0}, - {2, 5, 0, 0, 0, 0, 0, 0}, {0, 2, 5, 0, 0, 0, 0, 0}, {1, 2, 5, 0, 0, 0, 0, 0}, - {0, 1, 2, 5, 0, 0, 0, 0}, {3, 5, 0, 0, 0, 0, 0, 0}, {0, 3, 5, 0, 0, 0, 0, 0}, - {1, 3, 5, 0, 0, 0, 0, 0}, {0, 1, 3, 5, 0, 0, 0, 0}, {2, 3, 5, 0, 0, 0, 0, 0}, - {0, 2, 3, 5, 0, 0, 0, 0}, {1, 2, 3, 5, 0, 0, 0, 0}, {0, 1, 2, 3, 5, 0, 0, 0}, - {4, 5, 0, 0, 0, 0, 0, 0}, {0, 4, 5, 0, 0, 0, 0, 0}, {1, 4, 5, 0, 0, 0, 0, 0}, - {0, 1, 4, 5, 0, 0, 0, 0}, {2, 4, 5, 0, 0, 0, 0, 0}, {0, 2, 4, 5, 0, 0, 0, 0}, - {1, 2, 4, 5, 0, 0, 0, 0}, {0, 1, 2, 4, 5, 0, 0, 0}, {3, 4, 5, 0, 0, 0, 0, 0}, - {0, 3, 4, 5, 0, 0, 0, 0}, {1, 3, 4, 5, 0, 0, 0, 0}, {0, 1, 3, 4, 5, 0, 0, 0}, - {2, 3, 4, 5, 0, 0, 0, 0}, {0, 2, 3, 4, 5, 0, 0, 0}, {1, 2, 3, 4, 5, 0, 0, 0}, - {0, 1, 2, 3, 4, 5, 0, 0}, {6, 0, 0, 0, 0, 0, 0, 0}, {0, 6, 0, 0, 0, 0, 0, 0}, - {1, 6, 0, 0, 0, 0, 0, 0}, {0, 1, 6, 0, 0, 0, 0, 0}, {2, 6, 0, 0, 0, 0, 0, 0}, - {0, 2, 6, 0, 0, 0, 0, 0}, {1, 2, 6, 0, 0, 0, 0, 0}, {0, 1, 2, 6, 0, 0, 0, 0}, - {3, 6, 0, 0, 0, 0, 0, 0}, {0, 3, 6, 0, 0, 0, 0, 0}, {1, 3, 6, 0, 0, 0, 0, 0}, - {0, 1, 3, 6, 0, 0, 0, 0}, {2, 3, 6, 0, 0, 0, 0, 0}, {0, 2, 3, 6, 0, 0, 0, 0}, - {1, 2, 3, 6, 0, 0, 0, 0}, {0, 1, 2, 3, 6, 0, 0, 0}, {4, 6, 0, 0, 0, 0, 0, 0}, - {0, 4, 6, 0, 0, 0, 0, 0}, {1, 4, 6, 0, 0, 0, 0, 0}, {0, 1, 4, 6, 0, 0, 0, 0}, - {2, 4, 6, 0, 0, 0, 0, 0}, {0, 2, 4, 6, 0, 0, 0, 0}, {1, 2, 4, 6, 0, 0, 0, 0}, - {0, 1, 2, 4, 6, 0, 0, 0}, {3, 4, 6, 0, 0, 0, 0, 0}, {0, 3, 4, 6, 0, 0, 0, 0}, - {1, 3, 4, 6, 0, 0, 0, 0}, {0, 1, 3, 4, 6, 0, 0, 0}, {2, 3, 4, 6, 0, 0, 0, 0}, - {0, 2, 3, 4, 6, 0, 0, 0}, {1, 2, 3, 4, 6, 0, 0, 0}, {0, 1, 2, 3, 4, 6, 0, 0}, - {5, 6, 0, 0, 0, 0, 0, 0}, {0, 5, 6, 0, 0, 0, 0, 0}, {1, 5, 6, 0, 0, 0, 0, 0}, - {0, 1, 5, 6, 0, 0, 0, 0}, {2, 5, 6, 0, 0, 0, 0, 0}, {0, 2, 5, 6, 0, 0, 0, 0}, - {1, 2, 5, 6, 0, 0, 0, 0}, {0, 1, 2, 5, 6, 0, 0, 0}, {3, 5, 6, 0, 0, 0, 0, 0}, - {0, 3, 5, 6, 0, 0, 0, 0}, {1, 3, 5, 6, 0, 0, 0, 0}, {0, 1, 3, 5, 6, 0, 0, 0}, - {2, 3, 5, 6, 0, 0, 0, 0}, {0, 2, 3, 5, 6, 0, 0, 0}, {1, 2, 3, 5, 6, 0, 0, 0}, - {0, 1, 2, 3, 5, 6, 0, 0}, {4, 5, 6, 0, 0, 0, 0, 0}, {0, 4, 5, 6, 0, 0, 0, 0}, - {1, 4, 5, 6, 0, 0, 0, 0}, {0, 1, 4, 5, 6, 0, 0, 0}, {2, 4, 5, 6, 0, 0, 0, 0}, - {0, 2, 4, 5, 6, 0, 0, 0}, {1, 2, 4, 5, 6, 0, 0, 0}, {0, 1, 2, 4, 5, 6, 0, 0}, - {3, 4, 5, 6, 0, 0, 0, 0}, {0, 3, 4, 5, 6, 0, 0, 0}, {1, 3, 4, 5, 6, 0, 0, 0}, - {0, 1, 3, 4, 5, 6, 0, 0}, {2, 3, 4, 5, 6, 0, 0, 0}, {0, 2, 3, 4, 5, 6, 0, 0}, - {1, 2, 3, 4, 5, 6, 0, 0}, {0, 1, 2, 3, 4, 5, 6, 0}, {7, 0, 0, 0, 0, 0, 0, 0}, - {0, 7, 0, 0, 0, 0, 0, 0}, {1, 7, 0, 0, 0, 0, 0, 0}, {0, 1, 7, 0, 0, 0, 0, 0}, - {2, 7, 0, 0, 0, 0, 0, 0}, {0, 2, 7, 0, 0, 0, 0, 0}, {1, 2, 7, 0, 0, 0, 0, 0}, - {0, 1, 2, 7, 0, 0, 0, 0}, {3, 7, 0, 0, 0, 0, 0, 0}, {0, 3, 7, 0, 0, 0, 0, 0}, - {1, 3, 7, 0, 0, 0, 0, 0}, {0, 1, 3, 7, 0, 0, 0, 0}, {2, 3, 7, 0, 0, 0, 0, 0}, - {0, 2, 3, 7, 0, 0, 0, 0}, {1, 2, 3, 7, 0, 0, 0, 0}, {0, 1, 2, 3, 7, 0, 0, 0}, - {4, 7, 0, 0, 0, 0, 0, 0}, {0, 4, 7, 0, 0, 0, 0, 0}, {1, 4, 7, 0, 0, 0, 0, 0}, - {0, 1, 4, 7, 0, 0, 0, 0}, {2, 4, 7, 0, 0, 0, 0, 0}, {0, 2, 4, 7, 0, 0, 0, 0}, - {1, 2, 4, 7, 0, 0, 0, 0}, {0, 1, 2, 4, 7, 0, 0, 0}, {3, 4, 7, 0, 0, 0, 0, 0}, - {0, 3, 4, 7, 0, 0, 0, 0}, {1, 3, 4, 7, 0, 0, 0, 0}, {0, 1, 3, 4, 7, 0, 0, 0}, - {2, 3, 4, 7, 0, 0, 0, 0}, {0, 2, 3, 4, 7, 0, 0, 0}, {1, 2, 3, 4, 7, 0, 0, 0}, - {0, 1, 2, 3, 4, 7, 0, 0}, {5, 7, 0, 0, 0, 0, 0, 0}, {0, 5, 7, 0, 0, 0, 0, 0}, - {1, 5, 7, 0, 0, 0, 0, 0}, {0, 1, 5, 7, 0, 0, 0, 0}, {2, 5, 7, 0, 0, 0, 0, 0}, - {0, 2, 5, 7, 0, 0, 0, 0}, {1, 2, 5, 7, 0, 0, 0, 0}, {0, 1, 2, 5, 7, 0, 0, 0}, - {3, 5, 7, 0, 0, 0, 0, 0}, {0, 3, 5, 7, 0, 0, 0, 0}, {1, 3, 5, 7, 0, 0, 0, 0}, - {0, 1, 3, 5, 7, 0, 0, 0}, {2, 3, 5, 7, 0, 0, 0, 0}, {0, 2, 3, 5, 7, 0, 0, 0}, - {1, 2, 3, 5, 7, 0, 0, 0}, {0, 1, 2, 3, 5, 7, 0, 0}, {4, 5, 7, 0, 0, 0, 0, 0}, - {0, 4, 5, 7, 0, 0, 0, 0}, {1, 4, 5, 7, 0, 0, 0, 0}, {0, 1, 4, 5, 7, 0, 0, 0}, - {2, 4, 5, 7, 0, 0, 0, 0}, {0, 2, 4, 5, 7, 0, 0, 0}, {1, 2, 4, 5, 7, 0, 0, 0}, - {0, 1, 2, 4, 5, 7, 0, 0}, {3, 4, 5, 7, 0, 0, 0, 0}, {0, 3, 4, 5, 7, 0, 0, 0}, - {1, 3, 4, 5, 7, 0, 0, 0}, {0, 1, 3, 4, 5, 7, 0, 0}, {2, 3, 4, 5, 7, 0, 0, 0}, - {0, 2, 3, 4, 5, 7, 0, 0}, {1, 2, 3, 4, 5, 7, 0, 0}, {0, 1, 2, 3, 4, 5, 7, 0}, - {6, 7, 0, 0, 0, 0, 0, 0}, {0, 6, 7, 0, 0, 0, 0, 0}, {1, 6, 7, 0, 0, 0, 0, 0}, - {0, 1, 6, 7, 0, 0, 0, 0}, {2, 6, 7, 0, 0, 0, 0, 0}, {0, 2, 6, 7, 0, 0, 0, 0}, - {1, 2, 6, 7, 0, 0, 0, 0}, {0, 1, 2, 6, 7, 0, 0, 0}, {3, 6, 7, 0, 0, 0, 0, 0}, - {0, 3, 6, 7, 0, 0, 0, 0}, {1, 3, 6, 7, 0, 0, 0, 0}, {0, 1, 3, 6, 7, 0, 0, 0}, - {2, 3, 6, 7, 0, 0, 0, 0}, {0, 2, 3, 6, 7, 0, 0, 0}, {1, 2, 3, 6, 7, 0, 0, 0}, - {0, 1, 2, 3, 6, 7, 0, 0}, {4, 6, 7, 0, 0, 0, 0, 0}, {0, 4, 6, 7, 0, 0, 0, 0}, - {1, 4, 6, 7, 0, 0, 0, 0}, {0, 1, 4, 6, 7, 0, 0, 0}, {2, 4, 6, 7, 0, 0, 0, 0}, - {0, 2, 4, 6, 7, 0, 0, 0}, {1, 2, 4, 6, 7, 0, 0, 0}, {0, 1, 2, 4, 6, 7, 0, 0}, - {3, 4, 6, 7, 0, 0, 0, 0}, {0, 3, 4, 6, 7, 0, 0, 0}, {1, 3, 4, 6, 7, 0, 0, 0}, - {0, 1, 3, 4, 6, 7, 0, 0}, {2, 3, 4, 6, 7, 0, 0, 0}, {0, 2, 3, 4, 6, 7, 0, 0}, - {1, 2, 3, 4, 6, 7, 0, 0}, {0, 1, 2, 3, 4, 6, 7, 0}, {5, 6, 7, 0, 0, 0, 0, 0}, - {0, 5, 6, 7, 0, 0, 0, 0}, {1, 5, 6, 7, 0, 0, 0, 0}, {0, 1, 5, 6, 7, 0, 0, 0}, - {2, 5, 6, 7, 0, 0, 0, 0}, {0, 2, 5, 6, 7, 0, 0, 0}, {1, 2, 5, 6, 7, 0, 0, 0}, - {0, 1, 2, 5, 6, 7, 0, 0}, {3, 5, 6, 7, 0, 0, 0, 0}, {0, 3, 5, 6, 7, 0, 0, 0}, - {1, 3, 5, 6, 7, 0, 0, 0}, {0, 1, 3, 5, 6, 7, 0, 0}, {2, 3, 5, 6, 7, 0, 0, 0}, - {0, 2, 3, 5, 6, 7, 0, 0}, {1, 2, 3, 5, 6, 7, 0, 0}, {0, 1, 2, 3, 5, 6, 7, 0}, - {4, 5, 6, 7, 0, 0, 0, 0}, {0, 4, 5, 6, 7, 0, 0, 0}, {1, 4, 5, 6, 7, 0, 0, 0}, - {0, 1, 4, 5, 6, 7, 0, 0}, {2, 4, 5, 6, 7, 0, 0, 0}, {0, 2, 4, 5, 6, 7, 0, 0}, - {1, 2, 4, 5, 6, 7, 0, 0}, {0, 1, 2, 4, 5, 6, 7, 0}, {3, 4, 5, 6, 7, 0, 0, 0}, - {0, 3, 4, 5, 6, 7, 0, 0}, {1, 3, 4, 5, 6, 7, 0, 0}, {0, 1, 3, 4, 5, 6, 7, 0}, - {2, 3, 4, 5, 6, 7, 0, 0}, {0, 2, 3, 4, 5, 6, 7, 0}, {1, 2, 3, 4, 5, 6, 7, 0}, - {0, 1, 2, 3, 4, 5, 6, 7}}; + + constexpr OffsetIndices() : + offset_indices() { + for (int i = 0; i < 256; ++i) + { + std::uint64_t j = i, k = 0; + while (j) + { + offset_indices[i][k++] = constexpr_lsb(j); + j &= j - 1; + } + while (k < 8) + offset_indices[i][k++] = 0; + } + } + +} Lookup; // Find indices of nonzero numbers in an int32_t array template @@ -196,9 +138,9 @@ void find_nnz(const std::int32_t* input, std::uint16_t* out, IndexType& count_ou } for (IndexType j = 0; j < OutputsPerChunk; ++j) { - const auto lookup = (nnz >> (j * 8)) & 0xFF; - const auto offsets = - vec128_load(reinterpret_cast(&lookup_indices[lookup])); + const unsigned lookup = (nnz >> (j * 8)) & 0xFF; + const vec128_t offsets = + vec128_load(reinterpret_cast(&Lookup.offset_indices[lookup])); vec128_storeu(reinterpret_cast(out + count), vec128_add(base, offsets)); count += popcount(lookup); base = vec128_add(base, increment); diff --git a/src/nnue/network.cpp b/src/nnue/network.cpp index 6923e5a07..7ba358744 100644 --- a/src/nnue/network.cpp +++ b/src/nnue/network.cpp @@ -280,12 +280,6 @@ void Network::verify(std::string } -template -void Network::hint_common_access( - const Position& pos, AccumulatorCaches::Cache* cache) const { - featureTransformer->hint_common_access(pos, cache); -} - template NnueEvalTrace Network::trace_evaluate(const Position& pos, diff --git a/src/nnue/network.h b/src/nnue/network.h index f99fa1182..764481d94 100644 --- a/src/nnue/network.h +++ b/src/nnue/network.h @@ -67,9 +67,6 @@ class Network { AccumulatorCaches::Cache* cache) const; - void hint_common_access(const Position& pos, - AccumulatorCaches::Cache* cache) const; - void verify(std::string evalfilePath, const std::function&) const; NnueEvalTrace trace_evaluate(const Position& pos, AccumulatorCaches::Cache* cache) const; diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h index 26bdb3018..a5e2df4b2 100644 --- a/src/nnue/nnue_feature_transformer.h +++ b/src/nnue/nnue_feature_transformer.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include "../position.h" @@ -40,6 +41,11 @@ using BiasType = std::int16_t; using WeightType = std::int16_t; using PSQTWeightType = std::int32_t; +enum IncUpdateDirection { + FORWARD, + BACKWARDS +}; + // If vector instructions are enabled, we update and refresh the // accumulator tile by tile such that each tile fits in the CPU's // vector registers. @@ -143,6 +149,46 @@ using psqt_vec_t = int32x4_t; #endif +// Returns the inverse of a permutation +template +constexpr std::array +invert_permutation(const std::array& order) { + std::array inverse{}; + for (std::size_t i = 0; i < order.size(); i++) + inverse[order[i]] = i; + return inverse; +} + +// Divide a byte region of size TotalSize to chunks of size +// BlockSize, and permute the blocks by a given order +template +void permute(T (&data)[N], const std::array& order) { + constexpr std::size_t TotalSize = N * sizeof(T); + + static_assert(TotalSize % (BlockSize * OrderSize) == 0, + "ChunkSize * OrderSize must perfectly divide TotalSize"); + + constexpr std::size_t ProcessChunkSize = BlockSize * OrderSize; + + std::array buffer{}; + + std::byte* const bytes = reinterpret_cast(data); + + for (std::size_t i = 0; i < TotalSize; i += ProcessChunkSize) + { + std::byte* const values = &bytes[i]; + + for (std::size_t j = 0; j < OrderSize; j++) + { + auto* const buffer_chunk = &buffer[j * BlockSize]; + auto* const value_chunk = &values[order[j] * BlockSize]; + + std::copy(value_chunk, value_chunk + BlockSize, buffer_chunk); + } + + std::copy(std::begin(buffer), std::end(buffer), values); + } +} // Compute optimal SIMD register count for feature transformer accumulation. template @@ -206,6 +252,7 @@ class FeatureTransformer { // Number of output dimensions for one side static constexpr IndexType HalfDimensions = TransformedFeatureDimensions; + static constexpr bool Big = TransformedFeatureDimensions == TransformedFeatureDimensionsBig; private: using Tiling = SIMDTiling; @@ -221,62 +268,42 @@ class FeatureTransformer { // Size of forward propagation buffer static constexpr std::size_t BufferSize = OutputDimensions * sizeof(OutputType); + // Store the order by which 128-bit blocks of a 1024-bit data must + // be permuted so that calling packus on adjacent vectors of 16-bit + // integers loaded from the data results in the pre-permutation order + static constexpr auto PackusEpi16Order = []() -> std::array { +#if defined(USE_AVX512) + // _mm512_packus_epi16 after permutation: + // | 0 | 2 | 4 | 6 | // Vector 0 + // | 1 | 3 | 5 | 7 | // Vector 1 + // | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | // Packed Result + return {0, 2, 4, 6, 1, 3, 5, 7}; +#elif defined(USE_AVX2) + // _mm256_packus_epi16 after permutation: + // | 0 | 2 | | 4 | 6 | // Vector 0, 2 + // | 1 | 3 | | 5 | 7 | // Vector 1, 3 + // | 0 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | // Packed Result + return {0, 2, 1, 3, 4, 6, 5, 7}; +#else + return {0, 1, 2, 3, 4, 5, 6, 7}; +#endif + }(); + + static constexpr auto InversePackusEpi16Order = invert_permutation(PackusEpi16Order); + // Hash value embedded in the evaluation file static constexpr std::uint32_t get_hash_value() { return FeatureSet::HashValue ^ (OutputDimensions * 2); } - static constexpr void order_packs([[maybe_unused]] uint64_t* v) { -#if defined(USE_AVX512) // _mm512_packs_epi16 ordering - uint64_t tmp0 = v[2], tmp1 = v[3]; - v[2] = v[8], v[3] = v[9]; - v[8] = v[4], v[9] = v[5]; - v[4] = tmp0, v[5] = tmp1; - tmp0 = v[6], tmp1 = v[7]; - v[6] = v[10], v[7] = v[11]; - v[10] = v[12], v[11] = v[13]; - v[12] = tmp0, v[13] = tmp1; -#elif defined(USE_AVX2) // _mm256_packs_epi16 ordering - std::swap(v[2], v[4]); - std::swap(v[3], v[5]); -#endif + void permute_weights() { + permute<16>(biases, PackusEpi16Order); + permute<16>(weights, PackusEpi16Order); } - static constexpr void inverse_order_packs([[maybe_unused]] uint64_t* v) { -#if defined(USE_AVX512) // Inverse _mm512_packs_epi16 ordering - uint64_t tmp0 = v[2], tmp1 = v[3]; - v[2] = v[4], v[3] = v[5]; - v[4] = v[8], v[5] = v[9]; - v[8] = tmp0, v[9] = tmp1; - tmp0 = v[6], tmp1 = v[7]; - v[6] = v[12], v[7] = v[13]; - v[12] = v[10], v[13] = v[11]; - v[10] = tmp0, v[11] = tmp1; -#elif defined(USE_AVX2) // Inverse _mm256_packs_epi16 ordering - std::swap(v[2], v[4]); - std::swap(v[3], v[5]); -#endif - } - - void permute_weights([[maybe_unused]] void (*order_fn)(uint64_t*)) { -#if defined(USE_AVX2) - #if defined(USE_AVX512) - constexpr IndexType di = 16; - #else - constexpr IndexType di = 8; - #endif - uint64_t* b = reinterpret_cast(&biases[0]); - for (IndexType i = 0; i < HalfDimensions * sizeof(BiasType) / sizeof(uint64_t); i += di) - order_fn(&b[i]); - - for (IndexType j = 0; j < InputDimensions; ++j) - { - uint64_t* w = reinterpret_cast(&weights[j * HalfDimensions]); - for (IndexType i = 0; i < HalfDimensions * sizeof(WeightType) / sizeof(uint64_t); - i += di) - order_fn(&w[i]); - } -#endif + void unpermute_weights() { + permute<16>(biases, InversePackusEpi16Order); + permute<16>(weights, InversePackusEpi16Order); } inline void scale_weights(bool read) { @@ -298,7 +325,7 @@ class FeatureTransformer { read_leb_128(stream, weights, HalfDimensions * InputDimensions); read_leb_128(stream, psqtWeights, PSQTBuckets * InputDimensions); - permute_weights(inverse_order_packs); + permute_weights(); scale_weights(true); return !stream.fail(); } @@ -306,14 +333,14 @@ class FeatureTransformer { // Write network parameters bool write_parameters(std::ostream& stream) { - permute_weights(order_packs); + unpermute_weights(); scale_weights(false); write_leb_128(stream, biases, HalfDimensions); write_leb_128(stream, weights, HalfDimensions * InputDimensions); write_leb_128(stream, psqtWeights, PSQTBuckets * InputDimensions); - permute_weights(inverse_order_packs); + permute_weights(); scale_weights(true); return !stream.fail(); } @@ -445,102 +472,121 @@ class FeatureTransformer { return psqt; } // end of function transform() - void hint_common_access(const Position& pos, - AccumulatorCaches::Cache* cache) const { - update_accumulator(pos, cache); - update_accumulator(pos, cache); - } - private: - template - StateInfo* try_find_computed_accumulator(const Position& pos) const { - // Look for a usable accumulator of an earlier position. We keep track - // of the estimated gain in terms of features to be added/subtracted. - StateInfo* st = pos.state(); - int gain = FeatureSet::refresh_cost(pos); - while (st->previous && !(st->*accPtr).computed[Perspective]) - { - // This governs when a full feature refresh is needed and how many - // updates are better than just one full refresh. - if (FeatureSet::requires_refresh(st, Perspective) - || (gain -= FeatureSet::update_cost(st) + 1) < 0) - break; - st = st->previous; - } - return st; - } - - // Computes the accumulator of the next position. - template - void update_accumulator_incremental(const Position& pos, StateInfo* computed) const { + // Given a computed accumulator, computes the accumulator of another position. + template + void update_accumulator_incremental(const Square ksq, + StateInfo* target_state, + const StateInfo* computed) const { + [[maybe_unused]] constexpr bool Forward = Direction == FORWARD; + [[maybe_unused]] constexpr bool Backwards = Direction == BACKWARDS; assert((computed->*accPtr).computed[Perspective]); - assert(computed->next != nullptr); -#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[Tiling::NumRegs]; - psqt_vec_t psqt[Tiling::NumPsqtRegs]; -#endif + StateInfo* next = Forward ? computed->next : computed->previous; - const Square ksq = pos.square(Perspective); + assert(next != nullptr); + assert(!(next->*accPtr).computed[Perspective]); // The size must be enough to contain the largest possible update. // That might depend on the feature set and generally relies on the // feature set's update cost calculation to be correct and never allow // updates with more added/removed features than MaxActiveDimensions. + // In this case, the maximum size of both feature addition and removal + // is 2, since we are incrementally updating one move at a time. FeatureSet::IndexList removed, added; - FeatureSet::append_changed_indices(ksq, computed->next->dirtyPiece, removed, - added); + if constexpr (Forward) + FeatureSet::append_changed_indices(ksq, next->dirtyPiece, removed, added); + else + FeatureSet::append_changed_indices(ksq, computed->dirtyPiece, added, + removed); - StateInfo* next = computed->next; - assert(!(next->*accPtr).computed[Perspective]); + if (removed.size() == 0 && added.size() == 0) + { + std::memcpy((next->*accPtr).accumulation[Perspective], + (computed->*accPtr).accumulation[Perspective], + HalfDimensions * sizeof(BiasType)); + std::memcpy((next->*accPtr).psqtAccumulation[Perspective], + (computed->*accPtr).psqtAccumulation[Perspective], + PSQTBuckets * sizeof(PSQTWeightType)); + } + else + { + assert(added.size() == 1 || added.size() == 2); + assert(removed.size() == 1 || removed.size() == 2); + if (Forward) + assert(added.size() <= removed.size()); + else + assert(removed.size() <= added.size()); #ifdef VECTOR - if ((removed.size() == 1 || removed.size() == 2) && added.size() == 1) - { auto* accIn = reinterpret_cast(&(computed->*accPtr).accumulation[Perspective][0]); auto* accOut = reinterpret_cast(&(next->*accPtr).accumulation[Perspective][0]); + const IndexType offsetA0 = HalfDimensions * added[0]; + auto* columnA0 = reinterpret_cast(&weights[offsetA0]); const IndexType offsetR0 = HalfDimensions * removed[0]; auto* columnR0 = reinterpret_cast(&weights[offsetR0]); - const IndexType offsetA = HalfDimensions * added[0]; - auto* columnA = reinterpret_cast(&weights[offsetA]); - if (removed.size() == 1) + if ((Forward && removed.size() == 1) || (Backwards && added.size() == 1)) { + assert(added.size() == 1 && removed.size() == 1); for (IndexType i = 0; i < HalfDimensions * sizeof(WeightType) / sizeof(vec_t); ++i) - accOut[i] = vec_add_16(vec_sub_16(accIn[i], columnR0[i]), columnA[i]); + accOut[i] = vec_add_16(vec_sub_16(accIn[i], columnR0[i]), columnA0[i]); } - else + else if (Forward && added.size() == 1) { + assert(removed.size() == 2); const IndexType offsetR1 = HalfDimensions * removed[1]; 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]), + accOut[i] = vec_sub_16(vec_add_16(accIn[i], columnA0[i]), vec_add_16(columnR0[i], columnR1[i])); } + else if (Backwards && removed.size() == 1) + { + assert(added.size() == 2); + const IndexType offsetA1 = HalfDimensions * added[1]; + auto* columnA1 = reinterpret_cast(&weights[offsetA1]); + + for (IndexType i = 0; i < HalfDimensions * sizeof(WeightType) / sizeof(vec_t); ++i) + accOut[i] = vec_add_16(vec_add_16(accIn[i], columnA0[i]), + vec_sub_16(columnA1[i], columnR0[i])); + } + else + { + assert(added.size() == 2 && removed.size() == 2); + const IndexType offsetA1 = HalfDimensions * added[1]; + auto* columnA1 = reinterpret_cast(&weights[offsetA1]); + const IndexType offsetR1 = HalfDimensions * removed[1]; + auto* columnR1 = reinterpret_cast(&weights[offsetR1]); + + for (IndexType i = 0; i < HalfDimensions * sizeof(WeightType) / sizeof(vec_t); ++i) + accOut[i] = + vec_add_16(accIn[i], vec_sub_16(vec_add_16(columnA0[i], columnA1[i]), + vec_add_16(columnR0[i], columnR1[i]))); + } auto* accPsqtIn = reinterpret_cast( &(computed->*accPtr).psqtAccumulation[Perspective][0]); auto* accPsqtOut = reinterpret_cast(&(next->*accPtr).psqtAccumulation[Perspective][0]); + const IndexType offsetPsqtA0 = PSQTBuckets * added[0]; + auto* columnPsqtA0 = reinterpret_cast(&psqtWeights[offsetPsqtA0]); const IndexType offsetPsqtR0 = PSQTBuckets * removed[0]; auto* columnPsqtR0 = reinterpret_cast(&psqtWeights[offsetPsqtR0]); - const IndexType offsetPsqtA = PSQTBuckets * added[0]; - auto* columnPsqtA = reinterpret_cast(&psqtWeights[offsetPsqtA]); - if (removed.size() == 1) + if ((Forward && removed.size() == 1) + || (Backwards && added.size() == 1)) // added.size() == removed.size() == 1 { for (std::size_t i = 0; i < PSQTBuckets * sizeof(PSQTWeightType) / sizeof(psqt_vec_t); ++i) accPsqtOut[i] = vec_add_psqt_32(vec_sub_psqt_32(accPsqtIn[i], columnPsqtR0[i]), - columnPsqtA[i]); + columnPsqtA0[i]); } - else + else if (Forward && added.size() == 1) { const IndexType offsetPsqtR1 = PSQTBuckets * removed[1]; auto* columnPsqtR1 = @@ -549,117 +595,78 @@ class FeatureTransformer { for (std::size_t i = 0; i < PSQTBuckets * sizeof(PSQTWeightType) / sizeof(psqt_vec_t); ++i) accPsqtOut[i] = - vec_sub_psqt_32(vec_add_psqt_32(accPsqtIn[i], columnPsqtA[i]), + vec_sub_psqt_32(vec_add_psqt_32(accPsqtIn[i], columnPsqtA0[i]), vec_add_psqt_32(columnPsqtR0[i], columnPsqtR1[i])); } - } - else - { - for (IndexType i = 0; i < HalfDimensions / Tiling::TileHeight; ++i) + else if (Backwards && removed.size() == 1) { - // Load accumulator - 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]); + const IndexType offsetPsqtA1 = PSQTBuckets * added[1]; + auto* columnPsqtA1 = + reinterpret_cast(&psqtWeights[offsetPsqtA1]); - // Difference calculation for the deactivated features - for (const auto index : removed) - { - 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 * 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 * Tiling::TileHeight]); - for (IndexType j = 0; j < Tiling::NumRegs; ++j) - vec_store(&accTileOut[j], acc[j]); + for (std::size_t i = 0; + i < PSQTBuckets * sizeof(PSQTWeightType) / sizeof(psqt_vec_t); ++i) + accPsqtOut[i] = + vec_add_psqt_32(vec_add_psqt_32(accPsqtIn[i], columnPsqtA0[i]), + vec_sub_psqt_32(columnPsqtA1[i], columnPsqtR0[i])); } - - for (IndexType i = 0; i < PSQTBuckets / Tiling::PsqtTileHeight; ++i) + else { - // Load accumulator - 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]); + const IndexType offsetPsqtA1 = PSQTBuckets * added[1]; + auto* columnPsqtA1 = + reinterpret_cast(&psqtWeights[offsetPsqtA1]); + const IndexType offsetPsqtR1 = PSQTBuckets * removed[1]; + auto* columnPsqtR1 = + reinterpret_cast(&psqtWeights[offsetPsqtR1]); - // Difference calculation for the deactivated features - for (const auto index : removed) - { - 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 * 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 * Tiling::PsqtTileHeight]); - for (std::size_t j = 0; j < Tiling::NumPsqtRegs; ++j) - vec_store_psqt(&accTilePsqtOut[j], psqt[j]); + for (std::size_t i = 0; + i < PSQTBuckets * sizeof(PSQTWeightType) / sizeof(psqt_vec_t); ++i) + accPsqtOut[i] = vec_add_psqt_32( + accPsqtIn[i], + vec_sub_psqt_32(vec_add_psqt_32(columnPsqtA0[i], columnPsqtA1[i]), + vec_add_psqt_32(columnPsqtR0[i], columnPsqtR1[i]))); } - } #else - std::memcpy((next->*accPtr).accumulation[Perspective], - (computed->*accPtr).accumulation[Perspective], - HalfDimensions * sizeof(BiasType)); - std::memcpy((next->*accPtr).psqtAccumulation[Perspective], - (computed->*accPtr).psqtAccumulation[Perspective], - PSQTBuckets * sizeof(PSQTWeightType)); + std::memcpy((next->*accPtr).accumulation[Perspective], + (computed->*accPtr).accumulation[Perspective], + HalfDimensions * sizeof(BiasType)); + std::memcpy((next->*accPtr).psqtAccumulation[Perspective], + (computed->*accPtr).psqtAccumulation[Perspective], + PSQTBuckets * sizeof(PSQTWeightType)); - // Difference calculation for the deactivated features - for (const auto index : removed) - { - const IndexType offset = HalfDimensions * index; - for (IndexType i = 0; i < HalfDimensions; ++i) - (next->*accPtr).accumulation[Perspective][i] -= weights[offset + i]; + // Difference calculation for the deactivated features + for (const auto index : removed) + { + const IndexType offset = HalfDimensions * index; + for (IndexType i = 0; i < HalfDimensions; ++i) + (next->*accPtr).accumulation[Perspective][i] -= weights[offset + i]; - for (std::size_t i = 0; i < PSQTBuckets; ++i) - (next->*accPtr).psqtAccumulation[Perspective][i] -= - psqtWeights[index * PSQTBuckets + i]; - } + for (std::size_t i = 0; i < PSQTBuckets; ++i) + (next->*accPtr).psqtAccumulation[Perspective][i] -= + psqtWeights[index * PSQTBuckets + i]; + } - // Difference calculation for the activated features - for (const auto index : added) - { - const IndexType offset = HalfDimensions * index; - for (IndexType i = 0; i < HalfDimensions; ++i) - (next->*accPtr).accumulation[Perspective][i] += weights[offset + i]; + // Difference calculation for the activated features + for (const auto index : added) + { + const IndexType offset = HalfDimensions * index; + for (IndexType i = 0; i < HalfDimensions; ++i) + (next->*accPtr).accumulation[Perspective][i] += weights[offset + i]; - for (std::size_t i = 0; i < PSQTBuckets; ++i) - (next->*accPtr).psqtAccumulation[Perspective][i] += - psqtWeights[index * PSQTBuckets + i]; - } + for (std::size_t i = 0; i < PSQTBuckets; ++i) + (next->*accPtr).psqtAccumulation[Perspective][i] += + psqtWeights[index * PSQTBuckets + i]; + } #endif + } (next->*accPtr).computed[Perspective] = true; - if (next != pos.state()) - update_accumulator_incremental(pos, next); + if (next != target_state) + update_accumulator_incremental(ksq, target_state, next); } + template void update_accumulator_refresh_cache(const Position& pos, AccumulatorCaches::Cache* cache) const { @@ -823,16 +830,40 @@ class FeatureTransformer { template void update_accumulator(const Position& pos, AccumulatorCaches::Cache* cache) const { - if ((pos.state()->*accPtr).computed[Perspective]) - return; - StateInfo* oldest = try_find_computed_accumulator(pos); + StateInfo* st = pos.state(); + if ((st->*accPtr).computed[Perspective]) + return; // nothing to do - if ((oldest->*accPtr).computed[Perspective] && oldest != pos.state()) - // Start from the oldest computed accumulator, update all the - // accumulators up to the current position. - update_accumulator_incremental(pos, oldest); - else - update_accumulator_refresh_cache(pos, cache); + [[maybe_unused]] // only used when !Big + int gain = FeatureSet::refresh_cost(pos); + // Look for a usable already computed accumulator of an earlier position. + // When computing the small accumulator, we keep track of the estimated gain in + // terms of features to be added/subtracted. + // When computing the big accumulator, we expect to be able to reuse any + // accumulators, so we always try to do an incremental update. + do + { + if (FeatureSet::requires_refresh(st, Perspective) + || (!Big && (gain -= FeatureSet::update_cost(st) < 0)) || !st->previous + || st->previous->next != st) + { + // compute accumulator from scratch for this position + update_accumulator_refresh_cache(pos, cache); + if (Big && st != pos.state()) + // when computing a big accumulator from scratch we can use it to + // efficiently compute the accumulator backwards, until we get to a king + // move. We expect that we will need these accumulators later anyway, so + // computing them now will save some work. + update_accumulator_incremental( + pos.square(Perspective), st, pos.state()); + return; + } + st = st->previous; + } while (!(st->*accPtr).computed[Perspective]); + + // Start from the oldest computed accumulator, update all the + // accumulators up to the current position. + update_accumulator_incremental(pos.square(Perspective), pos.state(), st); } template diff --git a/src/nnue/nnue_misc.cpp b/src/nnue/nnue_misc.cpp index 1e2690503..2220684da 100644 --- a/src/nnue/nnue_misc.cpp +++ b/src/nnue/nnue_misc.cpp @@ -30,7 +30,6 @@ #include #include -#include "../evaluate.h" #include "../position.h" #include "../types.h" #include "../uci.h" @@ -43,15 +42,6 @@ namespace Stockfish::Eval::NNUE { constexpr std::string_view PieceToChar(" PNBRQK pnbrqk"); -void hint_common_parent_position(const Position& pos, - const Networks& networks, - AccumulatorCaches& caches) { - if (Eval::use_smallnet(pos)) - networks.small.hint_common_access(pos, &caches.small); - else - networks.big.hint_common_access(pos, &caches.big); -} - namespace { // Converts a Value into (centi)pawns and writes it in a buffer. // The buffer must have capacity for at least 5 chars. diff --git a/src/nnue/nnue_misc.h b/src/nnue/nnue_misc.h index a7647f846..02212160a 100644 --- a/src/nnue/nnue_misc.h +++ b/src/nnue/nnue_misc.h @@ -54,9 +54,6 @@ struct Networks; struct AccumulatorCaches; std::string trace(Position& pos, const Networks& networks, AccumulatorCaches& caches); -void hint_common_parent_position(const Position& pos, - const Networks& networks, - AccumulatorCaches& caches); } // namespace Stockfish::Eval::NNUE } // namespace Stockfish diff --git a/src/position.cpp b/src/position.cpp index 49f520e01..37e9a2eb5 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -119,6 +119,9 @@ void Position::init() { for (Piece pc : Pieces) for (Square s = SQ_A1; s <= SQ_H8; ++s) Zobrist::psq[pc][s] = rng.rand(); + // pawns on these squares will promote + std::fill_n(Zobrist::psq[W_PAWN] + SQ_A8, 8, 0); + std::fill_n(Zobrist::psq[B_PAWN], 8, 0); for (File f = FILE_A; f <= FILE_H; ++f) Zobrist::enpassant[f] = rng.rand(); @@ -335,7 +338,7 @@ void Position::set_check_info() const { void Position::set_state() const { st->key = st->materialKey = 0; - st->majorPieceKey = st->minorPieceKey = 0; + st->minorPieceKey = 0; st->nonPawnKey[WHITE] = st->nonPawnKey[BLACK] = 0; st->pawnKey = Zobrist::noPawns; st->nonPawnMaterial[WHITE] = st->nonPawnMaterial[BLACK] = VALUE_ZERO; @@ -360,18 +363,9 @@ void Position::set_state() const { { st->nonPawnMaterial[color_of(pc)] += PieceValue[pc]; - if (type_of(pc) >= ROOK) - st->majorPieceKey ^= Zobrist::psq[pc][s]; - - else + if (type_of(pc) <= BISHOP) st->minorPieceKey ^= Zobrist::psq[pc][s]; } - - else - { - st->majorPieceKey ^= Zobrist::psq[pc][s]; - st->minorPieceKey ^= Zobrist::psq[pc][s]; - } } } @@ -385,7 +379,7 @@ void Position::set_state() const { for (Piece pc : Pieces) for (int cnt = 0; cnt < pieceCount[pc]; ++cnt) - st->materialKey ^= Zobrist::psq[pc][cnt]; + st->materialKey ^= Zobrist::psq[pc][8 + cnt]; } @@ -742,7 +736,6 @@ void Position::do_move(Move m, do_castling(us, from, to, rfrom, rto); k ^= Zobrist::psq[captured][rfrom] ^ Zobrist::psq[captured][rto]; - st->majorPieceKey ^= Zobrist::psq[captured][rfrom] ^ Zobrist::psq[captured][rto]; st->nonPawnKey[us] ^= Zobrist::psq[captured][rfrom] ^ Zobrist::psq[captured][rto]; captured = NO_PIECE; } @@ -773,10 +766,7 @@ void Position::do_move(Move m, st->nonPawnMaterial[them] -= PieceValue[captured]; st->nonPawnKey[them] ^= Zobrist::psq[captured][capsq]; - if (type_of(captured) >= ROOK) - st->majorPieceKey ^= Zobrist::psq[captured][capsq]; - - else + if (type_of(captured) <= BISHOP) st->minorPieceKey ^= Zobrist::psq[captured][capsq]; } @@ -789,7 +779,7 @@ void Position::do_move(Move m, remove_piece(capsq); k ^= Zobrist::psq[captured][capsq]; - st->materialKey ^= Zobrist::psq[captured][pieceCount[captured]]; + st->materialKey ^= Zobrist::psq[captured][8 + pieceCount[captured]]; // Reset rule 50 counter st->rule50 = 0; @@ -853,15 +843,12 @@ void Position::do_move(Move m, dp.dirty_num++; // Update hash keys - k ^= Zobrist::psq[pc][to] ^ Zobrist::psq[promotion][to]; - st->pawnKey ^= Zobrist::psq[pc][to]; - st->materialKey ^= - Zobrist::psq[promotion][pieceCount[promotion] - 1] ^ Zobrist::psq[pc][pieceCount[pc]]; + // Zobrist::psq[pc][to] is zero, so we don't need to clear it + k ^= Zobrist::psq[promotion][to]; + st->materialKey ^= Zobrist::psq[promotion][8 + pieceCount[promotion] - 1] + ^ Zobrist::psq[pc][8 + pieceCount[pc]]; - if (promotionType >= ROOK) - st->majorPieceKey ^= Zobrist::psq[promotion][to]; - - else + if (promotionType <= BISHOP) st->minorPieceKey ^= Zobrist::psq[promotion][to]; // Update material @@ -879,16 +866,7 @@ void Position::do_move(Move m, { st->nonPawnKey[us] ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; - if (type_of(pc) == KING) - { - st->majorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; - st->minorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; - } - - else if (type_of(pc) >= ROOK) - st->majorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; - - else + if (type_of(pc) <= BISHOP) st->minorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; } @@ -1039,11 +1017,6 @@ void Position::do_null_move(StateInfo& newSt, const TranspositionTable& tt) { st->next = &newSt; st = &newSt; - st->dirtyPiece.dirty_num = 0; - st->dirtyPiece.piece[0] = NO_PIECE; // Avoid checks in UpdateAccumulator() - st->accumulatorBig.computed[WHITE] = st->accumulatorBig.computed[BLACK] = - st->accumulatorSmall.computed[WHITE] = st->accumulatorSmall.computed[BLACK] = false; - if (st->epSquare != SQ_NONE) { st->key ^= Zobrist::enpassant[file_of(st->epSquare)]; @@ -1051,9 +1024,13 @@ void Position::do_null_move(StateInfo& newSt, const TranspositionTable& tt) { } st->key ^= Zobrist::side; - ++st->rule50; prefetch(tt.first_entry(key())); + st->dirtyPiece.dirty_num = 0; + st->dirtyPiece.piece[0] = NO_PIECE; // Avoid checks in UpdateAccumulator() + st->accumulatorBig.computed[WHITE] = st->accumulatorBig.computed[BLACK] = + st->accumulatorSmall.computed[WHITE] = st->accumulatorSmall.computed[BLACK] = false; + st->pliesFromNull = 0; sideToMove = ~sideToMove; @@ -1163,8 +1140,9 @@ bool Position::see_ge(Move m, int threshold) const { else if ((bb = stmAttackers & pieces(QUEEN))) { - if ((swap = QueenValue - swap) < res) - break; + swap = QueenValue - swap; + // implies that the previous recapture was done by a higher rated piece than a Queen (King is excluded) + assert(swap >= res); occupied ^= least_significant_square_bb(bb); attackers |= (attacks_bb(to, occupied) & pieces(BISHOP, QUEEN)) @@ -1187,11 +1165,12 @@ bool Position::is_draw(int ply) const { if (st->rule50 > 99 && (!checkers() || MoveList(*this).size())) return true; - // Return a draw score if a position repeats once earlier but strictly - // after the root, or repeats twice before or at the root. - return st->repetition && st->repetition < ply; + return is_repetition(ply); } +// Return a draw score if a position repeats once earlier but strictly +// after the root, or repeats twice before or at the root. +bool Position::is_repetition(int ply) const { return st->repetition && st->repetition < ply; } // Tests whether there has been at least one repetition // of positions since the last capture or pawn move. diff --git a/src/position.h b/src/position.h index 0d49a60a9..eeea1b74c 100644 --- a/src/position.h +++ b/src/position.h @@ -43,7 +43,6 @@ struct StateInfo { // Copied when making a move Key materialKey; Key pawnKey; - Key majorPieceKey; Key minorPieceKey; Key nonPawnKey[COLOR_NB]; Value nonPawnMaterial[COLOR_NB]; @@ -64,9 +63,9 @@ struct StateInfo { int repetition; // Used by NNUE + DirtyPiece dirtyPiece; Eval::NNUE::Accumulator accumulatorBig; Eval::NNUE::Accumulator accumulatorSmall; - DirtyPiece dirtyPiece; }; @@ -154,7 +153,6 @@ class Position { Key key() const; Key material_key() const; Key pawn_key() const; - Key major_piece_key() const; Key minor_piece_key() const; Key non_pawn_key(Color c) const; @@ -163,6 +161,7 @@ class Position { int game_ply() const; bool is_chess960() const; bool is_draw(int ply) const; + bool is_repetition(int ply) const; bool upcoming_repetition(int ply) const; bool has_repeated() const; int rule50_count() const; @@ -304,8 +303,6 @@ inline Key Position::pawn_key() const { return st->pawnKey; } inline Key Position::material_key() const { return st->materialKey; } -inline Key Position::major_piece_key() const { return st->majorPieceKey; } - inline Key Position::minor_piece_key() const { return st->minorPieceKey; } inline Key Position::non_pawn_key(Color c) const { return st->nonPawnKey[c]; } diff --git a/src/search.cpp b/src/search.cpp index d32516455..7d93c02a4 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -42,7 +42,6 @@ #include "nnue/network.h" #include "nnue/nnue_accumulator.h" #include "nnue/nnue_common.h" -#include "nnue/nnue_misc.h" #include "position.h" #include "syzygy/tbprobe.h" #include "thread.h" @@ -65,6 +64,12 @@ using namespace Search; namespace { +// (*Scalers): +// The values with Scaler asterisks have proven non-linear scaling. +// They are optimized to time controls of 180 + 1.8 and longer, +// so changing them or adding conditions that are similar requires +// tests at these types of time controls. + // Futility margin Value futility_margin(Depth d, bool noTtCutNode, bool improving, bool oppWorsening) { Value futilityMult = 112 - 26 * noTtCutNode; @@ -78,32 +83,47 @@ constexpr int futility_move_count(bool improving, Depth depth) { return (3 + depth * depth) / (2 - improving); } -int correction_value(const Worker& w, const Position& pos, const Stack* ss) { +int correction_value(const Worker& w, const Position& pos, const Stack* const ss) { const Color us = pos.side_to_move(); const auto m = (ss - 1)->currentMove; - const auto pcv = w.pawnCorrectionHistory[us][pawn_structure_index(pos)]; - const auto macv = w.majorPieceCorrectionHistory[us][major_piece_index(pos)]; - const auto micv = w.minorPieceCorrectionHistory[us][minor_piece_index(pos)]; - const auto wnpcv = w.nonPawnCorrectionHistory[WHITE][us][non_pawn_index(pos)]; - const auto bnpcv = w.nonPawnCorrectionHistory[BLACK][us][non_pawn_index(pos)]; + const auto pcv = w.pawnCorrectionHistory[pawn_structure_index(pos)][us]; + const auto micv = w.minorPieceCorrectionHistory[minor_piece_index(pos)][us]; + const auto wnpcv = w.nonPawnCorrectionHistory[WHITE][non_pawn_index(pos)][us]; + const auto bnpcv = w.nonPawnCorrectionHistory[BLACK][non_pawn_index(pos)][us]; const auto cntcv = m.is_ok() ? (*(ss - 2)->continuationCorrectionHistory)[pos.piece_on(m.to_sq())][m.to_sq()] : 0; - return (6922 * pcv + 3837 * macv + 6238 * micv + 7490 * (wnpcv + bnpcv) + 6270 * cntcv); + return 6995 * pcv + 6593 * micv + 7753 * (wnpcv + bnpcv) + 6049 * cntcv; } // Add correctionHistory value to raw staticEval and guarantee evaluation // does not hit the tablebase range. -Value to_corrected_static_eval(Value v, const int cv) { +Value to_corrected_static_eval(const Value v, const int cv) { return std::clamp(v + cv / 131072, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); } -// History and stats update bonus, based on depth -int stat_bonus(Depth d) { return std::min(154 * d - 102, 1661); } +void update_correction_history(const Position& pos, + Stack* const ss, + Search::Worker& workerThread, + const int bonus) { + const Move m = (ss - 1)->currentMove; + const Color us = pos.side_to_move(); -// History and stats update malus, based on depth -int stat_malus(Depth d) { return std::min(831 * d - 269, 2666); } + static constexpr int nonPawnWeight = 165; + + workerThread.pawnCorrectionHistory[pawn_structure_index(pos)][us] + << bonus * 109 / 128; + workerThread.minorPieceCorrectionHistory[minor_piece_index(pos)][us] << bonus * 141 / 128; + workerThread.nonPawnCorrectionHistory[WHITE][non_pawn_index(pos)][us] + << bonus * nonPawnWeight / 128; + workerThread.nonPawnCorrectionHistory[BLACK][non_pawn_index(pos)][us] + << bonus * nonPawnWeight / 128; + + if (m.is_ok()) + (*(ss - 2)->continuationCorrectionHistory)[pos.piece_on(m.to_sq())][m.to_sq()] + << bonus * 138 / 128; +} // 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); } @@ -121,7 +141,8 @@ void update_all_stats(const Position& pos, ValueList& quietsSearched, ValueList& capturesSearched, Depth depth, - bool isTTMove); + bool isTTMove, + int moveCount); } // namespace @@ -321,7 +342,7 @@ void Search::Worker::iterative_deepening() { int searchAgainCounter = 0; - lowPlyHistory.fill(97); + lowPlyHistory.fill(95); // Iterative deepening loop until requested to stop or the target depth is reached while (++rootDepth < MAX_PLY && !threads.stop @@ -357,13 +378,13 @@ void Search::Worker::iterative_deepening() { selDepth = 0; // Reset aspiration window starting size - delta = 5 + std::abs(rootMoves[pvIdx].meanSquaredScore) / 12991; + delta = 5 + std::abs(rootMoves[pvIdx].meanSquaredScore) / 13000; 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] = 141 * avg / (std::abs(avg) + 83); + // Adjust optimism based on root move's averageScore + optimism[us] = 138 * avg / (std::abs(avg) + 81); optimism[~us] = -optimism[us]; // Start with a small aspiration window and, in the case of a fail @@ -553,12 +574,11 @@ void Search::Worker::iterative_deepening() { // Reset histories, usually before a new game void Search::Worker::clear() { - mainHistory.fill(63); - lowPlyHistory.fill(108); - captureHistory.fill(-631); - pawnHistory.fill(-1210); - pawnCorrectionHistory.fill(0); - majorPieceCorrectionHistory.fill(0); + mainHistory.fill(65); + lowPlyHistory.fill(107); + captureHistory.fill(-655); + pawnHistory.fill(-1215); + pawnCorrectionHistory.fill(4); minorPieceCorrectionHistory.fill(0); nonPawnCorrectionHistory[WHITE].fill(0); nonPawnCorrectionHistory[BLACK].fill(0); @@ -571,10 +591,10 @@ void Search::Worker::clear() { for (StatsType c : {NoCaptures, Captures}) for (auto& to : continuationHistory[inCheck][c]) for (auto& h : to) - h.fill(-479); + h.fill(-493); for (size_t i = 1; i < reductions.size(); ++i) - reductions[i] = int(2143 / 100.0 * std::log(i)); + reductions[i] = int(2937 / 128.0 * std::log(i)); refreshTable.clear(networks[numaAccessToken]); } @@ -622,8 +642,8 @@ Value Search::Worker::search( Value bestValue, value, eval, maxValue, probCutBeta; bool givesCheck, improving, priorCapture, opponentWorsening; bool capture, ttCapture; - int priorReduction = ss->reduction; - ss->reduction = 0; + int priorReduction = (ss - 1)->reduction; + (ss - 1)->reduction = 0; Piece movedPiece; ValueList capturesSearched; @@ -693,20 +713,21 @@ 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 > 9)) + && (cutNode == (ttData.value >= beta) || depth > 5)) { - // If ttMove is quiet, update move sorting heuristics on TT hit (~2 Elo) + // If ttMove is quiet, update move sorting heuristics on TT hit if (ttData.move && ttData.value >= beta) { - // Bonus for a quiet ttMove that fails high (~2 Elo) + // Bonus for a quiet ttMove that fails high if (!ttCapture) - update_quiet_histories(pos, ss, *this, ttData.move, stat_bonus(depth) * 746 / 1024); + update_quiet_histories(pos, ss, *this, ttData.move, + std::min(117600 * depth - 71344, 1244992) / 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) + // Extra penalty for early quiet moves of the previous ply + if (prevSq != SQ_NONE && (ss - 1)->moveCount <= 3 && !priorCapture) update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, - -stat_malus(depth + 1) * 1042 / 1024); + -std::min(779788 * (depth + 1) - 271806, 2958308) + / 1024); } // Partial workaround for the graph history interaction problem @@ -782,8 +803,6 @@ Value Search::Worker::search( else if (excludedMove) { // Providing the hint that this node's accumulator will be used often - // brings significant Elo gain (~13 Elo). - Eval::NNUE::hint_common_parent_position(pos, networks[numaAccessToken], refreshTable); unadjustedStaticEval = eval = ss->staticEval; } else if (ss->ttHit) @@ -792,12 +811,10 @@ Value Search::Worker::search( unadjustedStaticEval = ttData.eval; if (!is_valid(unadjustedStaticEval)) unadjustedStaticEval = evaluate(pos); - else if (PvNode) - Eval::NNUE::hint_common_parent_position(pos, networks[numaAccessToken], refreshTable); ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, correctionValue); - // ttValue can be used as a better position evaluation (~7 Elo) + // ttValue can be used as a better position evaluation if (is_valid(ttData.value) && (ttData.bound & (ttData.value > eval ? BOUND_LOWER : BOUND_UPPER))) eval = ttData.value; @@ -813,14 +830,14 @@ Value Search::Worker::search( tt.generation()); } - // Use static evaluation difference to improve quiet move ordering (~9 Elo) + // Use static evaluation difference to improve quiet move ordering if (((ss - 1)->currentMove).is_ok() && !(ss - 1)->inCheck && !priorCapture) { - 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; + int bonus = std::clamp(-10 * int((ss - 1)->staticEval + ss->staticEval), -1906, 1450) + 638; + thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << bonus * 1136 / 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 * 1107 / 1024; + << bonus * 1195 / 1024; } // Set up the improving flag, which is true if current static evaluation is @@ -829,39 +846,37 @@ Value Search::Worker::search( // false otherwise. The improving flag is used in various pruning heuristics. improving = ss->staticEval > (ss - 2)->staticEval; - opponentWorsening = ss->staticEval + (ss - 1)->staticEval > 2; + opponentWorsening = ss->staticEval > -(ss - 1)->staticEval; - if (priorReduction >= 3 && ss->staticEval + (ss - 1)->staticEval < 0) - { + if (priorReduction >= 3 && !opponentWorsening) depth++; - } + if (priorReduction >= 1 && depth >= 2 && ss->staticEval + (ss - 1)->staticEval > 200) + depth--; - // Step 7. Razoring (~1 Elo) + // Step 7. Razoring // If eval is really low, skip search entirely and return the qsearch value. // For PvNodes, we must have a guard against mates being returned. - if (!PvNode && eval < alpha - 462 - 297 * depth * depth) + if (!PvNode && eval < alpha - 446 - 303 * depth * depth) return qsearch(pos, ss, alpha, beta); - // Step 8. Futility pruning: child node (~40 Elo) + // Step 8. Futility pruning: child node // 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 / 310 + 40 - std::abs(correctionValue) / 131072 + - (ss - 1)->statScore / 326 + 37 - std::abs(correctionValue) / 132821 >= beta && eval >= beta && (!ttData.move || ttCapture) && !is_loss(beta) && !is_win(eval)) return beta + (eval - beta) / 3; - improving |= ss->staticEval >= beta + 97; - - // Step 9. Null move search with verification search (~35 Elo) + // Step 9. Null move search with verification search if (cutNode && (ss - 1)->currentMove != Move::null() && eval >= beta - && ss->staticEval >= beta - 20 * depth + 440 && !excludedMove && pos.non_pawn_material(us) - && ss->ply >= thisThread->nmpMinPly && !is_loss(beta)) + && ss->staticEval >= beta - 21 * depth + 455 - 60 * improving && !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) / 215, 7) + depth / 3 + 5; + Depth R = std::min(int(eval - beta) / 237, 6) + depth / 3 + 5; ss->currentMove = Move::null(); ss->continuationHistory = &thisThread->continuationHistory[0][0][NO_PIECE][0]; @@ -894,29 +909,25 @@ Value Search::Worker::search( } } - // Step 10. Internal iterative reductions (~9 Elo) + improving |= ss->staticEval >= beta + 97; + + // Step 10. Internal iterative reductions // 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; + // (* Scaler) Especially if they make IIR more aggressive. + if (((PvNode || cutNode) && depth >= 7 - 3 * PvNode) && !ttData.move) + depth--; - // Use qsearch if depth <= 0 - if (depth <= 0) - return qsearch(pos, ss, alpha, beta); - - // Step 11. ProbCut (~10 Elo) + // Step 11. ProbCut // 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 + 174 - 56 * improving; + probCutBeta = beta + 187 - 55 * 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 // depth is set to depth - 3 because probCut search has depth set to depth - 4 // but we also do a move before it. So effective depth is equal to depth - 3. - && !(ttData.depth >= depth - 3 && is_valid(ttData.value) && ttData.value < probCutBeta)) + && !(is_valid(ttData.value) && ttData.value < probCutBeta)) { assert(probCutBeta < VALUE_INFINITE && probCutBeta > beta); @@ -941,6 +952,7 @@ Value Search::Worker::search( thisThread->nodes.fetch_add(1, std::memory_order_relaxed); ss->currentMove = move; + ss->isTTMove = (move == ttData.move); ss->continuationHistory = &this->continuationHistory[ss->inCheck][true][movedPiece][move.to_sq()]; ss->continuationCorrectionHistory = @@ -971,8 +983,8 @@ Value Search::Worker::search( moves_loop: // When in check, search starts here - // Step 12. A small Probcut idea (~4 Elo) - probCutBeta = beta + 412; + // Step 12. A small Probcut idea + probCutBeta = beta + 413; 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; @@ -1032,15 +1044,19 @@ moves_loop: // When in check, search starts here Depth r = reduction(improving, depth, moveCount, delta); - // Decrease reduction if position is or has been on the PV (~7 Elo) - if (ss->ttPv) - r -= 1037 + (ttData.value > alpha) * 965 + (ttData.depth >= depth) * 960; + r -= 32 * moveCount; - // Step 14. Pruning at shallow depth (~120 Elo). + // Increase reduction for ttPv nodes (*Scaler) + // Smaller or even negative value is better for short time controls + // Bigger value is better for long time controls + if (ss->ttPv) + r += 1031; + + // Step 14. Pruning at shallow depth. // Depth conditions are important for mate finding. if (!rootNode && pos.non_pawn_material(us) && !is_loss(bestValue)) { - // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold (~8 Elo) + // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold if (moveCount >= futility_move_count(improving, depth)) mp.skip_quiet_moves(); @@ -1053,18 +1069,18 @@ moves_loop: // When in check, search starts here int captHist = thisThread->captureHistory[movedPiece][move.to_sq()][type_of(capturedPiece)]; - // Futility pruning for captures (~2 Elo) + // Futility pruning for captures if (!givesCheck && lmrDepth < 7 && !ss->inCheck) { - Value futilityValue = ss->staticEval + 271 + 243 * lmrDepth - + PieceValue[capturedPiece] + captHist / 7; + Value futilityValue = ss->staticEval + 242 + 238 * lmrDepth + + PieceValue[capturedPiece] + 95 * captHist / 700; if (futilityValue <= alpha) continue; } - // SEE based pruning for captures and checks (~11 Elo) - int seeHist = std::clamp(captHist / 37, -152 * depth, 141 * depth); - if (!pos.see_ge(move, -156 * depth - seeHist)) + // SEE based pruning for captures and checks + int seeHist = std::clamp(captHist / 36, -153 * depth, 134 * depth); + if (!pos.see_ge(move, -157 * depth - seeHist)) continue; } else @@ -1074,18 +1090,22 @@ moves_loop: // When in check, search starts here + (*contHist[1])[movedPiece][move.to_sq()] + thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][move.to_sq()]; - // Continuation history based pruning (~2 Elo) - if (history < -3901 * depth) + // Continuation history based pruning + if (history < -4107 * depth) continue; - history += 2 * thisThread->mainHistory[us][move.from_to()]; + history += 68 * thisThread->mainHistory[us][move.from_to()] / 32; - lmrDepth += history / 3459; + lmrDepth += history / 3576; - Value futilityValue = - ss->staticEval + (bestValue < ss->staticEval - 47 ? 137 : 47) + 142 * lmrDepth; + Value futilityValue = ss->staticEval + (bestMove ? 49 : 143) + 116 * lmrDepth; - // Futility pruning: parent node (~13 Elo) + if (bestValue < ss->staticEval - 150 && lmrDepth < 7) + futilityValue += 108; + + // Futility pruning: parent node + // (*Scaler): Generally, more frequent futility pruning + // scales well with respect to time and threads if (!ss->inCheck && lmrDepth < 12 && futilityValue <= alpha) { if (bestValue <= futilityValue && !is_decisive(bestValue) @@ -1096,35 +1116,32 @@ moves_loop: // When in check, search starts here lmrDepth = std::max(lmrDepth, 0); - // Prune moves with negative SEE (~4 Elo) - if (!pos.see_ge(move, -25 * lmrDepth * lmrDepth)) + // Prune moves with negative SEE + if (!pos.see_ge(move, -26 * lmrDepth * lmrDepth)) continue; } } - // Step 15. Extensions (~100 Elo) + // Step 15. Extensions // We take care to not overdo to avoid search getting stuck. if (ss->ply < thisThread->rootDepth * 2) { - // Singular extension search (~76 Elo, ~170 nElo). If all moves but one + // Singular extension search. If all moves but one // fail low on a search of (alpha-s, beta-s), and just one fails high on // (alpha, beta), then that move is singular and should be extended. To // verify this we do a reduced search on the position excluding the ttMove // and if the result is lower than ttValue minus a margin, then we will // extend the ttMove. Recursive singular search is avoided. - // Note: the depth margin and singularBeta margin are known for having - // non-linear scaling. Their values are optimized to time controls of - // 180+1.8 and longer so changing them requires tests at these types of - // time controls. Generally, higher singularBeta (i.e closer to ttValue) + // (* Scaler) Generally, higher singularBeta (i.e closer to ttValue) // and lower extension margins scale well. if (!rootNode && move == ttData.move && !excludedMove - && depth >= 5 - (thisThread->completedDepth > 33) + ss->ttPv + && depth >= 5 - (thisThread->completedDepth > 32) + ss->ttPv && is_valid(ttData.value) && !is_decisive(ttData.value) && (ttData.bound & BOUND_LOWER) && ttData.depth >= depth - 3) { - Value singularBeta = ttData.value - (52 + 74 * (ss->ttPv && !PvNode)) * depth / 64; + Value singularBeta = ttData.value - (55 + 81 * (ss->ttPv && !PvNode)) * depth / 58; Depth singularDepth = newDepth / 2; ss->excludedMove = move; @@ -1134,18 +1151,16 @@ moves_loop: // When in check, search starts here if (value < singularBeta) { - int corrValAdj = std::abs(correctionValue) / 262144; - int doubleMargin = 249 * PvNode - 194 * !ttCapture - corrValAdj; + int corrValAdj1 = std::abs(correctionValue) / 265083; + int corrValAdj2 = std::abs(correctionValue) / 253680; + int doubleMargin = 267 * PvNode - 181 * !ttCapture - corrValAdj1; int tripleMargin = - 94 + 287 * PvNode - 249 * !ttCapture + 99 * ss->ttPv - corrValAdj; - int quadMargin = - 394 + 287 * PvNode - 249 * !ttCapture + 99 * ss->ttPv - corrValAdj; + 96 + 282 * PvNode - 250 * !ttCapture + 103 * ss->ttPv - corrValAdj2; extension = 1 + (value < singularBeta - doubleMargin) - + (value < singularBeta - tripleMargin) - + (value < singularBeta - quadMargin); + + (value < singularBeta - tripleMargin); - depth += ((!PvNode) && (depth < 15)); + depth++; } // Multi-cut pruning @@ -1164,22 +1179,15 @@ moves_loop: // When in check, search starts here // if the ttMove is singular or can do a multi-cut, so we reduce the // ttMove in favor of other moves based on some conditions: - // If the ttMove is assumed to fail high over current beta (~7 Elo) + // If the ttMove is assumed to fail high over current beta else if (ttData.value >= beta) extension = -3; // If we are on a cutNode but the ttMove is not assumed to fail high - // over current beta (~1 Elo) + // over current beta else if (cutNode) extension = -2; } - - // Extension for capturing the previous moved piece (~1 Elo at LTC) - else if (PvNode && move.to_sq() == prevSq - && thisThread->captureHistory[movedPiece][move.to_sq()] - [type_of(pos.piece_on(move.to_sq()))] - > 4126) - extension = 1; } // Step 16. Make the move @@ -1191,57 +1199,54 @@ moves_loop: // When in check, search starts here // Update the current move (this must be done after singular extension search) ss->currentMove = move; + ss->isTTMove = (move == ttData.move); ss->continuationHistory = &thisThread->continuationHistory[ss->inCheck][capture][movedPiece][move.to_sq()]; ss->continuationCorrectionHistory = &thisThread->continuationCorrectionHistory[movedPiece][move.to_sq()]; uint64_t nodeCount = rootNode ? uint64_t(nodes) : 0; - // These reduction adjustments have proven non-linear scaling. - // They are optimized to time controls of 180 + 1.8 and longer, - // so changing them or adding conditions that are similar requires - // tests at these types of time controls. - - // Decrease reduction for PvNodes (~0 Elo on STC, ~2 Elo on LTC) - if (PvNode) - r -= 1018; + // Decrease reduction for PvNodes (*Scaler) + if (ss->ttPv) + r -= 2230 + PvNode * 1013 + (ttData.value > alpha) * 925 + + (ttData.depth >= depth) * (971 + cutNode * 1159); // These reduction adjustments have no proven non-linear scaling - r += 307; + r += 316 - moveCount * 32; - r -= std::abs(correctionValue) / 34112; + r -= std::abs(correctionValue) / 31568; - // Increase reduction for cut nodes (~4 Elo) + // Increase reduction for cut nodes if (cutNode) - r += 2355 - (ttData.depth >= depth && ss->ttPv) * 1141; + r += 2608 + 1024 * !ttData.move; - // Increase reduction if ttMove is a capture but the current move is not a capture (~3 Elo) + // Increase reduction if ttMove is a capture but the current move is not a capture if (ttCapture && !capture) - r += 1087 + (depth < 8) * 990; + r += 1123 + (depth < 8) * 982; - // Increase reduction if next ply has a lot of fail high (~5 Elo) + // Increase reduction if next ply has a lot of fail high if ((ss + 1)->cutoffCnt > 3) - r += 940 + allNode * 887; + r += 981 + allNode * 833; - // For first picked move (ttMove) reduce reduction (~3 Elo) + // For first picked move (ttMove) reduce reduction else if (move == ttData.move) - r -= 1960; + r -= 1982; if (capture) ss->statScore = - 7 * int(PieceValue[pos.captured_piece()]) + 688 * int(PieceValue[pos.captured_piece()]) / 100 + thisThread->captureHistory[movedPiece][move.to_sq()][type_of(pos.captured_piece())] - - 4666; + - 4653; else ss->statScore = 2 * thisThread->mainHistory[us][move.from_to()] + (*contHist[0])[movedPiece][move.to_sq()] - + (*contHist[1])[movedPiece][move.to_sq()] - 3874; + + (*contHist[1])[movedPiece][move.to_sq()] - 3591; - // Decrease/increase reduction for moves with a good/bad history (~8 Elo) - r -= ss->statScore * 1451 / 16384; + // Decrease/increase reduction for moves with a good/bad history + r -= ss->statScore * 1407 / 16384; - // Step 17. Late moves reduction / extension (LMR, ~117 Elo) + // Step 17. Late moves reduction / extension (LMR) if (depth >= 2 && moveCount > 1) { // In general we want to cap the LMR depth search at newDepth, but when @@ -1254,10 +1259,10 @@ moves_loop: // When in check, search starts here Depth d = std::max( 1, std::min(newDepth - r / 1024, newDepth + !allNode + (PvNode && !bestMove))); - (ss + 1)->reduction = newDepth - d; + ss->reduction = newDepth - d; - value = -search(pos, ss + 1, -(alpha + 1), -alpha, d, true); - (ss + 1)->reduction = 0; + value = -search(pos, ss + 1, -(alpha + 1), -alpha, d, true); + ss->reduction = 0; // Do a full-depth search when reduced LMR search fails high @@ -1265,16 +1270,16 @@ moves_loop: // When in check, search starts here { // 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 + 40 + 2 * newDepth); // (~1 Elo) - const bool doShallowerSearch = value < bestValue + 10; // (~2 Elo) + const bool doDeeperSearch = value > (bestValue + 41 + 2 * newDepth); + const bool doShallowerSearch = value < bestValue + 9; newDepth += doDeeperSearch - doShallowerSearch; if (newDepth > d) value = -search(pos, ss + 1, -(alpha + 1), -alpha, newDepth, !cutNode); - // Post LMR continuation history updates (~1 Elo) - int bonus = (value >= beta) * 2048; + // Post LMR continuation history updates + int bonus = (value >= beta) * 2010; update_continuation_histories(ss, movedPiece, move.to_sq(), bonus); } } @@ -1282,13 +1287,13 @@ moves_loop: // When in check, search starts here // Step 18. Full-depth search when LMR is skipped else if (!PvNode || moveCount > 1) { - // Increase reduction if ttMove is not present (~6 Elo) + // Increase reduction if ttMove is not present if (!ttData.move) - r += 2111; + r += 1111; - // 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 > 3444), !cutNode); + // Note that if expected reduction is high, we reduce search depth here + value = -search(pos, ss + 1, -(alpha + 1), -alpha, + newDepth - (r > 3554) - (r > 5373 && newDepth > 2), !cutNode); } // For PV nodes only, do a full PV search on the first move or after a fail high, @@ -1299,7 +1304,7 @@ moves_loop: // When in check, search starts here (ss + 1)->pv[0] = Move::none(); // Extend move from transposition table if we are about to dive into qsearch. - if (move == ttData.move && ss->ply <= thisThread->rootDepth * 2) + if (move == ttData.move && thisThread->rootDepth > 8) newDepth = std::max(newDepth, 1); value = -search(pos, ss + 1, -beta, -alpha, newDepth, false); @@ -1387,14 +1392,15 @@ moves_loop: // When in check, search starts here if (value >= beta) { - ss->cutoffCnt += !ttData.move + (extension < 2); + // (* Scaler) Especially if they make cutoffCnt increment more often. + ss->cutoffCnt += (extension < 2) || PvNode; assert(value >= beta); // Fail high break; } else { - // Reduce other moves if we have found at least one score improvement (~2 Elo) - if (depth > 2 && depth < 14 && !is_decisive(value)) + // Reduce other moves if we have found at least one score improvement + if (depth > 2 && depth < 15 && !is_decisive(value)) depth -= 2; assert(depth > 0); @@ -1433,30 +1439,30 @@ moves_loop: // When in check, search starts here // we update the stats of searched moves. else if (bestMove) update_all_stats(pos, ss, *this, bestMove, prevSq, quietsSearched, capturesSearched, depth, - bestMove == ttData.move); + bestMove == ttData.move, moveCount); // Bonus for prior countermove that caused the fail low else if (!priorCapture && prevSq != SQ_NONE) { - 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 / 106, 318); + int bonusScale = (118 * (depth > 5) + 36 * !allNode + 161 * ((ss - 1)->moveCount > 8) + + 133 * (!ss->inCheck && bestValue <= ss->staticEval - 107) + + 120 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 84) + + 81 * ((ss - 1)->isTTMove) + 100 * (ss->cutoffCnt <= 3) + + std::min(-(ss - 1)->statScore / 108, 320)); bonusScale = std::max(bonusScale, 0); - const int scaledBonus = stat_bonus(depth) * bonusScale / 32; + const int scaledBonus = std::min(160 * depth - 106, 1523) * bonusScale; update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, - scaledBonus * 436 / 1024); + scaledBonus * 416 / 32768); - thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << scaledBonus * 207 / 1024; + thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] + << scaledBonus * 219 / 32768; 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 * 1195 / 1024; + << scaledBonus * 1103 / 32768; } else if (priorCapture && prevSq != SQ_NONE) @@ -1465,16 +1471,16 @@ moves_loop: // When in check, search starts here Piece capturedPiece = pos.captured_piece(); assert(capturedPiece != NO_PIECE); thisThread->captureHistory[pos.piece_on(prevSq)][prevSq][type_of(capturedPiece)] - << stat_bonus(depth) * 2; + << std::min(330 * depth - 198, 3320); } if (PvNode) bestValue = std::min(bestValue, maxValue); // If no good move is found and the previous position was ttPv, then the previous - // opponent move is probably good and the new position is added to the search tree. (~7 Elo) + // opponent move is probably good and the new position is added to the search tree. if (bestValue <= alpha) - ss->ttPv = ss->ttPv || ((ss - 1)->ttPv && depth > 3); + ss->ttPv = ss->ttPv || (ss - 1)->ttPv; // Write gathered information in transposition table. Note that the // static evaluation is saved as it was before correction history. @@ -1491,22 +1497,9 @@ moves_loop: // When in check, search starts here && ((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; - 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 * 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)] - << bonus * nonPawnWeight / 128; - - if (m.is_ok()) - (*(ss - 2)->continuationCorrectionHistory)[pos.piece_on(m.to_sq())][m.to_sq()] << bonus; + update_correction_history(pos, ss, *thisThread, bonus); } assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); @@ -1518,7 +1511,7 @@ moves_loop: // When in check, search starts here // Quiescence search function, which is called by the main search function with // depth zero, or recursively with further decreasing depth. With depth <= 0, we // "should" be using static eval only, but tactical moves may confuse the static eval. -// To fight this horizon effect, we implement this qsearch of tactical moves (~155 Elo). +// To fight this horizon effect, we implement this qsearch of tactical moves. // See https://www.chessprogramming.org/Horizon_Effect // and https://www.chessprogramming.org/Quiescence_Search template @@ -1530,7 +1523,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) assert(alpha >= -VALUE_INFINITE && alpha < beta && beta <= VALUE_INFINITE); assert(PvNode || (alpha == beta - 1)); - // Check if we have an upcoming move that draws by repetition (~1 Elo) + // Check if we have an upcoming move that draws by repetition if (alpha < VALUE_DRAW && pos.upcoming_repetition(ss->ply)) { alpha = value_draw(this->nodes); @@ -1547,7 +1540,6 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) Value bestValue, value, futilityBase; bool pvHit, givesCheck, capture; int moveCount; - Color us = pos.side_to_move(); // Step 1. Initialize node if (PvNode) @@ -1602,7 +1594,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) ss->staticEval = bestValue = to_corrected_static_eval(unadjustedStaticEval, correctionValue); - // ttValue can be used as a better position evaluation (~13 Elo) + // ttValue can be used as a better position evaluation if (is_valid(ttData.value) && !is_decisive(ttData.value) && (ttData.bound & (ttData.value > bestValue ? BOUND_LOWER : BOUND_UPPER))) bestValue = ttData.value; @@ -1633,7 +1625,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) if (bestValue > alpha) alpha = bestValue; - futilityBase = ss->staticEval + 301; + futilityBase = ss->staticEval + 325; } const PieceToHistory* contHist[] = {(ss - 1)->continuationHistory, @@ -1662,9 +1654,9 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) moveCount++; // Step 6. Pruning - if (!is_loss(bestValue) && pos.non_pawn_material(us)) + if (!is_loss(bestValue)) { - // Futility pruning and moveCount pruning (~10 Elo) + // Futility pruning and moveCount pruning if (!givesCheck && move.to_sq() != prevSq && !is_loss(futilityBase) && move.type_of() != PROMOTION) { @@ -1674,7 +1666,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) Value futilityValue = futilityBase + PieceValue[pos.piece_on(move.to_sq())]; // If static eval + value of piece we are going to capture is - // much lower than alpha, we can prune this move. (~2 Elo) + // much lower than alpha, we can prune this move. if (futilityValue <= alpha) { bestValue = std::max(bestValue, futilityValue); @@ -1682,7 +1674,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) } // If static exchange evaluation is low enough - // we can prune this move. (~2 Elo) + // we can prune this move. if (!pos.see_ge(move, alpha - futilityBase)) { bestValue = std::min(alpha, futilityBase); @@ -1690,17 +1682,17 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) } } - // Continuation history based pruning (~3 Elo) + // Continuation history based pruning if (!capture && (*contHist[0])[pos.moved_piece(move)][move.to_sq()] + (*contHist[1])[pos.moved_piece(move)][move.to_sq()] + thisThread->pawnHistory[pawn_structure_index(pos)][pos.moved_piece(move)] [move.to_sq()] - <= 5228) + <= 5389) continue; - // Do not search moves with bad enough SEE values (~5 Elo) - if (!pos.see_ge(move, -80)) + // Do not search moves with bad enough SEE values + if (!pos.see_ge(move, -75)) continue; } @@ -1767,7 +1759,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 * 768 / rootDelta + !i * reductionScale * 108 / 300 + 1168; + return reductionScale - delta * 735 / rootDelta + !i * reductionScale * 191 / 512 + 1132; } // elapsed() returns the time elapsed since the search started. If the @@ -1856,41 +1848,42 @@ void update_all_stats(const Position& pos, ValueList& quietsSearched, ValueList& capturesSearched, Depth depth, - bool isTTMove) { + bool isTTMove, + int moveCount) { CapturePieceToHistory& captureHistory = workerThread.captureHistory; Piece moved_piece = pos.moved_piece(bestMove); PieceType captured; - int bonus = stat_bonus(depth) + 300 * isTTMove; - int malus = stat_malus(depth); + int bonus = std::min(162 * depth - 92, 1587) + 298 * isTTMove; + int malus = std::min(694 * depth - 230, 2503) - 32 * (moveCount - 1); if (!pos.capture_stage(bestMove)) { - update_quiet_histories(pos, ss, workerThread, bestMove, bonus * 1216 / 1024); + update_quiet_histories(pos, ss, workerThread, bestMove, bonus * 1202 / 1024); // Decrease stats for all non-best quiet moves for (Move move : quietsSearched) - update_quiet_histories(pos, ss, workerThread, move, -malus * 1062 / 1024); + update_quiet_histories(pos, ss, workerThread, move, -malus * 1152 / 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 * 1272 / 1024; + captureHistory[moved_piece][bestMove.to_sq()][captured] << bonus * 1236 / 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 * 966 / 1024); + update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -malus * 976 / 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 * 1205 / 1024; + captureHistory[moved_piece][move.to_sq()][captured] << -malus * 1224 / 1024; } } @@ -1899,7 +1892,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, 1025}, {2, 621}, {3, 325}, {4, 512}, {5, 122}, {6, 534}}}; + {{1, 1029}, {2, 656}, {3, 326}, {4, 536}, {5, 120}, {6, 537}}}; for (const auto [i, weight] : conthist_bonuses) { @@ -1920,12 +1913,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 * 879 / 1024; + workerThread.lowPlyHistory[ss->ply][move.from_to()] << bonus * 844 / 1024; - update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus * 888 / 1024); + update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus * 964 / 1024); int pIndex = pawn_structure_index(pos); - workerThread.pawnHistory[pIndex][pos.moved_piece(move)][move.to_sq()] << bonus * 634 / 1024; + workerThread.pawnHistory[pIndex][pos.moved_piece(move)][move.to_sq()] << bonus * 615 / 1024; } } @@ -2013,6 +2006,7 @@ void syzygy_extend_pv(const OptionsMap& options, auto t_start = std::chrono::steady_clock::now(); int moveOverhead = int(options["Move Overhead"]); + bool rule50 = bool(options["Syzygy50MoveRule"]); // Do not use more than moveOverhead / 2 time, if time management is active auto time_abort = [&t_start, &moveOverhead, &limits]() -> bool { @@ -2050,7 +2044,7 @@ void syzygy_extend_pv(const OptionsMap& options, pos.do_move(pvMove, st); // Do not allow for repetitions or drawing moves along the PV in TB regime - if (config.rootInTB && pos.is_draw(ply)) + if (config.rootInTB && ((rule50 && pos.is_draw(ply)) || pos.is_repetition(ply))) { pos.undo_move(pvMove); ply--; @@ -2069,7 +2063,7 @@ void syzygy_extend_pv(const OptionsMap& options, // Step 2, now extend the PV to mate, as if the user explored syzygy-tables.info // using top ranked moves (minimal DTZ), which gives optimal mates only for simple // endgames e.g. KRvK. - while (!pos.is_draw(0)) + while (!(rule50 && pos.is_draw(0))) { if (time_abort()) break; @@ -2112,7 +2106,7 @@ void syzygy_extend_pv(const OptionsMap& options, pos.do_move(pvMove, st); } - // Finding a draw in this function is an exceptional case, that cannot happen + // Finding a draw in this function is an exceptional case, that cannot happen when rule50 is false or // during engine game play, since we have a winning score, and play correctly // with TB support. However, it can be that a position is draw due to the 50 move // rule if it has been been reached on the board with a non-optimal 50 move counter diff --git a/src/search.h b/src/search.h index 8ccbc03b3..327f8ed48 100644 --- a/src/search.h +++ b/src/search.h @@ -77,6 +77,7 @@ struct Stack { bool ttHit; int cutoffCnt; int reduction; + bool isTTMove; }; @@ -299,7 +300,6 @@ class Worker { PawnHistory pawnHistory; CorrectionHistory pawnCorrectionHistory; - CorrectionHistory majorPieceCorrectionHistory; CorrectionHistory minorPieceCorrectionHistory; CorrectionHistory nonPawnCorrectionHistory[COLOR_NB]; CorrectionHistory continuationCorrectionHistory; diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index f1e484a44..2cd53db65 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -1622,7 +1622,7 @@ bool Tablebases::root_probe(Position& pos, WDLScore wdl = -probe_wdl(pos, &result); dtz = dtz_before_zeroing(wdl); } - else if (pos.is_draw(1)) + else if ((rule50 && pos.is_draw(1)) || pos.is_repetition(1)) { // In case a root move leads to a draw by repetition or 50-move rule, // we set dtz to zero. Note: since we are only 1 ply from the root, diff --git a/src/timeman.cpp b/src/timeman.cpp index d073a84a9..29ebffcaa 100644 --- a/src/timeman.cpp +++ b/src/timeman.cpp @@ -87,7 +87,7 @@ void TimeManagement::init(Search::LimitsType& limits, const TimePoint scaledTime = limits.time[us] / scaleFactor; const TimePoint scaledInc = limits.inc[us] / scaleFactor; - // Maximum move horizon of 50 moves + // Maximum move horizon int centiMTG = limits.movestogo ? std::min(limits.movestogo * 100, 5000) : 5051; // If less than one second, gradually reduce mtg diff --git a/src/tt.h b/src/tt.h index b65f9e49d..a92de2856 100644 --- a/src/tt.h +++ b/src/tt.h @@ -57,6 +57,8 @@ struct TTData { bool is_pv; TTData() = delete; + + // clang-format off TTData(Move m, Value v, Value ev, Depth d, Bound b, bool pv) : move(m), value(v), @@ -64,6 +66,7 @@ struct TTData { depth(d), bound(b), is_pv(pv) {}; + // clang-format on }; diff --git a/src/tune.cpp b/src/tune.cpp index aff96c8cb..f53a0eb52 100644 --- a/src/tune.cpp +++ b/src/tune.cpp @@ -55,7 +55,7 @@ void Tune::make_option(OptionsMap* opts, const string& n, int v, const SetRange& if (TuneResults.count(n)) v = TuneResults[n]; - (*opts)[n] << Option(v, r(v).first, r(v).second, on_tune); + opts->add(n, Option(v, r(v).first, r(v).second, on_tune)); LastOption = &((*opts)[n]); // Print formatted parameters, ready to be copy-pasted in Fishtest diff --git a/src/ucioption.cpp b/src/ucioption.cpp index b2f141eba..8933f30d4 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -58,17 +59,31 @@ void OptionsMap::setoption(std::istringstream& is) { sync_cout << "No such option: " << name << sync_endl; } -Option OptionsMap::operator[](const std::string& name) const { +const Option& OptionsMap::operator[](const std::string& name) const { auto it = options_map.find(name); - return it != options_map.end() ? it->second : Option(this); + assert(it != options_map.end()); + return it->second; } -Option& OptionsMap::operator[](const std::string& name) { +// Inits options and assigns idx in the correct printing order +void OptionsMap::add(const std::string& name, const Option& option) { if (!options_map.count(name)) - options_map[name] = Option(this); - return options_map[name]; + { + static size_t insert_order = 0; + + options_map[name] = option; + + options_map[name].parent = this; + options_map[name].idx = insert_order++; + } + else + { + std::cerr << "Option \"" << name << "\" was already added!" << std::endl; + std::exit(EXIT_FAILURE); + } } + std::size_t OptionsMap::count(const std::string& name) const { return options_map.count(name); } Option::Option(const OptionsMap* map) : @@ -131,19 +146,6 @@ bool Option::operator==(const char* s) const { bool Option::operator!=(const char* s) const { return !(*this == s); } -// Inits options and assigns idx in the correct printing order - -void Option::operator<<(const Option& o) { - - static size_t insert_order = 0; - - auto p = this->parent; - *this = o; - - this->parent = p; - idx = insert_order++; -} - // Updates currentValue and triggers on_change() action. It's up to // the GUI to check for option's limits, but we could receive the new value // from the user by console window, so let's check the bounds anyway. @@ -162,7 +164,7 @@ Option& Option::operator=(const std::string& v) { std::string token; std::istringstream ss(defaultValue); while (ss >> token) - comboMap[token] << Option(); + comboMap.add(token, Option()); if (!comboMap.count(v) || v == "var") return *this; } diff --git a/src/ucioption.h b/src/ucioption.h index c9f6787d3..3d7386c30 100644 --- a/src/ucioption.h +++ b/src/ucioption.h @@ -54,12 +54,13 @@ class Option { friend std::ostream& operator<<(std::ostream&, const OptionsMap&); + int operator<<(const Option&) = delete; + private: friend class OptionsMap; friend class Engine; friend class Tune; - void operator<<(const Option&); std::string defaultValue, currentValue, type; int min, max; @@ -82,8 +83,9 @@ class OptionsMap { void setoption(std::istringstream&); - Option operator[](const std::string&) const; - Option& operator[](const std::string&); + const Option& operator[](const std::string&) const; + + void add(const std::string&, const Option& option); std::size_t count(const std::string&) const; diff --git a/tests/testing.py b/tests/testing.py index bc1f6b15b..3a4b537e9 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -97,14 +97,17 @@ class Syzygy: tarball_path = os.path.join(tmpdirname, f"{file}.tar.gz") response = requests.get(url, stream=True) - with open(tarball_path, 'wb') as f: + with open(tarball_path, "wb") as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) with tarfile.open(tarball_path, "r:gz") as tar: tar.extractall(tmpdirname) - shutil.move(os.path.join(tmpdirname, file), os.path.join(PATH, "syzygy")) + shutil.move( + os.path.join(tmpdirname, file), os.path.join(PATH, "syzygy") + ) + class OrderedClassMembers(type): @classmethod @@ -307,7 +310,10 @@ class Stockfish: text=True, ) - self.process.stdout + if self.process.returncode != 0: + print(self.process.stdout) + print(self.process.stderr) + print(f"Process failed with return code {self.process.returncode}") return