1
0
Fork 0
mirror of https://github.com/sockspls/badfish synced 2025-04-29 16:23:09 +00:00

Introduce static evaluation correction 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 https://github.com/official-stockfish/Stockfish/pull/4950

Bench: 1157852
This commit is contained in:
Michael Chaly 2023-12-31 10:13:03 +03:00 committed by Disservin
parent 4ff297a6df
commit b4d995d0d9
5 changed files with 98 additions and 26 deletions

View file

@ -179,7 +179,7 @@ void MovePicker::score() {
// histories // histories
m.value = 2 * (*mainHistory)[pos.side_to_move()][from_to(m)]; 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 += 2 * (*continuationHistory[0])[pc][to];
m.value += (*continuationHistory[1])[pc][to]; m.value += (*continuationHistory[1])[pc][to];
m.value += (*continuationHistory[2])[pc][to] / 4; m.value += (*continuationHistory[2])[pc][to] / 4;
@ -216,7 +216,7 @@ void MovePicker::score() {
else else
m.value = (*mainHistory)[pos.side_to_move()][from_to(m)] m.value = (*mainHistory)[pos.side_to_move()][from_to(m)]
+ (*continuationHistory[0])[pos.moved_piece(m)][to_sq(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)];
} }
} }

View file

@ -33,12 +33,25 @@
namespace Stockfish { 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, static_assert((PAWN_HISTORY_SIZE & (PAWN_HISTORY_SIZE - 1)) == 0,
"PAWN_HISTORY_SIZE has to be a power of 2"); "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 // 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 // be a move or even a nested history. We use a class instead of a naked value
@ -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] // 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>; 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 // 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 // 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, // new pseudo-legal move each time it is called, until there are no moves left,

View file

