Skip to content

Commit

Permalink
Introduce static evaluation correction history
Browse files Browse the repository at this point in the history
Idea from Caissa (https://github.com/Witek902/Caissa) chess engine.

With given pawn structure collect data with how often search result and by how
much it was better / worse than static evalution of position and use it to
adjust static evaluation of positions with given pawn structure. Details:

1. excludes positions with fail highs and moves producing it being a capture;
2. update value is function of not only difference between best value and static
   evaluation but also is multiplied by linear function of depth;
3. maximum update value is maximum value of correction history divided by 2;
4. correction history itself is divided by 32 when applied so maximum value of
   static evaluation adjustment is 32 internal units.

Passed STC:
https://tests.stockfishchess.org/tests/view/658fc7b679aa8af82b955cac
LLR: 2.96 (-2.94,2.94) <0.00,2.00>
Total: 128672 W: 32757 L: 32299 D: 63616
Ptnml(0-2): 441, 15241, 32543, 15641, 470

Passed LTC:
https://tests.stockfishchess.org/tests/view/65903f6979aa8af82b9566f1
LLR: 2.95 (-2.94,2.94) <0.50,2.50>
Total: 97422 W: 24626 L: 24178 D: 48618
Ptnml(0-2): 41, 10837, 26527, 11245, 61

closes official-stockfish#4950

Bench: 1157852
  • Loading branch information
Vizvezdenec authored and Disservin committed Dec 31, 2023
1 parent 4ff297a commit b4d995d
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 26 deletions.
4 changes: 2 additions & 2 deletions src/movepick.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ void MovePicker::score() {

// histories
m.value = 2 * (*mainHistory)[pos.side_to_move()][from_to(m)];
m.value += 2 * (*pawnHistory)[pawn_structure(pos)][pc][to];
m.value += 2 * (*pawnHistory)[pawn_structure_index(pos)][pc][to];
m.value += 2 * (*continuationHistory[0])[pc][to];
m.value += (*continuationHistory[1])[pc][to];
m.value += (*continuationHistory[2])[pc][to] / 4;
Expand Down Expand Up @@ -216,7 +216,7 @@ void MovePicker::score() {
else
m.value = (*mainHistory)[pos.side_to_move()][from_to(m)]
+ (*continuationHistory[0])[pos.moved_piece(m)][to_sq(m)]
+ (*pawnHistory)[pawn_structure(pos)][pos.moved_piece(m)][to_sq(m)];
+ (*pawnHistory)[pawn_structure_index(pos)][pos.moved_piece(m)][to_sq(m)];
}
}

Expand Down
21 changes: 19 additions & 2 deletions src/movepick.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,25 @@

namespace Stockfish {

constexpr int PAWN_HISTORY_SIZE = 512; // has to be a power of 2
constexpr int PAWN_HISTORY_SIZE = 512; // has to be a power of 2
constexpr int CORRECTION_HISTORY_SIZE = 16384; // has to be a power of 2
constexpr int CORRECTION_HISTORY_LIMIT = 1024;

static_assert((PAWN_HISTORY_SIZE & (PAWN_HISTORY_SIZE - 1)) == 0,
"PAWN_HISTORY_SIZE has to be a power of 2");

inline int pawn_structure(const Position& pos) { return pos.pawn_key() & (PAWN_HISTORY_SIZE - 1); }
static_assert((CORRECTION_HISTORY_SIZE & (CORRECTION_HISTORY_SIZE - 1)) == 0,
"CORRECTION_HISTORY_SIZE has to be a power of 2");

enum PawnHistoryType {
Normal,
Correction
};

template<PawnHistoryType T = Normal>
inline int pawn_structure_index(const Position& pos) {
return pos.pawn_key() & ((T == Normal ? PAWN_HISTORY_SIZE : CORRECTION_HISTORY_SIZE) - 1);
}

// StatsEntry stores the stat table value. It is usually a number but could
// be a move or even a nested history. We use a class instead of a naked value
Expand Down Expand Up @@ -122,6 +135,10 @@ using ContinuationHistory = Stats<PieceToHistory, NOT_USED, PIECE_NB, SQUARE_NB>
// PawnHistory is addressed by the pawn structure and a move's [piece][to]
using PawnHistory = Stats<int16_t, 8192, PAWN_HISTORY_SIZE, PIECE_NB, SQUARE_NB>;

// CorrectionHistory is addressed by color and pawn structure
using CorrectionHistory =
Stats<int16_t, CORRECTION_HISTORY_LIMIT, COLOR_NB, CORRECTION_HISTORY_SIZE>;

// MovePicker class is used to pick one pseudo-legal move at a time from the
// current position. The most important method is next_move(), which returns a
// new pseudo-legal move each time it is called, until there are no moves left,
Expand Down
97 changes: 75 additions & 22 deletions src/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ constexpr int futility_move_count(bool improving, Depth depth) {
return improving ? (3 + depth * depth) : (3 + depth * depth) / 2;
}

// Guarantee evaluation does not hit the tablebase range
constexpr Value to_static_eval(const Value v) {
return std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1);
}

// History and stats update bonus, based on depth
int stat_bonus(Depth d) { return std::min(268 * d - 352, 1153); }

Expand Down Expand Up @@ -712,6 +717,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo

CapturePieceToHistory& captureHistory = thisThread->captureHistory;

Value unadjustedStaticEval = VALUE_NONE;

// Step 6. Static evaluation of the position
if (ss->inCheck)
{
Expand All @@ -725,26 +732,40 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
// Providing the hint that this node's accumulator will be used often
// brings significant Elo gain (~13 Elo).
Eval::NNUE::hint_common_parent_position(pos);
eval = ss->staticEval;
unadjustedStaticEval = eval = ss->staticEval;
}
else if (ss->ttHit)
{
// Never assume anything about values stored in TT
ss->staticEval = eval = tte->eval();
unadjustedStaticEval = ss->staticEval = eval = tte->eval();
if (eval == VALUE_NONE)
ss->staticEval = eval = evaluate(pos);
unadjustedStaticEval = ss->staticEval = eval = evaluate(pos);
else if (PvNode)
Eval::NNUE::hint_common_parent_position(pos);

Value newEval =
ss->staticEval
+ thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] / 32;

ss->staticEval = eval = to_static_eval(newEval);

// ttValue can be used as a better position evaluation (~7 Elo)
if (ttValue != VALUE_NONE && (tte->bound() & (ttValue > eval ? BOUND_LOWER : BOUND_UPPER)))
eval = ttValue;
}
else
{
ss->staticEval = eval = evaluate(pos);
// Save static evaluation into the transposition table
tte->save(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_NONE, MOVE_NONE, eval);
unadjustedStaticEval = ss->staticEval = eval = evaluate(pos);

Value newEval =
ss->staticEval
+ thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] / 32;

