Skip to content

Commit

Permalink
nn-eval-correction-history (#185)
Browse files Browse the repository at this point in the history
Elo   | 3.08 +- 2.85 (95%)
SPRT  | 8.0+0.08s Threads=1 Hash=32MB
LLR   | 2.95 (-2.94, 2.94) [0.00, 3.00]
Games | N: 28020 W: 7042 L: 6794 D: 14184
Penta | [161, 3196, 7111, 3318, 224]
http://chess.grantnet.us/test/34932/
Elo   | 3.93 +- 3.47 (95%)
SPRT  | 40.0+0.40s Threads=1 Hash=64MB
LLR   | 2.95 (-2.94, 2.94) [0.00, 3.00]
Games | N: 18664 W: 4634 L: 4423 D: 9607
Penta | [30, 2079, 4928, 2240, 55]
http://chess.grantnet.us/test/34934/
bench: 4473743
  • Loading branch information
connormcmonigle authored Jan 4, 2024
1 parent f9b341e commit f152efd
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 66 deletions.
2 changes: 2 additions & 0 deletions include/nnue/aligned_slice.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ template <typename T, std::size_t dim>
struct aligned_slice {
T* data;

[[nodiscard]] const T* ptr() const noexcept { return data; }

template <std::size_t out_dim, std::size_t offset = 0>
[[nodiscard]] aligned_slice<T, out_dim> slice() noexcept {
static_assert(offset + out_dim <= dim);
Expand Down
1 change: 1 addition & 0 deletions include/nnue/aligned_vector.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace nnue {

template <typename T, std::size_t dim>
struct aligned_vector {
static constexpr std::size_t dimension = dim;
alignas(simd::alignment) T data[dim];

template <typename F>
Expand Down
27 changes: 17 additions & 10 deletions include/nnue/eval.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
#include <nnue/weights.h>
#include <nnue/weights_streamer.h>
#include <search/search_constants.h>
#include <zobrist/zobrist_hasher.h>

#include <cmath>
#include <cstdint>
#include <iostream>
#include <string>
#include <utility>

namespace nnue {

Expand All @@ -56,35 +58,40 @@ struct eval : public chess::sided<eval, feature_transformer<weights::quantized_p
feature_transformer<quantized_parameter_type, feature::half_ka::numel, weights::base_dim> white;
feature_transformer<quantized_parameter_type, feature::half_ka::numel, weights::base_dim> black;

[[nodiscard]] inline parameter_type propagate(const bool pov) const noexcept {
const auto x1 = (pov ? weights_->white_fc0 : weights_->black_fc0)
.forward(base_)
.dequantized<parameter_type>(weights::dequantization_scale);

[[nodiscard]] inline std::pair<zobrist::quarter_hash_type, parameter_type> propagate(const bool pov) const noexcept {
const auto x1 = (pov ? weights_->white_fc0 : weights_->black_fc0).forward(base_).dequantized<parameter_type>(weights::dequantization_scale);
const auto x2 = concat(x1, weights_->fc1.forward(x1));
const auto x3 = concat(x2, weights_->fc2.forward(x2));
return weights_->fc3.forward(x3).item();

constexpr std::size_t dimension = decltype(x3)::dimension;
const zobrist::quarter_hash_type quarter_hash = zobrist::zobrist_hasher<zobrist::quarter_hash_type, dimension>.compute_hash(
[&x3](const std::size_t& i) { return x3.data[i] > parameter_type{}; });

return std::pair(quarter_hash, weights_->fc3.forward(x3).item());
}

[[nodiscard]] inline search::score_type evaluate(const bool pov, const parameter_type& phase) const noexcept {
[[nodiscard]] inline std::pair<zobrist::hash_type, search::score_type> evaluate(const bool pov, const parameter_type& phase) const noexcept {
constexpr auto one = static_cast<parameter_type>(1.0);
constexpr auto mg = static_cast<parameter_type>(0.7);
constexpr auto eg = static_cast<parameter_type>(0.55);

const parameter_type prediction = propagate(pov);
const auto [hash, prediction] = propagate(pov);
const parameter_type eval = phase * mg * prediction + (one - phase) * eg * prediction;

const parameter_type value =
search::logit_scale<parameter_type> * std::clamp(eval, search::min_logit<parameter_type>, search::max_logit<parameter_type>);
return static_cast<search::score_type>(value);

const auto score = static_cast<search::score_type>(value);
return std::pair(hash, score);
}

[[nodiscard]] eval next_child() const noexcept {
const std::size_t next_scratchpad_idx = scratchpad_idx_ + 1;
return eval(weights_, scratchpad_, scratchpad_idx_, next_scratchpad_idx);
}

eval(const quantized_weights* src, scratchpad_type* scratchpad, const std::size_t& parent_scratchpad_idx, const std::size_t& scratchpad_idx) noexcept
eval(
const quantized_weights* src, scratchpad_type* scratchpad, const std::size_t& parent_scratchpad_idx, const std::size_t& scratchpad_idx) noexcept
: weights_{src},
scratchpad_{scratchpad},
scratchpad_idx_{scratchpad_idx},
Expand Down
27 changes: 20 additions & 7 deletions include/search/eval_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,23 @@
namespace search {

struct eval_cache_entry {
zobrist::half_hash_type hash{};
score_type eval{};
using persisted_eval_type = std::int16_t;

zobrist::half_hash_type hash_{};
zobrist::quarter_hash_type feature_hash_{};
persisted_eval_type persisted_eval_{};

[[nodiscard]] constexpr const zobrist::half_hash_type& hash() const noexcept { return hash_; }
[[nodiscard]] constexpr const zobrist::quarter_hash_type& feature_hash() const noexcept { return feature_hash_; }
[[nodiscard]] constexpr score_type eval() const noexcept { return static_cast<score_type>(persisted_eval_); }

[[nodiscard]] static constexpr eval_cache_entry make(
const zobrist::hash_type& hash, const zobrist::quarter_hash_type& feature_hash, const score_type& eval) noexcept {
const zobrist::half_hash_type hash_upper_half = zobrist::upper_half(hash);
const persisted_eval_type persisted_eval = static_cast<persisted_eval_type>(eval);

return eval_cache_entry{hash_upper_half, feature_hash, persisted_eval};
}
};

struct eval_cache {
Expand All @@ -40,14 +55,12 @@ struct eval_cache {
[[nodiscard]] static constexpr std::size_t hash_function(const zobrist::hash_type& hash) noexcept { return hash & (N - 1); }
inline void prefetch(const zobrist::hash_type& hash) const noexcept { __builtin_prefetch(data.data() + hash_function(hash)); }

[[nodiscard]] constexpr std::optional<score_type> find(const zobrist::hash_type& hash) const noexcept {
if (data[hash_function(hash)].hash == zobrist::upper_half(hash)) { return data[hash_function(hash)].eval; }
[[nodiscard]] constexpr std::optional<eval_cache_entry> find(const zobrist::hash_type& hash) const noexcept {
if (data[hash_function(hash)].hash() == zobrist::upper_half(hash)) { return data[hash_function(hash)]; }
return std::nullopt;
}

void insert(const zobrist::hash_type& hash, const score_type& eval) noexcept {
data[hash_function(hash)] = eval_cache_entry{zobrist::upper_half(hash), eval};
}
void insert(const zobrist::hash_type& hash, const eval_cache_entry& entry) noexcept { data[hash_function(hash)] = entry; }

void clear() noexcept { return data.fill(eval_cache_entry{}); }
};
Expand Down
54 changes: 48 additions & 6 deletions include/search/eval_correction_history.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ struct eval_correction_history {

std::array<score_type, N> data{};

[[nodiscard]] static constexpr std::size_t hash_function(const zobrist::hash_type& feature_hash) noexcept { return feature_hash & mask; }
[[nodiscard]] static constexpr std::size_t hash_function(const zobrist::quarter_hash_type& feature_hash) noexcept { return feature_hash & mask; }

[[nodiscard]] inline score_type correction_for(const zobrist::hash_type& feature_hash) const noexcept {
[[nodiscard]] constexpr score_type correction_for(const zobrist::quarter_hash_type& feature_hash) const noexcept {
const score_type raw_correction = data[hash_function(feature_hash)];
return raw_correction / eval_correction_scale;
}

void update(const zobrist::hash_type& feature_hash, const bound_type& bound, const score_type& error) noexcept {
constexpr void update(const zobrist::quarter_hash_type& feature_hash, const bound_type& bound, const score_type& error) noexcept {
constexpr score_type score_correction_limit = 65536;

constexpr score_type filter_alpha = 1;
Expand All @@ -60,9 +60,51 @@ struct eval_correction_history {
void clear() noexcept { return data.fill(score_type{}); }
};

struct sided_eval_correction_history : public chess::sided<sided_eval_correction_history, eval_correction_history> {
eval_correction_history white;
eval_correction_history black;
template <std::size_t N>
struct composite_feature_hash {
std::array<zobrist::quarter_hash_type, N> hashes_;

[[nodiscard]] constexpr zobrist::quarter_hash_type hash(const std::size_t& i) const noexcept { return hashes_[i]; }
};

template <typename... Ts>
[[nodiscard]] constexpr composite_feature_hash<sizeof...(Ts)> composite_feature_hash_of(const Ts&... ts) noexcept {
return composite_feature_hash<sizeof...(Ts)>{{ts...}};
}

template <std::size_t N>
struct composite_eval_correction_history {
std::array<eval_correction_history, N> histories_{};

[[nodiscard]] constexpr score_type correction_for(const composite_feature_hash<N>& composite_hash) const noexcept {
score_type result{};

for (std::size_t i(0); i < N; ++i) {
const zobrist::quarter_hash_type hash = composite_hash.hash(i);
result += histories_[i].correction_for(hash);
}

return result;
}

constexpr void update(const composite_feature_hash<N>& composite_hash, const bound_type& bound, const score_type& error) noexcept {
for (std::size_t i(0); i < N; ++i) {
const zobrist::quarter_hash_type hash = composite_hash.hash(i);
histories_[i].update(hash, bound, error);
}
}

void clear() noexcept {
for (auto& history : histories_) { history.clear(); }
}
};

constexpr std::size_t eval_correction_history_num_hashes = 2;
struct sided_eval_correction_history
: public chess::sided<sided_eval_correction_history, composite_eval_correction_history<eval_correction_history_num_hashes>> {
using hash_type = composite_feature_hash<eval_correction_history_num_hashes>;
composite_eval_correction_history<eval_correction_history_num_hashes> white;
composite_eval_correction_history<eval_correction_history_num_hashes> black;

void clear() noexcept {
white.clear();
Expand Down
14 changes: 13 additions & 1 deletion include/search/search_worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,28 @@ struct pv_search_result<false> {
template <>
struct pv_search_result<true> {
using type = std::tuple<score_type, chess::move>;

};

template <bool is_root>
using pv_search_result_t = typename pv_search_result<is_root>::type;

struct evaluation_info {
sided_eval_correction_history::hash_type hash;
score_type static_value;
score_type value;
};

struct search_worker {
search_worker_external_state external;
search_worker_internal_state internal{};

template <bool is_pv, bool use_tt = true>
[[nodiscard]] inline evaluation_info evaluate_(
const stack_view& ss,
nnue::eval_node& eval_node,
const chess::board& bd,
const std::optional<transposition_table_entry>& maybe) noexcept;

template <bool is_pv, bool use_tt = true>
[[nodiscard]] score_type q_search(
const stack_view& ss,
Expand Down
1 change: 1 addition & 0 deletions include/search/search_worker_internal_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#pragma once

#include <chess/move.h>
#include <nnue/eval.h>
#include <nnue/feature_reset_cache.h>
#include <search/eval_cache.h>
#include <search/eval_correction_history.h>
Expand Down
2 changes: 2 additions & 0 deletions include/zobrist/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace zobrist {

using hash_type = std::uint64_t;
using half_hash_type = std::uint32_t;
using quarter_hash_type = std::uint16_t;

constexpr hash_type entropy_0 = 0x8c57d3cb77fabf02;
constexpr hash_type entropy_1 = 0xfe2951fb31cae837;
Expand All @@ -35,6 +36,7 @@ constexpr hash_type entropy_5 = 0x1df555934cfcb8f5;

constexpr half_hash_type lower_half(const hash_type& hash) { return hash & std::numeric_limits<half_hash_type>::max(); }
constexpr half_hash_type upper_half(const hash_type& hash) { return (hash >> 32) & std::numeric_limits<half_hash_type>::max(); }
constexpr quarter_hash_type lower_quarter(const hash_type& hash) { return hash & std::numeric_limits<quarter_hash_type>::max(); }

struct xorshift_generator {
hash_type seed_;
Expand Down
68 changes: 68 additions & 0 deletions include/zobrist/zobrist_hasher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Seer is a UCI chess engine by Connor McMonigle
Copyright (C) 2021-2023 Connor McMonigle
Seer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Seer is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <zobrist/util.h>

#include <algorithm>
#include <array>
#include <cstddef>
#include <random>
#include <utility>

namespace zobrist {

namespace detail {

template <typename T, std::size_t N>
struct zobrist_hash_source_impl {
std::array<T, N> data{};

constexpr zobrist_hash_source_impl(zobrist::xorshift_generator generator) noexcept {
for (auto& elem : data) { elem = generator.next(); }
}
};

template <typename T, std::size_t N>
inline constexpr zobrist_hash_source_impl<T, N> zobrist_hash_source = zobrist_hash_source_impl<T, N>{xorshift_generator(zobrist::entropy_0)};

} // namespace detail

template <typename T, std::size_t N>
struct zobrist_hasher_impl {
static constexpr T initial_hash_value = T{};
static constexpr std::size_t feature_cardinality = N;

template <typename F>
[[nodiscard]] constexpr T compute_hash(F&& indicator_function) const noexcept {
T hash = initial_hash_value;

#pragma omp simd
for (std::size_t i = 0; i < N; ++i) {
const T mask = static_cast<T>(indicator_function(i));
hash ^= mask * detail::zobrist_hash_source<T, N>.data[i];
}

return hash;
}
};

template <typename T, std::size_t N>
inline constexpr zobrist_hasher_impl<T, N> zobrist_hasher = zobrist_hasher_impl<T, N>{};

} // namespace zobrist
5 changes: 4 additions & 1 deletion src/engine/uci.cc
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,11 @@ void uci::eval() noexcept {
auto evaluator = nnue::eval(&weights_, scratchpad.get(), 0, 0);
position.feature_full_reset(evaluator);

const auto [hash, score] = evaluator.evaluate(position.turn(), position.phase<nnue::weights::parameter_type>());

os << "hash: " << hash << std::endl;
os << "phase: " << position.phase<nnue::weights::parameter_type>() << std::endl;
os << "score(phase): " << evaluator.evaluate(position.turn(), position.phase<nnue::weights::parameter_type>()) << std::endl;
os << "score(phase): " << score << std::endl;
}

void uci::probe() noexcept {
Expand Down
Loading

0 comments on commit f152efd

Please sign in to comment.