@ -93,6 +93,11 @@ constexpr int futility_move_count(bool improving, Depth depth) {
return improving ? (3 + depth * depth) : (3 + depth * depth) / 2; 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 // History and stats update bonus, based on depth
int stat_bonus(Depth d) { return std::min(268 * d - 352, 1153); } int stat_bonus(Depth d) { return std::min(268 * d - 352, 1153); }
@ -712,6 +717,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
CapturePieceToHistory& captureHistory = thisThread->captureHistory; CapturePieceToHistory& captureHistory = thisThread->captureHistory;
Value unadjustedStaticEval = VALUE_NONE;
// Step 6. Static evaluation of the position // Step 6. Static evaluation of the position
if (ss->inCheck) if (ss->inCheck)
{ {
@ -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 // Providing the hint that this node's accumulator will be used often
// brings significant Elo gain (~13 Elo). // brings significant Elo gain (~13 Elo).
Eval::NNUE::hint_common_parent_position(pos); Eval::NNUE::hint_common_parent_position(pos);
eval = ss->staticEval; unadjustedStaticEval = eval = ss->staticEval;
} }
else if (ss->ttHit) else if (ss->ttHit)
{ {
// Never assume anything about values stored in TT // Never assume anything about values stored in TT
ss->staticEval = eval = tte->eval(); unadjustedStaticEval = ss->staticEval = eval = tte->eval();
if (eval == VALUE_NONE) if (eval == VALUE_NONE)
ss->staticEval = eval = evaluate(pos); unadjustedStaticEval = ss->staticEval = eval = evaluate(pos);
else if (PvNode) else if (PvNode)
Eval::NNUE::hint_common_parent_position(pos); 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) // ttValue can be used as a better position evaluation (~7 Elo)
if (ttValue != VALUE_NONE && (tte->bound() & (ttValue > eval ? BOUND_LOWER : BOUND_UPPER))) if (ttValue != VALUE_NONE && (tte->bound() & (ttValue > eval ? BOUND_LOWER : BOUND_UPPER)))
eval = ttValue; eval = ttValue;
} }
else else
{ {
ss->staticEval = eval = evaluate(pos); unadjustedStaticEval = 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); 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) // Use static evaluation difference to improve quiet move ordering (~9 Elo)
@ -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); int bonus = std::clamp(-13 * int((ss - 1)->staticEval + ss->staticEval), -1652, 1546);
thisThread->mainHistory[~us][from_to((ss - 1)->currentMove)] << bonus; thisThread->mainHistory[~us][from_to((ss - 1)->currentMove)] << bonus;
if (type_of(pos.piece_on(prevSq)) != PAWN && type_of((ss - 1)->currentMove) != PROMOTION) 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 // Set up the improving flag, which is true if current static evaluation is
@ -888,7 +910,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
{ {
// Save ProbCut data into transposition table // Save ProbCut data into transposition table
tte->save(posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER, depth - 3, 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) return std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY ? value - (probCutBeta - beta)
: value; : value;
} }
@ -999,10 +1021,10 @@ moves_loop: // When in check, search starts here
} }
else else
{ {
int history = (*contHist[0])[movedPiece][to_sq(move)] int history =
+ (*contHist[1])[movedPiece][to_sq(move)] (*contHist[0])[movedPiece][to_sq(move)] + (*contHist[1])[movedPiece][to_sq(move)]
+ (*contHist[3])[movedPiece][to_sq(move)] + (*contHist[3])[movedPiece][to_sq(move)]
+ thisThread->pawnHistory[pawn_structure(pos)][movedPiece][to_sq(move)]; + thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][to_sq(move)];
// Continuation history based pruning (~2 Elo) // Continuation history based pruning (~2 Elo)
if (lmrDepth < 6 && history < -3752 * depth) if (lmrDepth < 6 && history < -3752 * depth)
@ -1364,12 +1386,23 @@ moves_loop: // When in check, search starts here
ss->ttPv = ss->ttPv || ((ss - 1)->ttPv && depth > 3); ss->ttPv = ss->ttPv || ((ss - 1)->ttPv && depth > 3);
// Write gathered information in transposition table // Write gathered information in transposition table
// Static evaluation is saved as it was before correction history
if (!excludedMove && !(rootNode && thisThread->pvIdx)) if (!excludedMove && !(rootNode && thisThread->pvIdx))
tte->save(posKey, value_to_tt(bestValue, ss->ply), ss->ttPv, tte->save(posKey, value_to_tt(bestValue, ss->ply), ss->ttPv,
bestValue >= beta ? BOUND_LOWER bestValue >= beta ? BOUND_LOWER
: PvNode && bestMove ? BOUND_EXACT : PvNode && bestMove ? BOUND_EXACT
: BOUND_UPPER, : 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); assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE);
@ -1450,6 +1483,8 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
&& (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER))) && (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER)))
return ttValue; return ttValue;
Value unadjustedStaticEval = VALUE_NONE;
// Step 4. Static evaluation of the position // Step 4. Static evaluation of the position
if (ss->inCheck) if (ss->inCheck)
bestValue = futilityBase = -VALUE_INFINITE; bestValue = futilityBase = -VALUE_INFINITE;
@ -1458,8 +1493,14 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
if (ss->ttHit) if (ss->ttHit)
{ {
// Never assume anything about values stored in TT // Never assume anything about values stored in TT
if ((ss->staticEval = bestValue = tte->eval()) == VALUE_NONE) if ((unadjustedStaticEval = ss->staticEval = bestValue = tte->eval()) == VALUE_NONE)
ss->staticEval = bestValue = evaluate(pos); 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) // ttValue can be used as a better position evaluation (~13 Elo)
if (ttValue != VALUE_NONE if (ttValue != VALUE_NONE
@ -1467,16 +1508,24 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
bestValue = ttValue; bestValue = ttValue;
} }
else else
{
// In case of null move search, use previous static eval with a different sign // 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; (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 // Stand pat. Return immediately if static value is at least beta
if (bestValue >= beta) if (bestValue >= beta)
{ {
if (!ss->ttHit) if (!ss->ttHit)
tte->save(posKey, value_to_tt(bestValue, ss->ply), false, BOUND_LOWER, DEPTH_NONE, tte->save(posKey, value_to_tt(bestValue, ss->ply), false, BOUND_LOWER, DEPTH_NONE,
MOVE_NONE, ss->staticEval); MOVE_NONE, unadjustedStaticEval);
return bestValue; return bestValue;
} }
@ -1620,8 +1669,10 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
bestValue = (3 * bestValue + beta) / 4; bestValue = (3 * bestValue + beta) / 4;
// Save gathered info in transposition table // 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, 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); assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE);
@ -1720,15 +1771,17 @@ void update_all_stats(const Position& pos,
// Increase stats for the best move in case it was a quiet move // Increase stats for the best move in case it was a quiet move
update_quiet_stats(pos, ss, bestMove, bestMoveBonus); 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 // Decrease stats for all non-best quiet moves
for (int i = 0; i < quietCount; ++i) for (int i = 0; i < quietCount; ++i)
{ {
thisThread->pawnHistory[pawn_structure(pos)][pos.moved_piece(quietsSearched[i])] thisThread
[to_sq(quietsSearched[i])] ->pawnHistory[pIndex][pos.moved_piece(quietsSearched[i])][to_sq(quietsSearched[i])]
<< -quietMoveMalus; << -quietMoveMalus;
thisThread->mainHistory[us][from_to(quietsSearched[i])] << -quietMoveMalus; thisThread->mainHistory[us][from_to(quietsSearched[i])] << -quietMoveMalus;
update_continuation_histories(ss, pos.moved_piece(quietsSearched[i]), update_continuation_histories(ss, pos.moved_piece(quietsSearched[i]),
to_sq(quietsSearched[i]), -quietMoveMalus); to_sq(quietsSearched[i]), -quietMoveMalus);

View file

@ -70,6 +70,7 @@ void Thread::clear() {
mainHistory.fill(0); mainHistory.fill(0);
captureHistory.fill(0); captureHistory.fill(0);
pawnHistory.fill(0); pawnHistory.fill(0);
correctionHistory.fill(0);
for (bool inCheck : {false, true}) for (bool inCheck : {false, true})
for (StatsType c : {NoCaptures, Captures}) for (StatsType c : {NoCaptures, Captures})

View file

@ -69,6 +69,7 @@ class Thread {
CapturePieceToHistory captureHistory; CapturePieceToHistory captureHistory;
ContinuationHistory continuationHistory[2][2]; ContinuationHistory continuationHistory[2][2];
PawnHistory pawnHistory; PawnHistory pawnHistory;
CorrectionHistory correctionHistory;
}; };