ss->staticEval = eval = to_static_eval(newEval);

// Static evaluation is saved as it was before adjustment by correction history
tte->save(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_NONE, MOVE_NONE,
unadjustedStaticEval);
}

// Use static evaluation difference to improve quiet move ordering (~9 Elo)
Expand All @@ -753,7 +774,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
int bonus = std::clamp(-13 * int((ss - 1)->staticEval + ss->staticEval), -1652, 1546);
thisThread->mainHistory[~us][from_to((ss - 1)->currentMove)] << bonus;
if (type_of(pos.piece_on(prevSq)) != PAWN && type_of((ss - 1)->currentMove) != PROMOTION)
thisThread->pawnHistory[pawn_structure(pos)][pos.piece_on(prevSq)][prevSq] << bonus / 4;
thisThread->pawnHistory[pawn_structure_index(pos)][pos.piece_on(prevSq)][prevSq]
<< bonus / 4;
}

// Set up the improving flag, which is true if current static evaluation is
Expand Down Expand Up @@ -888,7 +910,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
{
// Save ProbCut data into transposition table
tte->save(posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER, depth - 3,
move, ss->staticEval);
move, unadjustedStaticEval);
return std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY ? value - (probCutBeta - beta)
: value;
}
Expand Down Expand Up @@ -999,10 +1021,10 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
}
else
{
int history = (*contHist[0])[movedPiece][to_sq(move)]
+ (*contHist[1])[movedPiece][to_sq(move)]
+ (*contHist[3])[movedPiece][to_sq(move)]
+ thisThread->pawnHistory[pawn_structure(pos)][movedPiece][to_sq(move)];
int history =
(*contHist[0])[movedPiece][to_sq(move)] + (*contHist[1])[movedPiece][to_sq(move)]
+ (*contHist[3])[movedPiece][to_sq(move)]
+ thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][to_sq(move)];

// Continuation history based pruning (~2 Elo)
if (lmrDepth < 6 && history < -3752 * depth)
Expand Down Expand Up @@ -1364,12 +1386,23 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
ss->ttPv = ss->ttPv || ((ss - 1)->ttPv && depth > 3);

// Write gathered information in transposition table
// Static evaluation is saved as it was before correction history
if (!excludedMove && !(rootNode && thisThread->pvIdx))
tte->save(posKey, value_to_tt(bestValue, ss->ply), ss->ttPv,
bestValue >= beta ? BOUND_LOWER
: PvNode && bestMove ? BOUND_EXACT
: BOUND_UPPER,
depth, bestMove, ss->staticEval);
depth, bestMove, unadjustedStaticEval);

