From b4d995d0d910044cf4ea2ad3ee30fd1d21070cd8 Mon Sep 17 00:00:00 2001 From: Michael Chaly Date: Sun, 31 Dec 2023 10:13:03 +0300 Subject: [PATCH] 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 --- src/movepick.cpp | 4 +- src/movepick.h | 21 ++++++++++- src/search.cpp | 97 +++++++++++++++++++++++++++++++++++++----------- src/thread.cpp | 1 + src/thread.h | 1 + 5 files changed, 98 insertions(+), 26 deletions(-) diff --git a/src/movepick.cpp b/src/movepick.cpp index 0267a8e2e6f..ab37ff68e12 100644 --- a/src/movepick.cpp +++ b/src/movepick.cpp @@ -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; @@ -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)]; } } diff --git a/src/movepick.h b/src/movepick.h index 5077f4e3b72..eefc0d5037b 100644 --- a/src/movepick.h +++ b/src/movepick.h @@ -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 +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 @@ -122,6 +135,10 @@ using ContinuationHistory = Stats // PawnHistory is addressed by the pawn structure and a move's [piece][to] using PawnHistory = Stats; +// CorrectionHistory is addressed by color and pawn structure +using CorrectionHistory = + Stats; + // 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, diff --git a/src/search.cpp b/src/search.cpp index cb6b450de1c..1ec77bcd93b 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -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); } @@ -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) { @@ -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(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(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) @@ -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 @@ -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; } @@ -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) @@ -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(pos)] << bonus; + } 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))) return ttValue; + Value unadjustedStaticEval = VALUE_NONE; + // Step 4. Static evaluation of the position if (ss->inCheck) bestValue = futilityBase = -VALUE_INFINITE; @@ -1458,8 +1493,14 @@ 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(pos)] / 32; + + ss->staticEval = bestValue = to_static_eval(newEval); // ttValue can be used as a better position evaluation (~13 Elo) if (ttValue != VALUE_NONE @@ -1467,16 +1508,24 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { 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(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; } @@ -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); @@ -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); diff --git a/src/thread.cpp b/src/thread.cpp index de8de87d8a2..eeab18821ba 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -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}) diff --git a/src/thread.h b/src/thread.h index cb2f6db1d41..1edc9cc9e31 100644 --- a/src/thread.h +++ b/src/thread.h @@ -69,6 +69,7 @@ class Thread { CapturePieceToHistory captureHistory; ContinuationHistory continuationHistory[2][2]; PawnHistory pawnHistory; + CorrectionHistory correctionHistory; };