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

@@ -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<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)
@@ -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 @@ moves_loop: // When in check, search starts here
}
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 @@ moves_loop: // When in check, search starts here
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);
@@ -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<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
@@ -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<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;
}
@@ -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);