// Adjust correction history
if (!ss->inCheck && (!bestMove || !pos.capture(bestMove))
&& !(bestValue >= beta && bestValue <= ss->staticEval)
&& !(!bestMove && bestValue >= ss->staticEval))
{
auto bonus = std::clamp(int(bestValue - ss->staticEval) * depth / 8,
-CORRECTION_HISTORY_LIMIT / 4, CORRECTION_HISTORY_LIMIT / 4);
thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] << bonus;
}

assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE);

Expand Down Expand Up @@ -1450,6 +1483,8 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
&& (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER)))
return ttValue;

Value unadjustedStaticEval = VALUE_NONE;

// Step 4. Static evaluation of the position
if (ss->inCheck)
bestValue = futilityBase = -VALUE_INFINITE;
Expand All @@ -1458,25 +1493,39 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
if (ss->ttHit)
{
// Never assume anything about values stored in TT
if ((ss->staticEval = bestValue = tte->eval()) == VALUE_NONE)
ss->staticEval = bestValue = evaluate(pos);
if ((unadjustedStaticEval = ss->staticEval = bestValue = tte->eval()) == VALUE_NONE)
unadjustedStaticEval = ss->staticEval = bestValue = evaluate(pos);

Value newEval =
ss->staticEval
+ thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] / 32;

ss->staticEval = bestValue = to_static_eval(newEval);

// ttValue can be used as a better position evaluation (~13 Elo)
if (ttValue != VALUE_NONE
&& (tte->bound() & (ttValue > bestValue ? BOUND_LOWER : BOUND_UPPER)))
bestValue = ttValue;
}
else
{
// In case of null move search, use previous static eval with a different sign
ss->staticEval = bestValue =
unadjustedStaticEval = ss->staticEval = bestValue =
(ss - 1)->currentMove != MOVE_NULL ? evaluate(pos) : -(ss - 1)->staticEval;

Value newEval =
ss->staticEval
+ thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] / 32;

ss->staticEval = bestValue = to_static_eval(newEval);
}

// Stand pat. Return immediately if static value is at least beta
if (bestValue >= beta)
{
if (!ss->ttHit)
tte->save(posKey, value_to_tt(bestValue, ss->ply), false, BOUND_LOWER, DEPTH_NONE,
MOVE_NONE, ss->staticEval);
MOVE_NONE, unadjustedStaticEval);

return bestValue;
}
Expand Down Expand Up @@ -1620,8 +1669,10 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
bestValue = (3 * bestValue + beta) / 4;

// Save gathered info in transposition table
// Static evaluation is saved as it was before adjustment by correction history
tte->save(posKey, value_to_tt(bestValue, ss->ply), pvHit,
bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, ttDepth, bestMove, ss->staticEval);
bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, ttDepth, bestMove,
unadjustedStaticEval);

assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE);

Expand Down Expand Up @@ -1720,15 +1771,17 @@ void update_all_stats(const Position& pos,

// Increase stats for the best move in case it was a quiet move
update_quiet_stats(pos, ss, bestMove, bestMoveBonus);
thisThread->pawnHistory[pawn_structure(pos)][moved_piece][to_sq(bestMove)]
<< quietMoveBonus;

int pIndex = pawn_structure_index(pos);
thisThread->pawnHistory[pIndex][moved_piece][to_sq(bestMove)] << quietMoveBonus;

// Decrease stats for all non-best quiet moves
for (int i = 0; i < quietCount; ++i)
{
thisThread->pawnHistory[pawn_structure(pos)][pos.moved_piece(quietsSearched[i])]
[to_sq(quietsSearched[i])]
thisThread
->pawnHistory[pIndex][pos.moved_piece(quietsSearched[i])][to_sq(quietsSearched[i])]
<< -quietMoveMalus;

thisThread->mainHistory[us][from_to(quietsSearched[i])] << -quietMoveMalus;
update_continuation_histories(ss, pos.moved_piece(quietsSearched[i]),
to_sq(quietsSearched[i]), -quietMoveMalus);
Expand Down
1 change: 1 addition & 0 deletions src/thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ void Thread::clear() {
mainHistory.fill(0);
captureHistory.fill(0);
pawnHistory.fill(0);
correctionHistory.fill(0);

for (bool inCheck : {false, true})
for (StatsType c : {NoCaptures, Captures})
Expand Down
1 change: 1 addition & 0 deletions src/thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class Thread {
CapturePieceToHistory captureHistory;
ContinuationHistory continuationHistory[2][2];
PawnHistory pawnHistory;
CorrectionHistory correctionHistory;
};


Expand Down

0 comments on commit b4d995d

Please sign in to comment.