Update to Stockfish 15.1

This commit is contained in:
Peter Osterlund 2022-12-06 20:24:00 +01:00
parent 7be558d773
commit 10ff6f556b
29 changed files with 645 additions and 519 deletions

View File

@ -115,8 +115,6 @@ namespace Eval {
currentEvalFileName = eval_file;
}
}
if (currentEvalFileName != eval_file)
currentEvalFileName = "";
}
/// NNUE::verify() verifies that the last net used was loaded successfully
@ -161,7 +159,7 @@ namespace Trace {
Score scores[TERM_NB][COLOR_NB];
double to_cp(Value v) { return double(v) / PawnValueEg; }
double to_cp(Value v) { return double(v) / UCI::NormalizeToPawnValue; }
void add(int idx, Color c, Score s) {
scores[idx][c] = s;
@ -983,7 +981,7 @@ namespace {
// Initialize score by reading the incrementally updated scores included in
// the position object (material + piece square tables) and the material
// imbalance. Score is computed internally from the white point of view.
Score score = pos.psq_score() + me->imbalance() + pos.this_thread()->trend;
Score score = pos.psq_score() + me->imbalance();
// Probe the pawn hash table
pe = Pawns::probe(pos);
@ -1044,74 +1042,46 @@ make_v:
return v;
}
/// Fisher Random Chess: correction for cornered bishops, to fix chess960 play with NNUE
Value fix_FRC(const Position& pos) {
constexpr Bitboard Corners = 1ULL << SQ_A1 | 1ULL << SQ_H1 | 1ULL << SQ_A8 | 1ULL << SQ_H8;
if (!(pos.pieces(BISHOP) & Corners))
return VALUE_ZERO;
int correction = 0;
if ( pos.piece_on(SQ_A1) == W_BISHOP
&& pos.piece_on(SQ_B2) == W_PAWN)
correction -= CorneredBishop;
if ( pos.piece_on(SQ_H1) == W_BISHOP
&& pos.piece_on(SQ_G2) == W_PAWN)
correction -= CorneredBishop;
if ( pos.piece_on(SQ_A8) == B_BISHOP
&& pos.piece_on(SQ_B7) == B_PAWN)
correction += CorneredBishop;
if ( pos.piece_on(SQ_H8) == B_BISHOP
&& pos.piece_on(SQ_G7) == B_PAWN)
correction += CorneredBishop;
return pos.side_to_move() == WHITE ? Value(3 * correction)
: -Value(3 * correction);
}
} // namespace Eval
/// evaluate() is the evaluator for the outer world. It returns a static
/// evaluation of the position from the point of view of the side to move.
Value Eval::evaluate(const Position& pos) {
Value Eval::evaluate(const Position& pos, int* complexity) {
Value v;
bool useClassical = false;
Value psq = pos.psq_eg_stm();
// Deciding between classical and NNUE eval (~10 Elo): for high PSQ imbalance we use classical,
// but we switch to NNUE during long shuffling or with high material on the board.
if ( !useNNUE
|| ((pos.this_thread()->depth > 9 || pos.count<ALL_PIECES>() > 7) &&
abs(eg_value(pos.psq_score())) * 5 > (856 + pos.non_pawn_material() / 64) * (10 + pos.rule50_count())))
// We use the much less accurate but faster Classical eval when the NNUE
// option is set to false. Otherwise we use the NNUE eval unless the
// PSQ advantage is decisive and several pieces remain. (~3 Elo)
bool useClassical = !useNNUE || (pos.count<ALL_PIECES>() > 7 && abs(psq) > 1760);
if (useClassical)
v = Evaluation<NO_TRACE>(pos).value();
else
{
v = Evaluation<NO_TRACE>(pos).value(); // classical
useClassical = abs(v) >= 297;
}
int nnueComplexity;
int scale = 1064 + 106 * pos.non_pawn_material() / 5120;
// If result of a classical evaluation is much lower than threshold fall back to NNUE
if (useNNUE && !useClassical)
{
Value nnue = NNUE::evaluate(pos, true); // NNUE
int scale = 1036 + 22 * pos.non_pawn_material() / 1024;
Color stm = pos.side_to_move();
Value optimism = pos.this_thread()->optimism[stm];
Value psq = (stm == WHITE ? 1 : -1) * eg_value(pos.psq_score());
int complexity = 35 * abs(nnue - psq) / 256;
Color stm = pos.side_to_move();
Value optimism = pos.this_thread()->optimism[stm];
optimism = optimism * (44 + complexity) / 31;
v = (nnue + optimism) * scale / 1024 - optimism;
Value nnue = NNUE::evaluate(pos, true, &nnueComplexity);
if (pos.is_chess960())
v += fix_FRC(pos);
// Blend nnue complexity with (semi)classical complexity
nnueComplexity = ( 416 * nnueComplexity
+ 424 * abs(psq - nnue)
+ (optimism > 0 ? int(optimism) * int(psq - nnue) : 0)
) / 1024;
// Return hybrid NNUE complexity to caller
if (complexity)
*complexity = nnueComplexity;
optimism = optimism * (269 + nnueComplexity) / 256;
v = (nnue * scale + optimism * (scale - 754)) / 1024;
}
// Damp down the evaluation linearly when shuffling
@ -1120,6 +1090,10 @@ Value Eval::evaluate(const Position& pos) {
// Guarantee evaluation does not hit the tablebase range
v = std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1);
// When not using NNUE, return classical complexity to caller
if (complexity && (!useNNUE || useClassical))
*complexity = abs(v - psq);
return v;
}
@ -1141,8 +1115,6 @@ std::string Eval::trace(Position& pos) {
std::memset(scores, 0, sizeof(scores));
// Reset any global variable used in eval
pos.this_thread()->depth = 0;
pos.this_thread()->trend = SCORE_ZERO;
pos.this_thread()->bestValue = VALUE_ZERO;
pos.this_thread()->optimism[WHITE] = VALUE_ZERO;
pos.this_thread()->optimism[BLACK] = VALUE_ZERO;

View File

@ -31,7 +31,7 @@ class Position;
namespace Eval {
std::string trace(Position& pos);
Value evaluate(const Position& pos);
Value evaluate(const Position& pos, int* complexity = nullptr);
extern bool useNNUE;
extern std::string currentEvalFileName;
@ -39,12 +39,12 @@ namespace Eval {
// The default net name MUST follow the format nn-[SHA256 first 12 digits].nnue
// for the build process (profile-build and fishtest) to work. Do not change the
// name of the macro, as it is used in the Makefile.
#define EvalFileDefaultName "nn-6877cd24400e.nnue"
#define EvalFileDefaultName "nn-ad9b42354671.nnue"
namespace NNUE {
std::string trace(Position& pos);
Value evaluate(const Position& pos, bool adjusted = false);
Value evaluate(const Position& pos, bool adjusted = false, int* complexity = nullptr);
void init();
void verify();

View File

@ -67,9 +67,8 @@ namespace Stockfish {
namespace {
/// Version number. If Version is left empty, then compile date in the format
/// DD-MM-YY and show in engine_info.
const string Version = "15";
/// Version number or dev.
const string version = "15.1";
/// Our fancy logging facility. The trick here is to replace cin.rdbuf() and
/// cout.rdbuf() with two Tie objects that tie cin and cout to a file stream. We
@ -138,23 +137,41 @@ public:
} // namespace
/// engine_info() returns the full name of the current Stockfish version. This
/// will be either "Stockfish <Tag> DD-MM-YY" (where DD-MM-YY is the date when
/// the program was compiled) or "Stockfish <Version>", depending on whether
/// Version is empty.
/// engine_info() returns the full name of the current Stockfish version.
/// For local dev compiles we try to append the commit sha and commit date
/// from git if that fails only the local compilation date is set and "nogit" is specified:
/// Stockfish dev-YYYYMMDD-SHA
/// or
/// Stockfish dev-YYYYMMDD-nogit
///
/// For releases (non dev builds) we only include the version number:
/// Stockfish version
string engine_info(bool to_uci) {
stringstream ss;
ss << "Stockfish " << version << setfill('0');
const string months("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec");
string month, day, year;
stringstream ss, date(__DATE__); // From compiler, format is "Sep 21 2008"
ss << "Stockfish " << Version << setfill('0');
if (Version.empty())
if (version == "dev")
{
ss << "-";
#ifdef GIT_DATE
ss << GIT_DATE;
#else
const string months("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec");
string month, day, year;
stringstream date(__DATE__); // From compiler, format is "Sep 21 2008"
date >> month >> day >> year;
ss << setw(2) << day << setw(2) << (1 + months.find(month) / 4) << year.substr(2);
ss << year << setw(2) << setfill('0') << (1 + months.find(month) / 4) << setw(2) << setfill('0') << day;
#endif
ss << "-";
#ifdef GIT_SHA
ss << GIT_SHA;
#else
ss << "nogit";
#endif
}
ss << (to_uci ? "\nid author ": " by ")
@ -378,10 +395,9 @@ void std_aligned_free(void* ptr) {
#if defined(_WIN32)
static void* aligned_large_pages_alloc_windows(size_t allocSize) {
static void* aligned_large_pages_alloc_windows([[maybe_unused]] size_t allocSize) {
#if !defined(_WIN64)
(void)allocSize; // suppress unused-parameter compiler warning
return nullptr;
#else
@ -626,8 +642,7 @@ string argv0; // path+name of the executable binary, as given by argv
string binaryDirectory; // path of the executable directory
string workingDirectory; // path of the working directory
void init(int argc, char* argv[]) {
(void)argc;
void init([[maybe_unused]] int argc, char* argv[]) {
string pathSeparator;
// extract the path+name of the executable binary

View File

@ -116,56 +116,16 @@ class ValueList {
public:
std::size_t size() const { return size_; }
void resize(std::size_t newSize) { size_ = newSize; }
void push_back(const T& value) { values_[size_++] = value; }
T& operator[](std::size_t index) { return values_[index]; }
T* begin() { return values_; }
T* end() { return values_ + size_; }
const T& operator[](std::size_t index) const { return values_[index]; }
const T* begin() const { return values_; }
const T* end() const { return values_ + size_; }
void swap(ValueList& other) {
const std::size_t maxSize = std::max(size_, other.size_);
for (std::size_t i = 0; i < maxSize; ++i) {
std::swap(values_[i], other.values_[i]);
}
std::swap(size_, other.size_);
}
private:
T values_[MaxSize];
std::size_t size_ = 0;
};
/// sigmoid(t, x0, y0, C, P, Q) implements a sigmoid-like function using only integers,
/// with the following properties:
///
/// - sigmoid is centered in (x0, y0)
/// - sigmoid has amplitude [-P/Q , P/Q] instead of [-1 , +1]
/// - limit is (y0 - P/Q) when t tends to -infinity
/// - limit is (y0 + P/Q) when t tends to +infinity
/// - the slope can be adjusted using C > 0, smaller C giving a steeper sigmoid
/// - the slope of the sigmoid when t = x0 is P/(Q*C)
/// - sigmoid is increasing with t when P > 0 and Q > 0
/// - to get a decreasing sigmoid, change sign of P
/// - mean value of the sigmoid is y0
///
/// Use <https://www.desmos.com/calculator/jhh83sqq92> to draw the sigmoid
inline int64_t sigmoid(int64_t t, int64_t x0,
int64_t y0,
int64_t C,
int64_t P,
int64_t Q)
{
assert(C > 0);
assert(Q != 0);
return y0 + P * (t-x0) / (Q * (std::abs(t-x0) + C)) ;
}
/// xorshift64star Pseudo-Random Number Generator
/// This class is based on original code written and dedicated
/// to the public domain by Sebastiano Vigna (2014).

View File

@ -69,6 +69,7 @@ MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const ButterflyHist
stage = (pos.checkers() ? EVASION_TT : MAIN_TT) +
!(ttm && pos.pseudo_legal(ttm));
threatenedPieces = 0;
}
/// MovePicker constructor for quiescence search
@ -82,14 +83,13 @@ MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const ButterflyHist
stage = (pos.checkers() ? EVASION_TT : QSEARCH_TT) +
!( ttm
&& (pos.checkers() || depth > DEPTH_QS_RECAPTURES || to_sq(ttm) == recaptureSquare)
&& pos.pseudo_legal(ttm));
}
/// MovePicker constructor for ProbCut: we generate captures with SEE greater
/// than or equal to the given threshold.
MovePicker::MovePicker(const Position& p, Move ttm, Value th, Depth d, const CapturePieceToHistory* cph)
: pos(p), captureHistory(cph), ttMove(ttm), threshold(th), depth(d)
MovePicker::MovePicker(const Position& p, Move ttm, Value th, const CapturePieceToHistory* cph)
: pos(p), captureHistory(cph), ttMove(ttm), threshold(th)
{
assert(!pos.checkers());
@ -106,29 +106,19 @@ void MovePicker::score() {
static_assert(Type == CAPTURES || Type == QUIETS || Type == EVASIONS, "Wrong type");
Bitboard threatened, threatenedByPawn, threatenedByMinor, threatenedByRook;
[[maybe_unused]] Bitboard threatenedByPawn, threatenedByMinor, threatenedByRook;
if constexpr (Type == QUIETS)
{
Color us = pos.side_to_move();
// squares threatened by pawns
threatenedByPawn = pos.attacks_by<PAWN>(~us);
// squares threatened by minors or pawns
threatenedByMinor = pos.attacks_by<KNIGHT>(~us) | pos.attacks_by<BISHOP>(~us) | threatenedByPawn;
// squares threatened by rooks, minors or pawns
threatenedByRook = pos.attacks_by<ROOK>(~us) | threatenedByMinor;
// pieces threatened by pieces of lesser material value
threatened = (pos.pieces(us, QUEEN) & threatenedByRook)
| (pos.pieces(us, ROOK) & threatenedByMinor)
| (pos.pieces(us, KNIGHT, BISHOP) & threatenedByPawn);
}
else
{
// Silence unused variable warnings
(void) threatened;
(void) threatenedByPawn;
(void) threatenedByMinor;
(void) threatenedByRook;
// Pieces threatened by pieces of lesser material value
threatenedPieces = (pos.pieces(us, QUEEN) & threatenedByRook)
| (pos.pieces(us, ROOK) & threatenedByMinor)
| (pos.pieces(us, KNIGHT, BISHOP) & threatenedByPawn);
}
for (auto& m : *this)
@ -137,27 +127,27 @@ void MovePicker::score() {
+ (*captureHistory)[pos.moved_piece(m)][to_sq(m)][type_of(pos.piece_on(to_sq(m)))];
else if constexpr (Type == QUIETS)
m.value = (*mainHistory)[pos.side_to_move()][from_to(m)]
m.value = 2 * (*mainHistory)[pos.side_to_move()][from_to(m)]
+ 2 * (*continuationHistory[0])[pos.moved_piece(m)][to_sq(m)]
+ (*continuationHistory[1])[pos.moved_piece(m)][to_sq(m)]
+ (*continuationHistory[3])[pos.moved_piece(m)][to_sq(m)]
+ (*continuationHistory[5])[pos.moved_piece(m)][to_sq(m)]
+ (threatened & from_sq(m) ?
+ (threatenedPieces & from_sq(m) ?
(type_of(pos.moved_piece(m)) == QUEEN && !(to_sq(m) & threatenedByRook) ? 50000
: type_of(pos.moved_piece(m)) == ROOK && !(to_sq(m) & threatenedByMinor) ? 25000
: !(to_sq(m) & threatenedByPawn) ? 15000
: 0)
: 0);
: 0)
+ bool(pos.check_squares(type_of(pos.moved_piece(m))) & to_sq(m)) * 16384;
else // Type == EVASIONS
{
if (pos.capture(m))
m.value = PieceValue[MG][pos.piece_on(to_sq(m))]
- Value(type_of(pos.moved_piece(m)));
- Value(type_of(pos.moved_piece(m)))
+ (1 << 28);
else
m.value = (*mainHistory)[pos.side_to_move()][from_to(m)]
+ 2 * (*continuationHistory[0])[pos.moved_piece(m)][to_sq(m)]
- (1 << 28);
m.value = (*mainHistory)[pos.side_to_move()][from_to(m)]
+ (*continuationHistory[0])[pos.moved_piece(m)][to_sq(m)];
}
}
@ -201,7 +191,7 @@ top:
endMoves = generate<CAPTURES>(pos, cur);
score<CAPTURES>();
partial_insertion_sort(cur, endMoves, -3000 * depth);
partial_insertion_sort(cur, endMoves, std::numeric_limits<int>::min());
++stage;
goto top;

View File

@ -86,7 +86,8 @@ enum StatsType { NoCaptures, Captures };
/// unsuccessful during the current search, and is used for reduction and move
/// ordering decisions. It uses 2 tables (one for each color) indexed by
/// the move's from and to squares, see www.chessprogramming.org/Butterfly_Boards
typedef Stats<int16_t, 14365, COLOR_NB, int(SQUARE_NB) * int(SQUARE_NB)> ButterflyHistory;
/// (~11 elo)
typedef Stats<int16_t, 7183, COLOR_NB, int(SQUARE_NB) * int(SQUARE_NB)> ButterflyHistory;
/// CounterMoveHistory stores counter moves indexed by [piece][to] of the previous
/// move, see www.chessprogramming.org/Countermove_Heuristic
@ -101,6 +102,7 @@ typedef Stats<int16_t, 29952, PIECE_NB, SQUARE_NB> PieceToHistory;
/// ContinuationHistory is the combined history of a given pair of moves, usually
/// the current one given a previous one. The nested history table is based on
/// PieceToHistory instead of ButterflyBoards.
/// (~63 elo)
typedef Stats<PieceToHistory, NOT_USED, PIECE_NB, SQUARE_NB> ContinuationHistory;
@ -126,9 +128,11 @@ public:
const CapturePieceToHistory*,
const PieceToHistory**,
Square);
MovePicker(const Position&, Move, Value, Depth, const CapturePieceToHistory*);
MovePicker(const Position&, Move, Value, const CapturePieceToHistory*);
Move next_move(bool skipQuiets = false);
Bitboard threatenedPieces;
private:
template<PickType T, typename Pred> Move select(Pred);
template<GenType> void score();

View File

@ -137,13 +137,13 @@ namespace Stockfish::Eval::NNUE {
}
// Evaluation function. Perform differential calculation.
Value evaluate(const Position& pos, bool adjusted) {
Value evaluate(const Position& pos, bool adjusted, int* complexity) {
// We manually align the arrays on the stack because with gcc < 9.3
// overaligning stack variables with alignas() doesn't work correctly.
constexpr uint64_t alignment = CacheLineSize;
int delta = 10 - pos.non_pawn_material() / 1515;
int delta = 24 - pos.non_pawn_material() / 9560;
#if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN)
TransformedFeatureType transformedFeaturesUnaligned[
@ -161,9 +161,12 @@ namespace Stockfish::Eval::NNUE {
const auto psqt = featureTransformer->transform(pos, transformedFeatures, bucket);
const auto positional = network[bucket]->propagate(transformedFeatures);
if (complexity)
*complexity = abs(psqt - positional) / OutputScale;
// Give more value to positional evaluation when adjusted flag is set
if (adjusted)
return static_cast<Value>(((128 - delta) * psqt + (128 + delta) * positional) / 128 / OutputScale);
return static_cast<Value>(((1024 - delta) * psqt + (1024 + delta) * positional) / (1024 * OutputScale));
else
return static_cast<Value>((psqt + positional) / OutputScale);
}
@ -217,7 +220,7 @@ namespace Stockfish::Eval::NNUE {
buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' ');
int cp = std::abs(100 * v / PawnValueEg);
int cp = std::abs(100 * v / UCI::NormalizeToPawnValue);
if (cp >= 10000)
{
buffer[1] = '0' + cp / 10000; cp %= 10000;
@ -248,7 +251,7 @@ namespace Stockfish::Eval::NNUE {
buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' ');
double cp = 1.0 * std::abs(int(v)) / PawnValueEg;
double cp = 1.0 * std::abs(int(v)) / UCI::NormalizeToPawnValue;
sprintf(&buffer[1], "%6.2f", cp);
}

View File

@ -24,50 +24,51 @@
namespace Stockfish::Eval::NNUE::Features {
// Orient a square according to perspective (rotates by 180 for black)
inline Square HalfKAv2_hm::orient(Color perspective, Square s, Square ksq) {
return Square(int(s) ^ (bool(perspective) * SQ_A8) ^ ((file_of(ksq) < FILE_E) * SQ_H1));
}
// Index of a feature for a given king position and another piece on some square
inline IndexType HalfKAv2_hm::make_index(Color perspective, Square s, Piece pc, Square ksq) {
Square o_ksq = orient(perspective, ksq, ksq);
return IndexType(orient(perspective, s, ksq) + PieceSquareIndex[perspective][pc] + PS_NB * KingBuckets[o_ksq]);
template<Color Perspective>
inline IndexType HalfKAv2_hm::make_index(Square s, Piece pc, Square ksq) {
return IndexType((int(s) ^ OrientTBL[Perspective][ksq]) + PieceSquareIndex[Perspective][pc] + KingBuckets[Perspective][ksq]);
}
// Get a list of indices for active features
template<Color Perspective>
void HalfKAv2_hm::append_active_indices(
const Position& pos,
Color perspective,
IndexList& active
) {
Square ksq = pos.square<KING>(perspective);
Square ksq = pos.square<KING>(Perspective);
Bitboard bb = pos.pieces();
while (bb)
{
Square s = pop_lsb(bb);
active.push_back(make_index(perspective, s, pos.piece_on(s), ksq));
active.push_back(make_index<Perspective>(s, pos.piece_on(s), ksq));
}
}
// Explicit template instantiations
template void HalfKAv2_hm::append_active_indices<WHITE>(const Position& pos, IndexList& active);
template void HalfKAv2_hm::append_active_indices<BLACK>(const Position& pos, IndexList& active);
// append_changed_indices() : get a list of indices for recently changed features
template<Color Perspective>
void HalfKAv2_hm::append_changed_indices(
Square ksq,
const DirtyPiece& dp,
Color perspective,
IndexList& removed,
IndexList& added
) {
for (int i = 0; i < dp.dirty_num; ++i) {
if (dp.from[i] != SQ_NONE)
removed.push_back(make_index(perspective, dp.from[i], dp.piece[i], ksq));
removed.push_back(make_index<Perspective>(dp.from[i], dp.piece[i], ksq));
if (dp.to[i] != SQ_NONE)
added.push_back(make_index(perspective, dp.to[i], dp.piece[i], ksq));
added.push_back(make_index<Perspective>(dp.to[i], dp.piece[i], ksq));
}
}
// Explicit template instantiations
template void HalfKAv2_hm::append_changed_indices<WHITE>(Square ksq, const DirtyPiece& dp, IndexList& removed, IndexList& added);
template void HalfKAv2_hm::append_changed_indices<BLACK>(Square ksq, const DirtyPiece& dp, IndexList& removed, IndexList& added);
int HalfKAv2_hm::update_cost(const StateInfo* st) {
return st->dirtyPiece.dirty_num;
}

View File

@ -49,8 +49,8 @@ namespace Stockfish::Eval::NNUE::Features {
PS_B_ROOK = 7 * SQUARE_NB,
PS_W_QUEEN = 8 * SQUARE_NB,
PS_B_QUEEN = 9 * SQUARE_NB,
PS_KING = 10 * SQUARE_NB,
PS_NB = 11 * SQUARE_NB
PS_KING = 10 * SQUARE_NB,
PS_NB = 11 * SQUARE_NB
};
static constexpr IndexType PieceSquareIndex[COLOR_NB][PIECE_NB] = {
@ -62,11 +62,9 @@ namespace Stockfish::Eval::NNUE::Features {
PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_KING, PS_NONE }
};
// Orient a square according to perspective (rotates by 180 for black)
static Square orient(Color perspective, Square s, Square ksq);
// Index of a feature for a given king position and another piece on some square
static IndexType make_index(Color perspective, Square s, Piece pc, Square ksq);
template<Color Perspective>
static IndexType make_index(Square s, Piece pc, Square ksq);
public:
// Feature name
@ -79,15 +77,45 @@ namespace Stockfish::Eval::NNUE::Features {
static constexpr IndexType Dimensions =
static_cast<IndexType>(SQUARE_NB) * static_cast<IndexType>(PS_NB) / 2;
static constexpr int KingBuckets[64] = {
-1, -1, -1, -1, 31, 30, 29, 28,
-1, -1, -1, -1, 27, 26, 25, 24,
-1, -1, -1, -1, 23, 22, 21, 20,
-1, -1, -1, -1, 19, 18, 17, 16,
-1, -1, -1, -1, 15, 14, 13, 12,
-1, -1, -1, -1, 11, 10, 9, 8,
-1, -1, -1, -1, 7, 6, 5, 4,
-1, -1, -1, -1, 3, 2, 1, 0
#define B(v) (v * PS_NB)
static constexpr int KingBuckets[COLOR_NB][SQUARE_NB] = {
{ B(28), B(29), B(30), B(31), B(31), B(30), B(29), B(28),
B(24), B(25), B(26), B(27), B(27), B(26), B(25), B(24),
B(20), B(21), B(22), B(23), B(23), B(22), B(21), B(20),
B(16), B(17), B(18), B(19), B(19), B(18), B(17), B(16),
B(12), B(13), B(14), B(15), B(15), B(14), B(13), B(12),
B( 8), B( 9), B(10), B(11), B(11), B(10), B( 9), B( 8),
B( 4), B( 5), B( 6), B( 7), B( 7), B( 6), B( 5), B( 4),
B( 0), B( 1), B( 2), B( 3), B( 3), B( 2), B( 1), B( 0) },
{ B( 0), B( 1), B( 2), B( 3), B( 3), B( 2), B( 1), B( 0),
B( 4), B( 5), B( 6), B( 7), B( 7), B( 6), B( 5), B( 4),
B( 8), B( 9), B(10), B(11), B(11), B(10), B( 9), B( 8),
B(12), B(13), B(14), B(15), B(15), B(14), B(13), B(12),
B(16), B(17), B(18), B(19), B(19), B(18), B(17), B(16),
B(20), B(21), B(22), B(23), B(23), B(22), B(21), B(20),
B(24), B(25), B(26), B(27), B(27), B(26), B(25), B(24),
B(28), B(29), B(30), B(31), B(31), B(30), B(29), B(28) }
};
#undef B
// Orient a square according to perspective (rotates by 180 for black)
static constexpr int OrientTBL[COLOR_NB][SQUARE_NB] = {
{ SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1,
SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1,
SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1,
SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1,
SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1,
SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1,
SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1,
SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1 },
{ SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8,
SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8,
SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8,
SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8,
SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8,
SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8,
SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8,
SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8 }
};
// Maximum number of simultaneously active features.
@ -95,16 +123,16 @@ namespace Stockfish::Eval::NNUE::Features {
using IndexList = ValueList<IndexType, MaxActiveDimensions>;
// Get a list of indices for active features
template<Color Perspective>
static void append_active_indices(
const Position& pos,
Color perspective,
IndexList& active);
// Get a list of indices for recently changed features
template<Color Perspective>
static void append_changed_indices(
Square ksq,
const DirtyPiece& dp,
Color perspective,
IndexList& removed,
IndexList& added
);

View File

@ -25,7 +25,7 @@
#include <algorithm>
#include <type_traits>
#include "../nnue_common.h"
#include "../../simd.h"
#include "simd.h"
/*
This file contains the definition for a fully connected layer (aka affine transform).
@ -151,9 +151,15 @@ namespace Stockfish::Eval::NNUE::Layers {
template <IndexType InDims, IndexType OutDims, typename Enabled = void>
class AffineTransform;
#if defined (USE_AVX512)
constexpr IndexType LargeInputSize = 2 * 64;
#else
constexpr IndexType LargeInputSize = std::numeric_limits<IndexType>::max();
#endif
// A specialization for large inputs.
template <IndexType InDims, IndexType OutDims>
class AffineTransform<InDims, OutDims, std::enable_if_t<(ceil_to_multiple<IndexType>(InDims, MaxSimdWidth) >= 2*64)>> {
class AffineTransform<InDims, OutDims, std::enable_if_t<(ceil_to_multiple<IndexType>(InDims, MaxSimdWidth) >= LargeInputSize)>> {
public:
// Input/output type
using InputType = std::uint8_t;
@ -170,7 +176,7 @@ namespace Stockfish::Eval::NNUE::Layers {
using OutputBuffer = OutputType[PaddedOutputDimensions];
static_assert(PaddedInputDimensions >= 128, "Something went wrong. This specialization should not have been chosen.");
static_assert(PaddedInputDimensions >= LargeInputSize, "Something went wrong. This specialization should not have been chosen.");
#if defined (USE_AVX512)
static constexpr const IndexType InputSimdWidth = 64;
@ -369,7 +375,7 @@ namespace Stockfish::Eval::NNUE::Layers {
};
template <IndexType InDims, IndexType OutDims>
class AffineTransform<InDims, OutDims, std::enable_if_t<(ceil_to_multiple<IndexType>(InDims, MaxSimdWidth) < 2*64)>> {
class AffineTransform<InDims, OutDims, std::enable_if_t<(ceil_to_multiple<IndexType>(InDims, MaxSimdWidth) < LargeInputSize)>> {
public:
// Input/output type
// Input/output type
@ -387,7 +393,7 @@ namespace Stockfish::Eval::NNUE::Layers {
using OutputBuffer = OutputType[PaddedOutputDimensions];
static_assert(PaddedInputDimensions < 128, "Something went wrong. This specialization should not have been chosen.");
static_assert(PaddedInputDimensions < LargeInputSize, "Something went wrong. This specialization should not have been chosen.");
#if defined (USE_SSSE3)
static constexpr const IndexType OutputSimdWidth = SimdWidth / 4;

View File

@ -0,0 +1,120 @@
/*
Stockfish, a UCI chess playing engine derived from Glaurung 2.1
Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file)
Stockfish is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Stockfish is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Definition of layer ClippedReLU of NNUE evaluation function
#ifndef NNUE_LAYERS_SQR_CLIPPED_RELU_H_INCLUDED
#define NNUE_LAYERS_SQR_CLIPPED_RELU_H_INCLUDED
#include "../nnue_common.h"
namespace Stockfish::Eval::NNUE::Layers {
// Clipped ReLU
template <IndexType InDims>
class SqrClippedReLU {
public:
// Input/output type
using InputType = std::int32_t;
using OutputType = std::uint8_t;
// Number of input/output dimensions
static constexpr IndexType InputDimensions = InDims;
static constexpr IndexType OutputDimensions = InputDimensions;
static constexpr IndexType PaddedOutputDimensions =
ceil_to_multiple<IndexType>(OutputDimensions, 32);
using OutputBuffer = OutputType[PaddedOutputDimensions];
// Hash value embedded in the evaluation file
static constexpr std::uint32_t get_hash_value(std::uint32_t prevHash) {
std::uint32_t hashValue = 0x538D24C7u;
hashValue += prevHash;
return hashValue;
}
// Read network parameters
bool read_parameters(std::istream&) {
return true;
}
// Write network parameters
bool write_parameters(std::ostream&) const {
return true;
}
// Forward propagation
const OutputType* propagate(
const InputType* input, OutputType* output) const {
#if defined(USE_SSE2)
constexpr IndexType NumChunks = InputDimensions / 16;
#ifdef USE_SSE41
const __m128i Zero = _mm_setzero_si128();
#else
const __m128i k0x80s = _mm_set1_epi8(-128);
#endif
static_assert(WeightScaleBits == 6);
const auto in = reinterpret_cast<const __m128i*>(input);
const auto out = reinterpret_cast<__m128i*>(output);
for (IndexType i = 0; i < NumChunks; ++i) {
__m128i words0 = _mm_packs_epi32(
_mm_load_si128(&in[i * 4 + 0]),
_mm_load_si128(&in[i * 4 + 1]));
__m128i words1 = _mm_packs_epi32(
_mm_load_si128(&in[i * 4 + 2]),
_mm_load_si128(&in[i * 4 + 3]));
// Not sure if
words0 = _mm_srli_epi16(_mm_mulhi_epi16(words0, words0), 3);
words1 = _mm_srli_epi16(_mm_mulhi_epi16(words1, words1), 3);
const __m128i packedbytes = _mm_packs_epi16(words0, words1);
_mm_store_si128(&out[i],
#ifdef USE_SSE41
_mm_max_epi8(packedbytes, Zero)
#else
_mm_subs_epi8(_mm_adds_epi8(packedbytes, k0x80s), k0x80s)
#endif
);
}
constexpr IndexType Start = NumChunks * 16;
#else
constexpr IndexType Start = 0;
#endif
for (IndexType i = Start; i < InputDimensions; ++i) {
output[i] = static_cast<OutputType>(
// realy should be /127 but we need to make it fast
// needs to be accounted for in the trainer
std::max(0ll, std::min(127ll, (((long long)input[i] * input[i]) >> (2 * WeightScaleBits)) / 128)));
}
return output;
}
};
} // namespace Stockfish::Eval::NNUE::Layers
#endif // NNUE_LAYERS_SQR_CLIPPED_RELU_H_INCLUDED

View File

@ -29,6 +29,7 @@
#include "layers/affine_transform.h"
#include "layers/clipped_relu.h"
#include "layers/sqr_clipped_relu.h"
#include "../misc.h"
@ -48,8 +49,9 @@ struct Network
static constexpr int FC_1_OUTPUTS = 32;
Layers::AffineTransform<TransformedFeatureDimensions, FC_0_OUTPUTS + 1> fc_0;
Layers::SqrClippedReLU<FC_0_OUTPUTS + 1> ac_sqr_0;
Layers::ClippedReLU<FC_0_OUTPUTS + 1> ac_0;
Layers::AffineTransform<FC_0_OUTPUTS, FC_1_OUTPUTS> fc_1;
Layers::AffineTransform<FC_0_OUTPUTS * 2, FC_1_OUTPUTS> fc_1;
Layers::ClippedReLU<FC_1_OUTPUTS> ac_1;
Layers::AffineTransform<FC_1_OUTPUTS, 1> fc_2;
@ -93,6 +95,7 @@ struct Network
struct alignas(CacheLineSize) Buffer
{
alignas(CacheLineSize) decltype(fc_0)::OutputBuffer fc_0_out;
alignas(CacheLineSize) decltype(ac_sqr_0)::OutputType ac_sqr_0_out[ceil_to_multiple<IndexType>(FC_0_OUTPUTS * 2, 32)];
alignas(CacheLineSize) decltype(ac_0)::OutputBuffer ac_0_out;
alignas(CacheLineSize) decltype(fc_1)::OutputBuffer fc_1_out;
alignas(CacheLineSize) decltype(ac_1)::OutputBuffer ac_1_out;
@ -114,8 +117,10 @@ struct Network
#endif
fc_0.propagate(transformedFeatures, buffer.fc_0_out);
ac_sqr_0.propagate(buffer.fc_0_out, buffer.ac_sqr_0_out);
ac_0.propagate(buffer.fc_0_out, buffer.ac_0_out);
fc_1.propagate(buffer.ac_0_out, buffer.fc_1_out);
std::memcpy(buffer.ac_sqr_0_out + FC_0_OUTPUTS, buffer.ac_0_out, FC_0_OUTPUTS * sizeof(decltype(ac_0)::OutputType));
fc_1.propagate(buffer.ac_sqr_0_out, buffer.fc_1_out);
ac_1.propagate(buffer.fc_1_out, buffer.ac_1_out);
fc_2.propagate(buffer.ac_1_out, buffer.fc_2_out);

View File

@ -120,12 +120,12 @@ namespace Stockfish::Eval::NNUE {
#define vec_zero() _mm_setzero_si64()
#define vec_set_16(a) _mm_set1_pi16(a)
inline vec_t vec_max_16(vec_t a,vec_t b){
vec_t comparison = _mm_cmpgt_pi16(a,b);
return _mm_or_si64(_mm_and_si64(comparison, a), _mm_andnot_si64(comparison, b));
vec_t comparison = _mm_cmpgt_pi16(a,b);
return _mm_or_si64(_mm_and_si64(comparison, a), _mm_andnot_si64(comparison, b));
}
inline vec_t vec_min_16(vec_t a,vec_t b){
vec_t comparison = _mm_cmpgt_pi16(a,b);
return _mm_or_si64(_mm_and_si64(comparison, b), _mm_andnot_si64(comparison, a));
vec_t comparison = _mm_cmpgt_pi16(a,b);
return _mm_or_si64(_mm_and_si64(comparison, b), _mm_andnot_si64(comparison, a));
}
#define vec_msb_pack_16(a,b) _mm_packs_pi16(_mm_srli_pi16(a,7),_mm_srli_pi16(b,7))
#define vec_load_psqt(a) (*(a))
@ -150,10 +150,10 @@ namespace Stockfish::Eval::NNUE {
#define vec_max_16(a,b) vmaxq_s16(a,b)
#define vec_min_16(a,b) vminq_s16(a,b)
inline vec_t vec_msb_pack_16(vec_t a, vec_t b){
const int8x8_t shifta = vshrn_n_s16(a, 7);
const int8x8_t shiftb = vshrn_n_s16(b, 7);
const int8x16_t compacted = vcombine_s8(shifta,shiftb);
return *reinterpret_cast<const vec_t*> (&compacted);
const int8x8_t shifta = vshrn_n_s16(a, 7);
const int8x8_t shiftb = vshrn_n_s16(b, 7);
const int8x16_t compacted = vcombine_s8(shifta,shiftb);
return *reinterpret_cast<const vec_t*> (&compacted);
}
#define vec_load_psqt(a) (*(a))
#define vec_store_psqt(a,b) *(a)=(b)
@ -271,8 +271,8 @@ namespace Stockfish::Eval::NNUE {
// Convert input features
std::int32_t transform(const Position& pos, OutputType* output, int bucket) const {
update_accumulator(pos, WHITE);
update_accumulator(pos, BLACK);
update_accumulator<WHITE>(pos);
update_accumulator<BLACK>(pos);
const Color perspectives[2] = {pos.side_to_move(), ~pos.side_to_move()};
const auto& accumulation = pos.state()->accumulator.accumulation;
@ -290,7 +290,7 @@ namespace Stockfish::Eval::NNUE {
#if defined(VECTOR)
constexpr IndexType OutputChunkSize = MaxChunkSize;
constexpr IndexType OutputChunkSize = MaxChunkSize;
static_assert((HalfDimensions / 2) % OutputChunkSize == 0);
constexpr IndexType NumOutputChunks = HalfDimensions / 2 / OutputChunkSize;
@ -338,7 +338,8 @@ namespace Stockfish::Eval::NNUE {
private:
void update_accumulator(const Position& pos, const Color perspective) const {
template<Color Perspective>
void update_accumulator(const Position& pos) const {
// The size must be enough to contain the largest possible update.
// That might depend on the feature set and generally relies on the
@ -356,18 +357,18 @@ namespace Stockfish::Eval::NNUE {
// of the estimated gain in terms of features to be added/subtracted.
StateInfo *st = pos.state(), *next = nullptr;
int gain = FeatureSet::refresh_cost(pos);
while (st->previous && !st->accumulator.computed[perspective])
while (st->previous && !st->accumulator.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)
if ( FeatureSet::requires_refresh(st, Perspective)
|| (gain -= FeatureSet::update_cost(st) + 1) < 0)
break;
next = st;
st = st->previous;
}
if (st->accumulator.computed[perspective])
if (st->accumulator.computed[Perspective])
{
if (next == nullptr)
return;
@ -376,17 +377,17 @@ namespace Stockfish::Eval::NNUE {
// accumulator. Then, we update the current accumulator (pos.state()).
// Gather all features to be updated.
const Square ksq = pos.square<KING>(perspective);
const Square ksq = pos.square<KING>(Perspective);
FeatureSet::IndexList removed[2], added[2];
FeatureSet::append_changed_indices(
ksq, next->dirtyPiece, perspective, removed[0], added[0]);
FeatureSet::append_changed_indices<Perspective>(
ksq, next->dirtyPiece, removed[0], added[0]);
for (StateInfo *st2 = pos.state(); st2 != next; st2 = st2->previous)
FeatureSet::append_changed_indices(
ksq, st2->dirtyPiece, perspective, removed[1], added[1]);
FeatureSet::append_changed_indices<Perspective>(
ksq, st2->dirtyPiece, removed[1], added[1]);
// Mark the accumulators as computed.
next->accumulator.computed[perspective] = true;
pos.state()->accumulator.computed[perspective] = true;
next->accumulator.computed[Perspective] = true;
pos.state()->accumulator.computed[Perspective] = true;
// Now update the accumulators listed in states_to_update[], where the last element is a sentinel.
StateInfo *states_to_update[3] =
@ -396,7 +397,7 @@ namespace Stockfish::Eval::NNUE {
{
// Load accumulator
auto accTile = reinterpret_cast<vec_t*>(
&st->accumulator.accumulation[perspective][j * TileHeight]);
&st->accumulator.accumulation[Perspective][j * TileHeight]);
for (IndexType k = 0; k < NumRegs; ++k)
acc[k] = vec_load(&accTile[k]);
@ -422,7 +423,7 @@ namespace Stockfish::Eval::NNUE {
// Store accumulator
accTile = reinterpret_cast<vec_t*>(
&states_to_update[i]->accumulator.accumulation[perspective][j * TileHeight]);
&states_to_update[i]->accumulator.accumulation[Perspective][j * TileHeight]);
for (IndexType k = 0; k < NumRegs; ++k)
vec_store(&accTile[k], acc[k]);
}
@ -432,7 +433,7 @@ namespace Stockfish::Eval::NNUE {
{
// Load accumulator
auto accTilePsqt = reinterpret_cast<psqt_vec_t*>(
&st->accumulator.psqtAccumulation[perspective][j * PsqtTileHeight]);
&st->accumulator.psqtAccumulation[Perspective][j * PsqtTileHeight]);
for (std::size_t k = 0; k < NumPsqtRegs; ++k)
psqt[k] = vec_load_psqt(&accTilePsqt[k]);
@ -458,7 +459,7 @@ namespace Stockfish::Eval::NNUE {
// Store accumulator
accTilePsqt = reinterpret_cast<psqt_vec_t*>(
&states_to_update[i]->accumulator.psqtAccumulation[perspective][j * PsqtTileHeight]);
&states_to_update[i]->accumulator.psqtAccumulation[Perspective][j * PsqtTileHeight]);
for (std::size_t k = 0; k < NumPsqtRegs; ++k)
vec_store_psqt(&accTilePsqt[k], psqt[k]);
}
@ -467,12 +468,12 @@ namespace Stockfish::Eval::NNUE {
#else
for (IndexType i = 0; states_to_update[i]; ++i)
{
std::memcpy(states_to_update[i]->accumulator.accumulation[perspective],
st->accumulator.accumulation[perspective],
std::memcpy(states_to_update[i]->accumulator.accumulation[Perspective],
st->accumulator.accumulation[Perspective],
HalfDimensions * sizeof(BiasType));
for (std::size_t k = 0; k < PSQTBuckets; ++k)
states_to_update[i]->accumulator.psqtAccumulation[perspective][k] = st->accumulator.psqtAccumulation[perspective][k];
states_to_update[i]->accumulator.psqtAccumulation[Perspective][k] = st->accumulator.psqtAccumulation[Perspective][k];
st = states_to_update[i];
@ -482,10 +483,10 @@ namespace Stockfish::Eval::NNUE {
const IndexType offset = HalfDimensions * index;
for (IndexType j = 0; j < HalfDimensions; ++j)
st->accumulator.accumulation[perspective][j] -= weights[offset + j];
st->accumulator.accumulation[Perspective][j] -= weights[offset + j];
for (std::size_t k = 0; k < PSQTBuckets; ++k)
st->accumulator.psqtAccumulation[perspective][k] -= psqtWeights[index * PSQTBuckets + k];
st->accumulator.psqtAccumulation[Perspective][k] -= psqtWeights[index * PSQTBuckets + k];
}
// Difference calculation for the activated features
@ -494,10 +495,10 @@ namespace Stockfish::Eval::NNUE {
const IndexType offset = HalfDimensions * index;
for (IndexType j = 0; j < HalfDimensions; ++j)
st->accumulator.accumulation[perspective][j] += weights[offset + j];
st->accumulator.accumulation[Perspective][j] += weights[offset + j];
for (std::size_t k = 0; k < PSQTBuckets; ++k)
st->accumulator.psqtAccumulation[perspective][k] += psqtWeights[index * PSQTBuckets + k];
st->accumulator.psqtAccumulation[Perspective][k] += psqtWeights[index * PSQTBuckets + k];
}
}
#endif
@ -506,9 +507,9 @@ namespace Stockfish::Eval::NNUE {
{
// Refresh the accumulator
auto& accumulator = pos.state()->accumulator;
accumulator.computed[perspective] = true;
accumulator.computed[Perspective] = true;
FeatureSet::IndexList active;
FeatureSet::append_active_indices(pos, perspective, active);
FeatureSet::append_active_indices<Perspective>(pos, active);
#ifdef VECTOR
for (IndexType j = 0; j < HalfDimensions / TileHeight; ++j)
@ -528,7 +529,7 @@ namespace Stockfish::Eval::NNUE {
}
auto accTile = reinterpret_cast<vec_t*>(
&accumulator.accumulation[perspective][j * TileHeight]);
&accumulator.accumulation[Perspective][j * TileHeight]);
for (unsigned k = 0; k < NumRegs; k++)
vec_store(&accTile[k], acc[k]);
}
@ -548,27 +549,27 @@ namespace Stockfish::Eval::NNUE {
}
auto accTilePsqt = reinterpret_cast<psqt_vec_t*>(
&accumulator.psqtAccumulation[perspective][j * PsqtTileHeight]);
&accumulator.psqtAccumulation[Perspective][j * PsqtTileHeight]);
for (std::size_t k = 0; k < NumPsqtRegs; ++k)
vec_store_psqt(&accTilePsqt[k], psqt[k]);
}
#else
std::memcpy(accumulator.accumulation[perspective], biases,
std::memcpy(accumulator.accumulation[Perspective], biases,
HalfDimensions * sizeof(BiasType));
for (std::size_t k = 0; k < PSQTBuckets; ++k)
accumulator.psqtAccumulation[perspective][k] = 0;
accumulator.psqtAccumulation[Perspective][k] = 0;
for (const auto index : active)
{
const IndexType offset = HalfDimensions * index;
for (IndexType j = 0; j < HalfDimensions; ++j)
accumulator.accumulation[perspective][j] += weights[offset + j];
accumulator.accumulation[Perspective][j] += weights[offset + j];
for (std::size_t k = 0; k < PSQTBuckets; ++k)
accumulator.psqtAccumulation[perspective][k] += psqtWeights[index * PSQTBuckets + k];
accumulator.psqtAccumulation[Perspective][k] += psqtWeights[index * PSQTBuckets + k];
}
#endif
}

View File

@ -129,7 +129,7 @@ void Position::init() {
// Prepare the cuckoo tables
std::memset(cuckoo, 0, sizeof(cuckoo));
std::memset(cuckooMove, 0, sizeof(cuckooMove));
int count = 0;
[[maybe_unused]] int count = 0;
for (Piece pc : Pieces)
for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1)
for (Square s2 = Square(s1 + 1); s2 <= SQ_H8; ++s2)
@ -1054,7 +1054,10 @@ Key Position::key_after(Move m) const {
if (captured)
k ^= Zobrist::psq[captured][to];
return k ^ Zobrist::psq[pc][to] ^ Zobrist::psq[pc][from];
k ^= Zobrist::psq[pc][to] ^ Zobrist::psq[pc][from];
return (captured || type_of(pc) == PAWN)
? k : adjust_key50<true>(k);
}
@ -1099,10 +1102,12 @@ bool Position::see_ge(Move m, Value threshold) const {
// Don't allow pinned pieces to attack as long as there are
// pinners on their original square.
if (pinners(~stm) & occupied)
{
stmAttackers &= ~blockers_for_king(stm);
if (!stmAttackers)
break;
if (!stmAttackers)
break;
}
res ^= 1;

View File

@ -161,6 +161,7 @@ public:
bool has_repeated() const;
int rule50_count() const;
Score psq_score() const;
Value psq_eg_stm() const;
Value non_pawn_material(Color c) const;
Value non_pawn_material() const;
@ -184,6 +185,8 @@ private:
void move_piece(Square from, Square to);
template<bool Do>
void do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto);
template<bool AfterMove>
Key adjust_key50(Key k) const;
// Data members
Piece board[SQUARE_NB];
@ -326,8 +329,14 @@ inline int Position::pawns_on_same_color_squares(Color c, Square s) const {
}
inline Key Position::key() const {
return st->rule50 < 14 ? st->key
: st->key ^ make_key((st->rule50 - 14) / 8);
return adjust_key50<false>(st->key);
}
template<bool AfterMove>
inline Key Position::adjust_key50(Key k) const
{
return st->rule50 < 14 - AfterMove
? k : k ^ make_key((st->rule50 - (14 - AfterMove)) / 8);
}
inline Key Position::pawn_key() const {
@ -342,6 +351,10 @@ inline Score Position::psq_score() const {
return psq;
}
inline Value Position::psq_eg_stm() const {
return (sideToMove == WHITE ? 1 : -1) * eg_value(psq);
}
inline Value Position::non_pawn_material(Color c) const {
return st->nonPawnMaterial[c];
}

View File

@ -63,7 +63,7 @@ namespace {
// Futility margin
Value futility_margin(Depth d, bool improving) {
return Value(168 * (d - improving));
return Value(165 * (d - improving));
}
// Reductions lookup table, initialized at startup
@ -71,21 +71,22 @@ namespace {
Depth reduction(bool i, Depth d, int mn, Value delta, Value rootDelta) {
int r = Reductions[d] * Reductions[mn];
return (r + 1463 - int(delta) * 1024 / int(rootDelta)) / 1024 + (!i && r > 1010);
return (r + 1642 - int(delta) * 1024 / int(rootDelta)) / 1024 + (!i && r > 916);
}
constexpr int futility_move_count(bool improving, Depth depth) {
return (3 + depth * depth) / (2 - improving);
return improving ? (3 + depth * depth)
: (3 + depth * depth) / 2;
}
// History and stats update bonus, based on depth
int stat_bonus(Depth d) {
return std::min((9 * d + 270) * d - 311 , 2145);
return std::min((12 * d + 282) * d - 349 , 1594);
}
// Add a small random component to draw evaluations to avoid 3-fold blindness
Value value_draw(Thread* thisThread) {
return VALUE_DRAW + Value(2 * (thisThread->nodes & 1) - 1);
Value value_draw(const Thread* thisThread) {
return VALUE_DRAW - 1 + Value(thisThread->nodes & 0x2);
}
// Skill structure is used to implement strength limit. If we have an uci_elo then
@ -115,7 +116,7 @@ namespace {
Value value_to_tt(Value v, int ply);
Value value_from_tt(Value v, int ply, int r50c);
void update_pv(Move* pv, Move move, Move* childPv);
void update_pv(Move* pv, Move move, const Move* childPv);
void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus);
void update_quiet_stats(const Position& pos, Stack* ss, Move move, int bonus);
void update_all_stats(const Position& pos, Stack* ss, Move bestMove, Value bestValue, Value beta, Square prevSq,
@ -157,7 +158,7 @@ namespace {
void Search::init() {
for (int i = 1; i < MAX_MOVES; ++i)
Reductions[i] = int((20.81 + std::log(Threads.size()) / 2) * std::log(i));
Reductions[i] = int((20.26 + std::log(Threads.size()) / 2) * std::log(i));
}
@ -238,9 +239,12 @@ void MainThread::search() {
bestPreviousScore = bestThread->rootMoves[0].score;
bestPreviousAverageScore = bestThread->rootMoves[0].averageScore;
for (Thread* th : Threads)
th->previousDepth = bestThread->completedDepth;
// Send again PV info if we have a new best thread
if (bestThread != this)
sync_cout << UCI::pv(bestThread->rootPos, bestThread->completedDepth, -VALUE_INFINITE, VALUE_INFINITE) << sync_endl;
sync_cout << UCI::pv(bestThread->rootPos, bestThread->completedDepth) << sync_endl;
sync_cout << "bestmove " << UCI::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960());
@ -303,11 +307,9 @@ void Thread::search() {
multiPV = std::min(multiPV, rootMoves.size());
complexityAverage.set(202, 1);
complexityAverage.set(155, 1);
trend = SCORE_ZERO;
optimism[ us] = Value(39);
optimism[~us] = -optimism[us];
optimism[us] = optimism[~us] = VALUE_ZERO;
int searchAgainCounter = 0;
@ -349,16 +351,12 @@ void Thread::search() {
if (rootDepth >= 4)
{
Value prev = rootMoves[pvIdx].averageScore;
delta = Value(16) + int(prev) * prev / 19178;
delta = Value(10) + int(prev) * prev / 15620;
alpha = std::max(prev - delta,-VALUE_INFINITE);
beta = std::min(prev + delta, VALUE_INFINITE);
// Adjust trend and optimism based on root move's previousScore
int tr = sigmoid(prev, 3, 8, 90, 125, 1);
trend = (us == WHITE ? make_score(tr, tr / 2)
: -make_score(tr, tr / 2));
int opt = sigmoid(prev, 8, 17, 144, 13966, 183);
// Adjust optimism based on root move's previousScore
int opt = 118 * prev / (std::abs(prev) + 169);
optimism[ us] = Value(opt);
optimism[~us] = -optimism[us];
}
@ -369,7 +367,9 @@ void Thread::search() {
int failedHighCnt = 0;
while (true)
{
Depth adjustedDepth = std::max(1, rootDepth - failedHighCnt - searchAgainCounter);
// Adjust the effective depth searched, but ensuring at least one effective increment for every
// four searchAgain steps (see issue #2717).
Depth adjustedDepth = std::max(1, rootDepth - failedHighCnt - 3 * (searchAgainCounter + 1) / 4);
bestValue = Stockfish::search<Root>(rootPos, ss, alpha, beta, adjustedDepth, false);
// Bring the best move to the front. It is critical that sorting
@ -392,7 +392,7 @@ void Thread::search() {
&& multiPV == 1
&& (bestValue <= alpha || bestValue >= beta)
&& Time.elapsed() > 3000)
sync_cout << UCI::pv(rootPos, rootDepth, alpha, beta) << sync_endl;
sync_cout << UCI::pv(rootPos, rootDepth) << sync_endl;
// In case of failing low/high increase aspiration window and
// re-search, otherwise exit the loop.
@ -423,7 +423,7 @@ void Thread::search() {
if ( mainThread
&& (Threads.stop || pvIdx + 1 == multiPV || Time.elapsed() > 3000))
sync_cout << UCI::pv(rootPos, rootDepth, alpha, beta) << sync_endl;
sync_cout << UCI::pv(rootPos, rootDepth) << sync_endl;
}
if (!Threads.stop)
@ -459,17 +459,16 @@ void Thread::search() {
&& !Threads.stop
&& !mainThread->stopOnPonderhit)
{
double fallingEval = (69 + 12 * (mainThread->bestPreviousAverageScore - bestValue)
+ 6 * (mainThread->iterValue[iterIdx] - bestValue)) / 781.4;
double fallingEval = (71 + 12 * (mainThread->bestPreviousAverageScore - bestValue)
+ 6 * (mainThread->iterValue[iterIdx] - bestValue)) / 656.7;
fallingEval = std::clamp(fallingEval, 0.5, 1.5);
// If the bestMove is stable over several iterations, reduce time accordingly
timeReduction = lastBestMoveDepth + 10 < completedDepth ? 1.63 : 0.73;
double reduction = (1.56 + mainThread->previousTimeReduction) / (2.20 * timeReduction);
double bestMoveInstability = 1.073 + std::max(1.0, 2.25 - 9.9 / rootDepth)
* totBestMoveChanges / Threads.size();
timeReduction = lastBestMoveDepth + 9 < completedDepth ? 1.37 : 0.65;
double reduction = (1.4 + mainThread->previousTimeReduction) / (2.15 * timeReduction);
double bestMoveInstability = 1 + 1.7 * totBestMoveChanges / Threads.size();
int complexity = mainThread->complexityAverage.value();
double complexPosition = std::clamp(1.0 + (complexity - 326) / 1618.1, 0.5, 1.5);
double complexPosition = std::min(1.0 + (complexity - 261) / 1738.7, 1.5);
double totalTime = Time.optimum() * fallingEval * reduction * bestMoveInstability * complexPosition;
@ -490,7 +489,7 @@ void Thread::search() {
}
else if ( Threads.increaseDepth
&& !mainThread->ponder
&& Time.elapsed() > totalTime * 0.43)
&& Time.elapsed() > totalTime * 0.53)
Threads.increaseDepth = false;
else
Threads.increaseDepth = true;
@ -553,18 +552,17 @@ namespace {
Move ttMove, move, excludedMove, bestMove;
Depth extension, newDepth;
Value bestValue, value, ttValue, eval, maxValue, probCutBeta;
bool givesCheck, improving, didLMR, priorCapture;
bool capture, doFullDepthSearch, moveCountPruning, ttCapture;
bool givesCheck, improving, priorCapture, singularQuietLMR;
bool capture, moveCountPruning, ttCapture;
Piece movedPiece;
int moveCount, captureCount, quietCount, bestMoveCount, improvement, complexity;
int moveCount, captureCount, quietCount, improvement, complexity;
// Step 1. Initialize node
Thread* thisThread = pos.this_thread();
thisThread->depth = depth;
ss->inCheck = pos.checkers();
priorCapture = pos.captured_piece();
Color us = pos.side_to_move();
moveCount = bestMoveCount = captureCount = quietCount = ss->moveCount = 0;
moveCount = captureCount = quietCount = ss->moveCount = 0;
bestValue = -VALUE_INFINITE;
maxValue = VALUE_INFINITE;
@ -604,8 +602,8 @@ namespace {
(ss+1)->ttPv = false;
(ss+1)->excludedMove = bestMove = MOVE_NONE;
(ss+2)->killers[0] = (ss+2)->killers[1] = MOVE_NONE;
(ss+2)->cutoffCnt = 0;
ss->doubleExtensions = (ss-1)->doubleExtensions;
ss->depth = depth;
Square prevSq = to_sq((ss-1)->currentMove);
// Initialize statScore to zero for the grandchildren of the current position.
@ -632,10 +630,9 @@ namespace {
// At non-PV nodes we check for an early TT cutoff
if ( !PvNode
&& ss->ttHit
&& tte->depth() > depth - (thisThread->id() % 2 == 1)
&& tte->depth() > depth - (tte->bound() == BOUND_EXACT)
&& ttValue != VALUE_NONE // Possible in case of TT access race
&& (ttValue >= beta ? (tte->bound() & BOUND_LOWER)
: (tte->bound() & BOUND_UPPER)))
&& (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER)))
{
// If ttMove is quiet, update move sorting heuristics on TT hit (~1 Elo)
if (ttMove)
@ -734,11 +731,9 @@ namespace {
// Never assume anything about values stored in TT
ss->staticEval = eval = tte->eval();
if (eval == VALUE_NONE)
ss->staticEval = eval = evaluate(pos);
// Randomize draw evaluation
if (eval == VALUE_DRAW)
eval = value_draw(thisThread);
ss->staticEval = eval = evaluate(pos, &complexity);
else // Fall back to (semi)classical complexity for TT hits, the NNUE complexity is lost
complexity = abs(ss->staticEval - pos.psq_eg_stm());
// ttValue can be used as a better position evaluation (~4 Elo)
if ( ttValue != VALUE_NONE
@ -747,17 +742,19 @@ namespace {
}
else
{
ss->staticEval = eval = evaluate(pos);
ss->staticEval = eval = evaluate(pos, &complexity);
// Save static evaluation into transposition table
if (!excludedMove)
tte->save(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_NONE, MOVE_NONE, eval);
}
thisThread->complexityAverage.update(complexity);
// Use static evaluation difference to improve quiet move ordering (~3 Elo)
if (is_ok((ss-1)->currentMove) && !(ss-1)->inCheck && !priorCapture)
{
int bonus = std::clamp(-16 * int((ss-1)->staticEval + ss->staticEval), -2000, 2000);
int bonus = std::clamp(-19 * int((ss-1)->staticEval + ss->staticEval), -1914, 1914);
thisThread->mainHistory[~us][from_to((ss-1)->currentMove)] << bonus;
}
@ -767,19 +764,13 @@ namespace {
// margin and the improving flag are used in various pruning heuristics.
improvement = (ss-2)->staticEval != VALUE_NONE ? ss->staticEval - (ss-2)->staticEval
: (ss-4)->staticEval != VALUE_NONE ? ss->staticEval - (ss-4)->staticEval
: 175;
: 168;
improving = improvement > 0;
complexity = abs(ss->staticEval - (us == WHITE ? eg_value(pos.psq_score()) : -eg_value(pos.psq_score())));
thisThread->complexityAverage.update(complexity);
// Step 7. Razoring.
// If eval is really low check with qsearch if it can exceed alpha, if it can't,
// return a fail low.
if ( !PvNode
&& depth <= 7
&& eval < alpha - 348 - 258 * depth * depth)
if (eval < alpha - 369 - 254 * depth * depth)
{
value = qsearch<NonPV>(pos, ss, alpha - 1, alpha);
if (value < alpha)
@ -790,18 +781,18 @@ namespace {
// The depth condition is important for mate finding.
if ( !ss->ttPv
&& depth < 8
&& eval - futility_margin(depth, improving) - (ss-1)->statScore / 256 >= beta
&& eval - futility_margin(depth, improving) - (ss-1)->statScore / 303 >= beta
&& eval >= beta
&& eval < 26305) // larger than VALUE_KNOWN_WIN, but smaller than TB wins.
&& eval < 28031) // larger than VALUE_KNOWN_WIN, but smaller than TB wins
return eval;
// Step 9. Null move search with verification search (~22 Elo)
if ( !PvNode
&& (ss-1)->currentMove != MOVE_NULL
&& (ss-1)->statScore < 14695
&& (ss-1)->statScore < 17139
&& eval >= beta
&& eval >= ss->staticEval
&& ss->staticEval >= beta - 15 * depth - improvement / 15 + 198 + complexity / 28
&& ss->staticEval >= beta - 20 * depth - improvement / 13 + 233 + complexity / 25
&& !excludedMove
&& pos.non_pawn_material(us)
&& (ss->ply >= thisThread->nmpMinPly || us != thisThread->nmpColor))
@ -809,7 +800,7 @@ namespace {
assert(eval - beta >= 0);
// Null move dynamic reduction based on depth, eval and complexity of position
Depth R = std::min(int(eval - beta) / 147, 5) + depth / 3 + 4 - (complexity > 753);
Depth R = std::min(int(eval - beta) / 168, 7) + depth / 3 + 4 - (complexity > 861);
ss->currentMove = MOVE_NULL;
ss->continuationHistory = &thisThread->continuationHistory[0][0][NO_PIECE][0];
@ -845,7 +836,7 @@ namespace {
}
}
probCutBeta = beta + 179 - 46 * improving;
probCutBeta = beta + 191 - 54 * improving;
// Step 10. ProbCut (~4 Elo)
// If we have a good enough capture and a reduced search returns a value
@ -864,21 +855,16 @@ namespace {
{
assert(probCutBeta < VALUE_INFINITE);
MovePicker mp(pos, ttMove, probCutBeta - ss->staticEval, depth - 3, &captureHistory);
bool ttPv = ss->ttPv;
bool captureOrPromotion;
ss->ttPv = false;
MovePicker mp(pos, ttMove, probCutBeta - ss->staticEval, &captureHistory);
while ((move = mp.next_move()) != MOVE_NONE)
if (move != excludedMove && pos.legal(move))
{
assert(pos.capture(move) || promotion_type(move) == QUEEN);
captureOrPromotion = true;
ss->currentMove = move;
ss->continuationHistory = &thisThread->continuationHistory[ss->inCheck]
[captureOrPromotion]
[true]
[pos.moved_piece(move)]
[to_sq(move)];
@ -895,34 +881,31 @@ namespace {
if (value >= probCutBeta)
{
// if transposition table doesn't have equal or more deep info write probCut data into it
if ( !(ss->ttHit
&& tte->depth() >= depth - 3
&& ttValue != VALUE_NONE))
tte->save(posKey, value_to_tt(value, ss->ply), ttPv,
BOUND_LOWER,
depth - 3, move, ss->staticEval);
// Save ProbCut data into transposition table
tte->save(posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER, depth - 3, move, ss->staticEval);
return value;
}
}
ss->ttPv = ttPv;
}
// Step 11. If the position is not in TT, decrease depth by 2 or 1 depending on node type (~3 Elo)
if ( PvNode
&& depth >= 3
// Step 11. If the position is not in TT, decrease depth by 3.
// Use qsearch if depth is equal or below zero (~4 Elo)
if ( PvNode
&& !ttMove)
depth -= 3;
if (depth <= 0)
return qsearch<PV>(pos, ss, alpha, beta);
if ( cutNode
&& depth >= 9
&& !ttMove)
depth -= 2;
if ( cutNode
&& depth >= 8
&& !ttMove)
depth--;
moves_loop: // When in check, search starts here
// Step 12. A small Probcut idea, when we are in check (~0 Elo)
probCutBeta = beta + 481;
probCutBeta = beta + 417;
if ( ss->inCheck
&& !PvNode
&& depth >= 2
@ -949,7 +932,7 @@ moves_loop: // When in check, search starts here
ss->killers);
value = bestValue;
moveCountPruning = false;
moveCountPruning = singularQuietLMR = false;
// Indicate PvNodes that will probably fail low if the node was searched
// at a depth equal or greater than the current depth, and the result of this search was a fail low.
@ -1013,17 +996,16 @@ moves_loop: // When in check, search starts here
|| givesCheck)
{
// Futility pruning for captures (~0 Elo)
if ( !pos.empty(to_sq(move))
&& !givesCheck
if ( !givesCheck
&& !PvNode
&& lmrDepth < 6
&& lmrDepth < 7
&& !ss->inCheck
&& ss->staticEval + 281 + 179 * lmrDepth + PieceValue[EG][pos.piece_on(to_sq(move))]
&& ss->staticEval + 180 + 201 * lmrDepth + PieceValue[EG][pos.piece_on(to_sq(move))]
+ captureHistory[movedPiece][to_sq(move)][type_of(pos.piece_on(to_sq(move)))] / 6 < alpha)
continue;
// SEE based pruning (~9 Elo)
if (!pos.see_ge(move, Value(-203) * depth))
if (!pos.see_ge(move, Value(-222) * depth))
continue;
}
else
@ -1037,16 +1019,16 @@ moves_loop: // When in check, search starts here
&& history < -3875 * (depth - 1))
continue;
history += thisThread->mainHistory[us][from_to(move)];
history += 2 * thisThread->mainHistory[us][from_to(move)];
// Futility pruning: parent node (~9 Elo)
if ( !ss->inCheck
&& lmrDepth < 11
&& ss->staticEval + 122 + 138 * lmrDepth + history / 60 <= alpha)
&& lmrDepth < 13
&& ss->staticEval + 106 + 145 * lmrDepth + history / 52 <= alpha)
continue;
// Prune moves with negative SEE (~3 Elo)
if (!pos.see_ge(move, Value(-25 * lmrDepth * lmrDepth - 20 * lmrDepth)))
if (!pos.see_ge(move, Value(-24 * lmrDepth * lmrDepth - 15 * lmrDepth)))
continue;
}
}
@ -1061,7 +1043,7 @@ moves_loop: // When in check, search starts here
// a reduced search on all the other moves but the ttMove and if the
// result is lower than ttValue minus a margin, then we will extend the ttMove.
if ( !rootNode
&& depth >= 4 + 2 * (PvNode && tte->is_pv())
&& depth >= 4 - (thisThread->previousDepth > 24) + 2 * (PvNode && tte->is_pv())
&& move == ttMove
&& !excludedMove // Avoid recursive singular search
/* && ttValue != VALUE_NONE Already implicit in the next condition */
@ -1069,7 +1051,7 @@ moves_loop: // When in check, search starts here
&& (tte->bound() & BOUND_LOWER)
&& tte->depth() >= depth - 3)
{
Value singularBeta = ttValue - 3 * depth;
Value singularBeta = ttValue - (3 + (ss->ttPv && !PvNode)) * depth;
Depth singularDepth = (depth - 1) / 2;
ss->excludedMove = move;
@ -1079,11 +1061,12 @@ moves_loop: // When in check, search starts here
if (value < singularBeta)
{
extension = 1;
singularQuietLMR = !ttCapture;
// Avoid search explosion by limiting the number of double extensions
if ( !PvNode
&& value < singularBeta - 26
&& ss->doubleExtensions <= 8)
&& value < singularBeta - 25
&& ss->doubleExtensions <= 9)
extension = 2;
}
@ -1098,19 +1081,23 @@ moves_loop: // When in check, search starts here
// If the eval of ttMove is greater than beta, we reduce it (negative extension)
else if (ttValue >= beta)
extension = -2;
// If the eval of ttMove is less than alpha and value, we reduce it (negative extension)
else if (ttValue <= alpha && ttValue <= value)
extension = -1;
}
// Check extensions (~1 Elo)
else if ( givesCheck
&& depth > 9
&& abs(ss->staticEval) > 71)
&& abs(ss->staticEval) > 82)
extension = 1;
// Quiet ttMove extensions (~0 Elo)
else if ( PvNode
&& move == ttMove
&& move == ss->killers[0]
&& (*contHist[0])[movedPiece][to_sq(move)] >= 5491)
&& (*contHist[0])[movedPiece][to_sq(move)] >= 5177)
extension = 1;
}
@ -1131,8 +1118,6 @@ moves_loop: // When in check, search starts here
// Step 16. Make the move
pos.do_move(move, st, givesCheck);
bool doDeeperSearch = false;
// Step 17. Late moves reduction / extension (LMR, ~98 Elo)
// We use various heuristics for the sons of a node after the first son has
// been searched. In general we would like to reduce them, but there are many
@ -1145,11 +1130,6 @@ moves_loop: // When in check, search starts here
{
Depth r = reduction(improving, depth, moveCount, delta, thisThread->rootDelta);
// Decrease reduction at some PvNodes (~2 Elo)
if ( PvNode
&& bestMoveCount <= 3)
r--;
// Decrease reduction if position is or has been on the PV
// and node is not likely to fail low. (~3 Elo)
if ( ss->ttPv
@ -1161,63 +1141,59 @@ moves_loop: // When in check, search starts here
r--;
// Increase reduction for cut nodes (~3 Elo)
if (cutNode && move != ss->killers[0])
if (cutNode)
r += 2;
// Increase reduction if ttMove is a capture (~3 Elo)
if (ttCapture)
r++;
// Decrease reduction at PvNodes if bestvalue
// is vastly different from static evaluation
if (PvNode && !ss->inCheck && abs(ss->staticEval - bestValue) > 250)
// Decrease reduction for PvNodes based on depth
if (PvNode)
r -= 1 + 11 / (3 + depth);
// Decrease reduction if ttMove has been singularly extended (~1 Elo)
if (singularQuietLMR)
r--;
// Increase depth based reduction if PvNode
if (PvNode)
r -= 15 / ( 3 + depth );
// Decrease reduction if we move a threatened piece (~1 Elo)
if ( depth > 9
&& (mp.threatenedPieces & from_sq(move)))
r--;
ss->statScore = thisThread->mainHistory[us][from_to(move)]
// Increase reduction if next ply has a lot of fail high
if ((ss+1)->cutoffCnt > 3)
r++;
ss->statScore = 2 * thisThread->mainHistory[us][from_to(move)]
+ (*contHist[0])[movedPiece][to_sq(move)]
+ (*contHist[1])[movedPiece][to_sq(move)]
+ (*contHist[3])[movedPiece][to_sq(move)]
- 4334;
- 4433;
// Decrease/increase reduction for moves with a good/bad history (~30 Elo)
r -= ss->statScore / 15914;
r -= ss->statScore / (13628 + 4000 * (depth > 7 && depth < 19));
// In general we want to cap the LMR depth search at newDepth. But if reductions
// are really negative and movecount is low, we allow this move to be searched
// deeper than the first move (this may lead to hidden double extensions).
int deeper = r >= -1 ? 0
: moveCount <= 4 ? 2
: PvNode && depth > 4 ? 1
: cutNode && moveCount <= 8 ? 1
: 0;
Depth d = std::clamp(newDepth - r, 1, newDepth + deeper);
// In general we want to cap the LMR depth search at newDepth, but when
// reduction is negative, we allow this move a limited search extension
// beyond the first move depth. This may lead to hidden double extensions.
Depth d = std::clamp(newDepth - r, 1, newDepth + 1);
value = -search<NonPV>(pos, ss+1, -(alpha+1), -alpha, d, true);
// If the son is reduced and fails high it will be re-searched at full depth
doFullDepthSearch = value > alpha && d < newDepth;
doDeeperSearch = value > (alpha + 78 + 11 * (newDepth - d));
didLMR = true;
}
else
{
doFullDepthSearch = !PvNode || moveCount > 1;
didLMR = false;
}
// Step 18. Full depth search when LMR is skipped or fails high
if (doFullDepthSearch)
{
value = -search<NonPV>(pos, ss+1, -(alpha+1), -alpha, newDepth + doDeeperSearch, !cutNode);
// If the move passed LMR update its stats
if (didLMR)
// Do full depth search when reduced LMR search fails high
if (value > alpha && d < newDepth)
{
// Adjust full depth search based on LMR results - if result
// was good enough search deeper, if it was bad enough search shallower
const bool doDeeperSearch = value > (alpha + 64 + 11 * (newDepth - d));
const bool doShallowerSearch = value < bestValue + newDepth;
newDepth += doDeeperSearch - doShallowerSearch;
if (newDepth > d)
value = -search<NonPV>(pos, ss+1, -(alpha+1), -alpha, newDepth, !cutNode);
int bonus = value > alpha ? stat_bonus(newDepth)
: -stat_bonus(newDepth);
@ -1228,6 +1204,12 @@ moves_loop: // When in check, search starts here
}
}
// Step 18. Full depth search when LMR is skipped
else if (!PvNode || moveCount > 1)
{
value = -search<NonPV>(pos, ss+1, -(alpha+1), -alpha, newDepth, !cutNode);
}
// For PV nodes only, do a full PV search on the first move or after a fail
// high (in the latter case search only if value < beta), otherwise let the
// parent node fail low with value <= alpha and try another move.
@ -1264,6 +1246,8 @@ moves_loop: // When in check, search starts here
{
rm.score = value;
rm.selDepth = thisThread->selDepth;
rm.scoreLowerbound = value >= beta;
rm.scoreUpperbound = value <= alpha;
rm.pv.resize(1);
assert((ss+1)->pv);
@ -1299,16 +1283,26 @@ moves_loop: // When in check, search starts here
if (PvNode && value < beta) // Update alpha! Always alpha < beta
{
alpha = value;
bestMoveCount++;
// Reduce other moves if we have found at least one score improvement
if ( depth > 1
&& depth < 6
&& beta < VALUE_KNOWN_WIN
&& alpha > -VALUE_KNOWN_WIN)
depth -= 1;
assert(depth > 0);
}
else
{
ss->cutoffCnt++;
assert(value >= beta); // Fail high
break;
}
}
}
// If the move is worse than some previously searched move, remember it to update its stats later
if (move != bestMove)
{
@ -1346,14 +1340,14 @@ moves_loop: // When in check, search starts here
quietsSearched, quietCount, capturesSearched, captureCount, depth);
// Bonus for prior countermove that caused the fail low
else if ( (depth >= 4 || PvNode)
else if ( (depth >= 5 || PvNode)
&& !priorCapture)
{
//Assign extra bonus if current node is PvNode or cutNode
//or fail low was really bad
bool extraBonus = PvNode
|| cutNode
|| bestValue < alpha - 70 * depth;
|| bestValue < alpha - 62 * depth;
update_continuation_histories(ss-1, pos.piece_on(prevSq), prevSq, stat_bonus(depth) * (1 + extraBonus));
}
@ -1381,6 +1375,7 @@ moves_loop: // When in check, search starts here
// qsearch() is the quiescence search function, which is called by the main search
// function with zero depth, or recursively with further decreasing depth per call.
// (~155 elo)
template <NodeType nodeType>
Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
@ -1437,8 +1432,7 @@ moves_loop: // When in check, search starts here
&& ss->ttHit
&& tte->depth() >= ttDepth
&& ttValue != VALUE_NONE // Only in case of TT access race
&& (ttValue >= beta ? (tte->bound() & BOUND_LOWER)
: (tte->bound() & BOUND_UPPER)))
&& (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER)))
return ttValue;
// Evaluate the position statically
@ -1480,7 +1474,7 @@ moves_loop: // When in check, search starts here
if (PvNode && bestValue > alpha)
alpha = bestValue;
futilityBase = bestValue + 118;
futilityBase = bestValue + 153;
}
const PieceToHistory* contHist[] = { (ss-1)->continuationHistory, (ss-2)->continuationHistory,
@ -1554,18 +1548,17 @@ moves_loop: // When in check, search starts here
[to_sq(move)];
// Continuation history based pruning (~2 Elo)
if ( !capture
if ( !capture
&& bestValue > VALUE_TB_LOSS_IN_MAX_PLY
&& (*contHist[0])[pos.moved_piece(move)][to_sq(move)] < CounterMovePruneThreshold
&& (*contHist[1])[pos.moved_piece(move)][to_sq(move)] < CounterMovePruneThreshold)
&& (*contHist[0])[pos.moved_piece(move)][to_sq(move)] < 0
&& (*contHist[1])[pos.moved_piece(move)][to_sq(move)] < 0)
continue;
// movecount pruning for quiet check evasions
if ( bestValue > VALUE_TB_LOSS_IN_MAX_PLY
&& quietCheckEvasions > 1
&& !capture
&& ss->inCheck)
continue;
// We prune after 2nd quiet check evasion where being 'in check' is implicitly checked through the counter
// and being a 'quiet' apart from being a tt move is assumed after an increment because captures are pushed ahead.
if ( bestValue > VALUE_TB_LOSS_IN_MAX_PLY
&& quietCheckEvasions > 1)
break;
quietCheckEvasions += !capture && ss->inCheck;
@ -1662,7 +1655,7 @@ moves_loop: // When in check, search starts here
// update_pv() adds current move and appends child pv[]
void update_pv(Move* pv, Move move, Move* childPv) {
void update_pv(Move* pv, Move move, const Move* childPv) {
for (*pv++ = move; childPv && *childPv != MOVE_NONE; )
*pv++ = *childPv++;
@ -1675,19 +1668,18 @@ moves_loop: // When in check, search starts here
void update_all_stats(const Position& pos, Stack* ss, Move bestMove, Value bestValue, Value beta, Square prevSq,
Move* quietsSearched, int quietCount, Move* capturesSearched, int captureCount, Depth depth) {
int bonus1, bonus2;
Color us = pos.side_to_move();
Thread* thisThread = pos.this_thread();
CapturePieceToHistory& captureHistory = thisThread->captureHistory;
Piece moved_piece = pos.moved_piece(bestMove);
PieceType captured = type_of(pos.piece_on(to_sq(bestMove)));
bonus1 = stat_bonus(depth + 1);
bonus2 = bestValue > beta + PawnValueMg ? bonus1 // larger bonus
: stat_bonus(depth); // smaller bonus
int bonus1 = stat_bonus(depth + 1);
if (!pos.capture(bestMove))
{
int bonus2 = bestValue > beta + 137 ? bonus1 // larger bonus
: stat_bonus(depth); // smaller bonus
// Increase stats for the best move in case it was a quiet move
update_quiet_stats(pos, ss, bestMove, bonus2);
@ -1830,7 +1822,7 @@ void MainThread::check_time() {
/// UCI::pv() formats PV information according to the UCI protocol. UCI requires
/// that all (if any) unsearched PV lines are sent using a previous search score.
string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) {
string UCI::pv(const Position& pos, Depth depth) {
std::stringstream ss;
TimePoint elapsed = Time.elapsed() + 1;
@ -1868,16 +1860,13 @@ string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) {
if (Options["UCI_ShowWDL"])
ss << UCI::wdl(v, pos.game_ply());
if (!tb && i == pvIdx)
ss << (v >= beta ? " lowerbound" : v <= alpha ? " upperbound" : "");
if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact
ss << (rootMoves[i].scoreLowerbound ? " lowerbound" : (rootMoves[i].scoreUpperbound ? " upperbound" : ""));
ss << " nodes " << nodesSearched
<< " nps " << nodesSearched * 1000 / elapsed;
if (elapsed > 1000) // Earlier makes little sense
ss << " hashfull " << TT.hashfull();
ss << " tbhits " << tbHits
<< " nps " << nodesSearched * 1000 / elapsed
<< " hashfull " << TT.hashfull()
<< " tbhits " << tbHits
<< " time " << elapsed
<< " pv";

View File

@ -31,9 +31,6 @@ class Position;
namespace Search {
/// Threshold used for countermoves based pruning
constexpr int CounterMovePruneThreshold = 0;
/// Stack struct keeps track of the information we need to remember from nodes
/// shallower and deeper in the tree during the search. Each search thread has
@ -47,13 +44,13 @@ struct Stack {
Move excludedMove;
Move killers[2];
Value staticEval;
Depth depth;
int statScore;
int moveCount;
bool inCheck;
bool ttPv;
bool ttHit;
int doubleExtensions;
int cutoffCnt;
};
@ -74,6 +71,8 @@ struct RootMove {
Value score = -VALUE_INFINITE;
Value previousScore = -VALUE_INFINITE;
Value averageScore = -VALUE_INFINITE;
bool scoreLowerbound = false;
bool scoreUpperbound = false;
int selDepth = 0;
int tbRank = 0;
Value tbScore;

View File

@ -59,6 +59,7 @@ namespace Stockfish {
namespace {
constexpr int TBPIECES = 7; // Max number of supported pieces
constexpr int MAX_DTZ = 1 << 18; // Max DTZ supported, large enough to deal with the syzygy TB limit.
enum { BigEndian, LittleEndian };
enum TBType { WDL, DTZ }; // Used as template parameter
@ -472,8 +473,6 @@ TBTables TBTables;
// If the corresponding file exists two new objects TBTable<WDL> and TBTable<DTZ>
// are created and added to the lists and hash table. Called at init time.
void TBTables::add(const std::vector<PieceType>& pieces) {
if (sizeof(char*) < 8 && pieces.size() >= 6)
return; // Not enough address space to support 6-men TB on 32-bit OS
std::string code;
@ -1292,7 +1291,7 @@ void Tablebases::init(const std::string& paths) {
for (auto s : diagonal)
MapA1D1D4[s] = code++;
// MapKK[] encodes all the 461 possible legal positions of two kings where
// MapKK[] encodes all the 462 possible legal positions of two kings where
// the first is in the a1-d1-d4 triangle. If the first king is on the a1-d4
// diagonal, the other one shall not to be above the a1-h8 diagonal.
std::vector<std::pair<int, Square>> bothOnDiagonal;
@ -1524,7 +1523,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) {
// Check whether a position was repeated since the last zeroing move.
bool rep = pos.has_repeated();
int dtz, bound = Options["Syzygy50MoveRule"] ? 900 : 1;
int dtz, bound = Options["Syzygy50MoveRule"] ? (MAX_DTZ - 100) : 1;
// Probe and rank each move
for (auto& m : rootMoves)
@ -1567,8 +1566,8 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) {
// Better moves are ranked higher. Certain wins are ranked equally.
// Losing moves are ranked equally unless a 50-move draw is in sight.
int r = dtz > 0 ? (dtz + cnt50 <= 99 && !rep ? 1000 : 1000 - (dtz + cnt50))
: dtz < 0 ? (-dtz * 2 + cnt50 < 100 ? -1000 : -1000 + (-dtz + cnt50))
int r = dtz > 0 ? (dtz + cnt50 <= 99 && !rep ? MAX_DTZ : MAX_DTZ - (dtz + cnt50))
: dtz < 0 ? (-dtz * 2 + cnt50 < 100 ? -MAX_DTZ : -MAX_DTZ + (-dtz + cnt50))
: 0;
m.tbRank = r;
@ -1576,9 +1575,9 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) {
// 1 cp to cursed wins and let it grow to 49 cp as the positions gets
// closer to a real win.
m.tbScore = r >= bound ? VALUE_MATE - MAX_PLY - 1
: r > 0 ? Value((std::max( 3, r - 800) * int(PawnValueEg)) / 200)
: r > 0 ? Value((std::max( 3, r - (MAX_DTZ - 200)) * int(PawnValueEg)) / 200)
: r == 0 ? VALUE_DRAW
: r > -bound ? Value((std::min(-3, r + 800) * int(PawnValueEg)) / 200)
: r > -bound ? Value((std::min(-3, r + (MAX_DTZ - 200)) * int(PawnValueEg)) / 200)
: -VALUE_MATE + MAX_PLY + 1;
}
@ -1592,7 +1591,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) {
// A return value false indicates that not all probes were successful.
bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves) {
static const int WDL_to_rank[] = { -1000, -899, 0, 899, 1000 };
static const int WDL_to_rank[] = { -MAX_DTZ, -MAX_DTZ + 101, 0, MAX_DTZ - 101, MAX_DTZ };
ProbeState result;
StateInfo st;

View File

@ -31,8 +31,6 @@ enum WDLScore {
WDLDraw = 0, // Draw
WDLCursedWin = 1, // Win, but draw under 50-move rule
WDLWin = 2, // Win
WDLScoreNone = -1000
};
// Possible states after a probing operation

View File

@ -60,15 +60,13 @@ void Thread::clear() {
counterMoves.fill(MOVE_NONE);
mainHistory.fill(0);
captureHistory.fill(0);
previousDepth = 0;
for (bool inCheck : { false, true })
for (StatsType c : { NoCaptures, Captures })
{
for (auto& to : continuationHistory[inCheck][c])
for (auto& h : to)
h->fill(-71);
continuationHistory[inCheck][c][NO_PIECE][0]->fill(Search::CounterMovePruneThreshold - 1);
}
for (auto& h : to)
h->fill(-71);
}
@ -223,11 +221,14 @@ Thread* ThreadPool::get_best_thread() const {
minScore = std::min(minScore, th->rootMoves[0].score);
// Vote according to score and depth, and select the best thread
for (Thread* th : *this)
{
votes[th->rootMoves[0].pv[0]] +=
(th->rootMoves[0].score - minScore + 14) * int(th->completedDepth);
auto thread_value = [minScore](Thread* th) {
return (th->rootMoves[0].score - minScore + 14) * int(th->completedDepth);
};
for (Thread* th : *this)
votes[th->rootMoves[0].pv[0]] += thread_value(th);
for (Thread* th : *this)
if (abs(bestThread->rootMoves[0].score) >= VALUE_TB_WIN_IN_MAX_PLY)
{
// Make sure we pick the shortest mate / TB conversion or stave off mate the longest
@ -236,9 +237,10 @@ Thread* ThreadPool::get_best_thread() const {
}
else if ( th->rootMoves[0].score >= VALUE_TB_WIN_IN_MAX_PLY
|| ( th->rootMoves[0].score > VALUE_TB_LOSS_IN_MAX_PLY
&& votes[th->rootMoves[0].pv[0]] > votes[bestThread->rootMoves[0].pv[0]]))
&& ( votes[th->rootMoves[0].pv[0]] > votes[bestThread->rootMoves[0].pv[0]]
|| ( votes[th->rootMoves[0].pv[0]] == votes[bestThread->rootMoves[0].pv[0]]
&& thread_value(th) > thread_value(bestThread)))))
bestThread = th;
}
return bestThread;
}

View File

@ -69,13 +69,12 @@ public:
Position rootPos;
StateInfo rootState;
Search::RootMoves rootMoves;
Depth rootDepth, completedDepth, depth;
Depth rootDepth, completedDepth, previousDepth;
Value rootDelta;
CounterMoveHistory counterMoves;
ButterflyHistory mainHistory;
CapturePieceToHistory captureHistory;
ContinuationHistory continuationHistory[2][2];
Score trend;
};

View File

@ -80,7 +80,7 @@ void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) {
// game time for the current move, so also cap to 20% of available game time.
if (limits.movestogo == 0)
{
optScale = std::min(0.0084 + std::pow(ply + 3.0, 0.5) * 0.0042,
optScale = std::min(0.0120 + std::pow(ply + 3.0, 0.45) * 0.0039,
0.2 * limits.time[us] / double(timeLeft))
* optExtra;
maxScale = std::min(7.0, 4.0 + ply / 12.0);

View File

@ -450,7 +450,7 @@ constexpr Square to_sq(Move m) {
}
constexpr int from_to(Move m) {
return m & 0xFFF;
return m & 0xFFF;
}
constexpr MoveType type_of(Move m) {

View File

@ -40,14 +40,14 @@ extern vector<string> setup_bench(const Position&, istream&);
namespace {
// FEN string of the initial position, normal chess
// FEN string for the initial position in standard chess
const char* StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
// position() is called when engine receives the "position" UCI command.
// The function sets up the position described in the given FEN string ("fen")
// or the starting position ("startpos") and then makes the moves given in the
// following move list ("moves").
// position() is called when the engine receives the "position" UCI command.
// It sets up the position that is described in the given FEN string ("fen") or
// the initial position ("startpos") and then makes the moves given in the following
// move list ("moves").
void position(Position& pos, istringstream& is, StateListPtr& states) {
@ -59,7 +59,7 @@ namespace {
if (token == "startpos")
{
fen = StartFEN;
is >> token; // Consume "moves" token if any
is >> token; // Consume the "moves" token, if any
}
else if (token == "fen")
while (is >> token && token != "moves")
@ -67,10 +67,10 @@ namespace {
else
return;
states = StateListPtr(new std::deque<StateInfo>(1)); // Drop old and create a new one
states = StateListPtr(new std::deque<StateInfo>(1)); // Drop the old state and create a new one
pos.set(fen, Options["UCI_Chess960"], &states->back(), Threads.main());
// Parse move list (if any)
// Parse the move list, if any
while (is >> token && (m = UCI::to_move(pos, token)) != MOVE_NONE)
{
states->emplace_back();
@ -78,8 +78,8 @@ namespace {
}
}
// trace_eval() prints the evaluation for the current position, consistent with the UCI
// options set so far.
// trace_eval() prints the evaluation of the current position, consistent with
// the UCI options set so far.
void trace_eval(Position& pos) {
@ -93,20 +93,20 @@ namespace {
}
// setoption() is called when engine receives the "setoption" UCI command. The
// function updates the UCI option ("name") to the given value ("value").
// setoption() is called when the engine receives the "setoption" UCI command.
// The function updates the UCI option ("name") to the given value ("value").
void setoption(istringstream& is) {
string token, name, value;
is >> token; // Consume "name" token
is >> token; // Consume the "name" token
// Read option name (can contain spaces)
// Read the option name (can contain spaces)
while (is >> token && token != "value")
name += (name.empty() ? "" : " ") + token;
// Read option value (can contain spaces)
// Read the option value (can contain spaces)
while (is >> token)
value += (value.empty() ? "" : " ") + token;
@ -117,9 +117,9 @@ namespace {
}
// go() is called when engine receives the "go" UCI command. The function sets
// the thinking time and other parameters from the input string, then starts
// the search.
// go() is called when the engine receives the "go" UCI command. The function
// sets the thinking time and other parameters from the input string, then starts
// with a search.
void go(Position& pos, istringstream& is, StateListPtr& states) {
@ -127,7 +127,7 @@ namespace {
string token;
bool ponderMode = false;
limits.startTime = now(); // As early as possible!
limits.startTime = now(); // The search starts as early as possible
while (is >> token)
if (token == "searchmoves") // Needs to be the last command on the line
@ -151,9 +151,9 @@ namespace {
}
// bench() is called when engine receives the "bench" command. Firstly
// a list of UCI commands is setup according to bench parameters, then
// it is run one by one printing a summary at the end.
// bench() is called when the engine receives the "bench" command.
// Firstly, a list of UCI commands is set up according to the bench
// parameters, then it is run one by one, printing a summary at the end.
void bench(Position& pos, istream& args, StateListPtr& states) {
@ -184,12 +184,12 @@ namespace {
}
else if (token == "setoption") setoption(is);
else if (token == "position") position(pos, is, states);
else if (token == "ucinewgame") { Search::clear(); elapsed = now(); } // Search::clear() may take some while
else if (token == "ucinewgame") { Search::clear(); elapsed = now(); } // Search::clear() may take a while
}
elapsed = now() - elapsed + 1; // Ensure positivity to avoid a 'divide by zero'
dbg_print(); // Just before exiting
dbg_print();
cerr << "\n==========================="
<< "\nTotal time (ms) : " << elapsed
@ -197,36 +197,40 @@ namespace {
<< "\nNodes/second : " << 1000 * nodes / elapsed << endl;
}
// The win rate model returns the probability (per mille) of winning given an eval
// and a game-ply. The model fits rather accurately the LTC fishtest statistics.
// The win rate model returns the probability of winning (in per mille units) given an
// eval and a game ply. It fits the LTC fishtest statistics rather accurately.
int win_rate_model(Value v, int ply) {
// The model captures only up to 240 plies, so limit input (and rescale)
// The model only captures up to 240 plies, so limit the input and then rescale
double m = std::min(240, ply) / 64.0;
// Coefficients of a 3rd order polynomial fit based on fishtest data
// for two parameters needed to transform eval to the argument of a
// logistic function.
double as[] = {-1.17202460e-01, 5.94729104e-01, 1.12065546e+01, 1.22606222e+02};
double bs[] = {-1.79066759, 11.30759193, -17.43677612, 36.47147479};
// The coefficients of a third-order polynomial fit is based on the fishtest data
// for two parameters that need to transform eval to the argument of a logistic
// function.
constexpr double as[] = { -0.58270499, 2.68512549, 15.24638015, 344.49745382};
constexpr double bs[] = { -2.65734562, 15.96509799, -20.69040836, 73.61029937 };
// Enforce that NormalizeToPawnValue corresponds to a 50% win rate at ply 64
static_assert(UCI::NormalizeToPawnValue == int(as[0] + as[1] + as[2] + as[3]));
double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3];
double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3];
// Transform eval to centipawns with limited range
double x = std::clamp(double(100 * v) / PawnValueEg, -2000.0, 2000.0);
// Transform the eval to centipawns with limited range
double x = std::clamp(double(v), -4000.0, 4000.0);
// Return win rate in per mille (rounded to nearest)
// Return the win rate in per mille units rounded to the nearest value
return int(0.5 + 1000 / (1 + std::exp((a - x) / b)));
}
} // namespace
/// UCI::loop() waits for a command from stdin, parses it and calls the appropriate
/// function. Also intercepts EOF from stdin to ensure gracefully exiting if the
/// GUI dies unexpectedly. When called with some command line arguments, e.g. to
/// run 'bench', once the command is executed the function returns immediately.
/// In addition to the UCI ones, also some additional debug commands are supported.
/// UCI::loop() waits for a command from the stdin, parses it and then calls the appropriate
/// function. It also intercepts an end-of-file (EOF) indication from the stdin to ensure a
/// graceful exit if the GUI dies unexpectedly. When called with some command-line arguments,
/// like running 'bench', the function returns immediately after the command is executed.
/// In addition to the UCI ones, some additional debug commands are also supported.
void UCI::loop(int argc, char* argv[]) {
@ -240,24 +244,24 @@ void UCI::loop(int argc, char* argv[]) {
cmd += std::string(argv[i]) + " ";
do {
if (argc == 1 && !getline(cin, cmd)) // Block here waiting for input or EOF
if (argc == 1 && !getline(cin, cmd)) // Wait for an input or an end-of-file (EOF) indication
cmd = "quit";
istringstream is(cmd);
token.clear(); // Avoid a stale if getline() returns empty or blank line
token.clear(); // Avoid a stale if getline() returns nothing or a blank line
is >> skipws >> token;
if ( token == "quit"
|| token == "stop")
Threads.stop = true;
// The GUI sends 'ponderhit' to tell us the user has played the expected move.
// So 'ponderhit' will be sent if we were told to ponder on the same move the
// user has played. We should continue searching but switch from pondering to
// normal search.
// The GUI sends 'ponderhit' to tell that the user has played the expected move.
// So, 'ponderhit' is sent if pondering was done on the same move that the user
// has played. The search should continue, but should also switch from pondering
// to the normal search.
else if (token == "ponderhit")
Threads.main()->ponder = false; // Switch to normal search
Threads.main()->ponder = false; // Switch to the normal search
else if (token == "uci")
sync_cout << "id name " << engine_info(true)
@ -270,8 +274,8 @@ void UCI::loop(int argc, char* argv[]) {
else if (token == "ucinewgame") Search::clear();
else if (token == "isready") sync_cout << "readyok" << sync_endl;
// Additional custom non-UCI commands, mainly for debugging.
// Do not use these commands during a search!
// Add custom non-UCI commands, mainly for debugging purposes.
// These commands must not be used during a search!
else if (token == "flip") pos.flip();
else if (token == "bench") bench(pos, is, states);
else if (token == "d") sync_cout << pos << sync_endl;
@ -285,19 +289,25 @@ void UCI::loop(int argc, char* argv[]) {
filename = f;
Eval::NNUE::save_eval(filename);
}
else if (token == "--help" || token == "help" || token == "--license" || token == "license")
sync_cout << "\nStockfish is a powerful chess engine for playing and analyzing."
"\nIt is released as free software licensed under the GNU GPLv3 License."
"\nStockfish is normally used with a graphical user interface (GUI) and implements"
"\nthe Universal Chess Interface (UCI) protocol to communicate with a GUI, an API, etc."
"\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme"
"\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n" << sync_endl;
else if (!token.empty() && token[0] != '#')
sync_cout << "Unknown command: " << cmd << sync_endl;
sync_cout << "Unknown command: '" << cmd << "'. Type help for more information." << sync_endl;
} while (token != "quit" && argc == 1); // Command line args are one-shot
} while (token != "quit" && argc == 1); // The command-line arguments are one-shot
}
/// UCI::value() converts a Value to a string suitable for use with the UCI
/// protocol specification:
/// UCI::value() converts a Value to a string by adhering to the UCI protocol specification:
///
/// cp <x> The score from the engine's point of view in centipawns.
/// mate <y> Mate in y moves, not plies. If the engine is getting mated
/// use negative values for y.
/// mate <y> Mate in 'y' moves (not plies). If the engine is getting mated,
/// uses negative values for 'y'.
string UCI::value(Value v) {
@ -306,7 +316,7 @@ string UCI::value(Value v) {
stringstream ss;
if (abs(v) < VALUE_MATE_IN_MAX_PLY)
ss << "cp " << v * 100 / PawnValueEg;
ss << "cp " << v * 100 / NormalizeToPawnValue;
else
ss << "mate " << (v > 0 ? VALUE_MATE - v + 1 : -VALUE_MATE - v) / 2;
@ -314,8 +324,8 @@ string UCI::value(Value v) {
}
/// UCI::wdl() report WDL statistics given an evaluation and a game ply, based on
/// data gathered for fishtest LTC games.
/// UCI::wdl() reports the win-draw-loss (WDL) statistics given an evaluation
/// and a game ply based on the data gathered for fishtest LTC games.
string UCI::wdl(Value v, int ply) {
@ -338,9 +348,9 @@ std::string UCI::square(Square s) {
/// UCI::move() converts a Move to a string in coordinate notation (g1f3, a7a8q).
/// The only special case is castling, where we print in the e1g1 notation in
/// normal chess mode, and in e1h1 notation in chess960 mode. Internally all
/// castling moves are always encoded as 'king captures rook'.
/// The only special case is castling where the e1g1 notation is printed in
/// standard chess mode and in e1h1 notation it is printed in Chess960 mode.
/// Internally, all castling moves are always encoded as 'king captures rook'.
string UCI::move(Move m, bool chess960) {
@ -370,8 +380,8 @@ string UCI::move(Move m, bool chess960) {
Move UCI::to_move(const Position& pos, string& str) {
if (str.length() == 5) // Junior could send promotion piece in uppercase
str[4] = char(tolower(str[4]));
if (str.length() == 5)
str[4] = char(tolower(str[4])); // The promotion piece character must be lowercased
for (const auto& m : MoveList<LEGAL>(pos))
if (str == UCI::move(m, pos.is_chess960()))

View File

@ -30,17 +30,24 @@ class Position;
namespace UCI {
// Normalizes the internal value as reported by evaluate or search
// to the UCI centipawn result used in output. This value is derived from
// the win_rate_model() such that Stockfish outputs an advantage of
// "100 centipawns" for a position if the engine has a 50% probability to win
// from this position in selfplay at fishtest LTC time control.
const int NormalizeToPawnValue = 361;
class Option;
/// Custom comparator because UCI options should be case insensitive
/// Define a custom comparator, because the UCI options should be case-insensitive
struct CaseInsensitiveLess {
bool operator() (const std::string&, const std::string&) const;
};
/// Our options container is actually a std::map
/// The options container is defined as a std::map
typedef std::map<std::string, Option, CaseInsensitiveLess> OptionsMap;
/// Option class implements an option as defined by UCI protocol
/// The Option class implements each option as specified by the UCI protocol
class Option {
typedef void (*OnChange)(const Option&);
@ -72,7 +79,7 @@ void loop(int argc, char* argv[]);
std::string value(Value v);
std::string square(Square s);
std::string move(Move m, bool chess960);
std::string pv(const Position& pos, Depth depth, Value alpha, Value beta);
std::string pv(const Position& pos, Depth depth);
std::string wdl(Value v, int ply);
Move to_move(const Position& pos, std::string& str);

View File

@ -61,7 +61,7 @@ void init(OptionsMap& o) {
constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048;
o["Debug Log File"] << Option("", on_logger);
o["Threads"] << Option(1, 1, 512, on_threads);
o["Threads"] << Option(1, 1, 1024, on_threads);
o["Hash"] << Option(16, 1, MaxHashMB, on_hash_size);
o["Clear Hash"] << Option(on_clear_hash);
o["Ponder"] << Option(false);

View File

@ -36,7 +36,7 @@ import org.petero.droidfish.EngineOptions;
/** Stockfish engine running as process, started from assets resource. */
public class InternalStockFish extends ExternalEngine {
private static final String defaultNet = "nn-6877cd24400e.nnue";
private static final String defaultNet = "nn-ad9b42354671.nnue";
private static final String netOption = "evalfile";
private File defaultNetFile; // To get the full path of the copied default network file