Skip to content

Commit b4d995d

Browse files
VizvezdenecDisservin
authored andcommitted
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 #4950 Bench: 1157852
1 parent 4ff297a commit b4d995d

File tree

5 files changed

+98
-26
lines changed

5 files changed

+98
-26
lines changed

src/movepick.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ void MovePicker::score() {
179179

180180
// histories
181181
m.value = 2 * (*mainHistory)[pos.side_to_move()][from_to(m)];
182-
m.value += 2 * (*pawnHistory)[pawn_structure(pos)][pc][to];
182+
m.value += 2 * (*pawnHistory)[pawn_structure_index(pos)][pc][to];
183183
m.value += 2 * (*continuationHistory[0])[pc][to];
184184
m.value += (*continuationHistory[1])[pc][to];
185185
m.value += (*continuationHistory[2])[pc][to] / 4;
@@ -216,7 +216,7 @@ void MovePicker::score() {
216216
else
217217
m.value = (*mainHistory)[pos.side_to_move()][from_to(m)]
218218
+ (*continuationHistory[0])[pos.moved_piece(m)][to_sq(m)]
219-
+ (*pawnHistory)[pawn_structure(pos)][pos.moved_piece(m)][to_sq(m)];
219+
+ (*pawnHistory)[pawn_structure_index(pos)][pos.moved_piece(m)][to_sq(m)];
220220
}
221221
}
222222

src/movepick.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,25 @@
3333

3434
namespace Stockfish {
3535

36-
constexpr int PAWN_HISTORY_SIZE = 512; // has to be a power of 2
36+
constexpr int PAWN_HISTORY_SIZE = 512; // has to be a power of 2
37+
constexpr int CORRECTION_HISTORY_SIZE = 16384; // has to be a power of 2
38+
constexpr int CORRECTION_HISTORY_LIMIT = 1024;
3739

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

41-
inline int pawn_structure(const Position& pos) { return pos.pawn_key() & (PAWN_HISTORY_SIZE - 1); }
43+
static_assert((CORRECTION_HISTORY_SIZE & (CORRECTION_HISTORY_SIZE - 1)) == 0,
44+
"CORRECTION_HISTORY_SIZE has to be a power of 2");
45+
46+
enum PawnHistoryType {
47+
Normal,
48+
Correction
49+
};
50+
51+
template<PawnHistoryType T = Normal>
52+
inline int pawn_structure_index(const Position& pos) {
53+
return pos.pawn_key() & ((T == Normal ? PAWN_HISTORY_SIZE : CORRECTION_HISTORY_SIZE) - 1);
54+
}
4255

4356
// StatsEntry stores the stat table value. It is usually a number but could
4457
// 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>
122135
// PawnHistory is addressed by the pawn structure and a move's [piece][to]
123136
using PawnHistory = Stats<int16_t, 8192, PAWN_HISTORY_SIZE, PIECE_NB, SQUARE_NB>;
124137

138+
// CorrectionHistory is addressed by color and pawn structure
139+
using CorrectionHistory =
140+
Stats<int16_t, CORRECTION_HISTORY_LIMIT, COLOR_NB, CORRECTION_HISTORY_SIZE>;
141+
125142
// MovePicker class is used to pick one pseudo-legal move at a time from the
126143
// current position. The most important method is next_move(), which returns a
127144
// new pseudo-legal move each time it is called, until there are no moves left,

src/search.cpp

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ constexpr int futility_move_count(bool improving, Depth depth) {
9393
return improving ? (3 + depth * depth) : (3 + depth * depth) / 2;
9494
}
9595

96+
// Guarantee evaluation does not hit the tablebase range
97+
constexpr Value to_static_eval(const Value v) {
98+
return std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1);
99+
}
100+
96101
// History and stats update bonus, based on depth
97102
int stat_bonus(Depth d) { return std::min(268 * d - 352, 1153); }
98103

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

713718
CapturePieceToHistory& captureHistory = thisThread->captureHistory;
714719

720+
Value unadjustedStaticEval = VALUE_NONE;
721+
715722
// Step 6. Static evaluation of the position
716723
if (ss->inCheck)
717724
{
@@ -725,26 +732,40 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
725732
// Providing the hint that this node's accumulator will be used often
726733
// brings significant Elo gain (~13 Elo).
727734
Eval::NNUE::hint_common_parent_position(pos);
728-
eval = ss->staticEval;
735+
unadjustedStaticEval = eval = ss->staticEval;
729736
}
730737
else if (ss->ttHit)
731738
{
732739
// Never assume anything about values stored in TT
733-
ss->staticEval = eval = tte->eval();
740+
unadjustedStaticEval = ss->staticEval = eval = tte->eval();
734741
if (eval == VALUE_NONE)
735-
ss->staticEval = eval = evaluate(pos);
742+
unadjustedStaticEval = ss->staticEval = eval = evaluate(pos);
736743
else if (PvNode)
737744
Eval::NNUE::hint_common_parent_position(pos);
738745

746+
Value newEval =
747+
ss->staticEval
748+
+ thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] / 32;
749+
750+
ss->staticEval = eval = to_static_eval(newEval);
751+
739752
// ttValue can be used as a better position evaluation (~7 Elo)
740753
if (ttValue != VALUE_NONE && (tte->bound() & (ttValue > eval ? BOUND_LOWER : BOUND_UPPER)))
741754
eval = ttValue;
742755
}
743756
else
744757
{
745-
ss->staticEval = eval = evaluate(pos);
746-
// Save static evaluation into the transposition table
747-
tte->save(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_NONE, MOVE_NONE, eval);
758+
unadjustedStaticEval = ss->staticEval = eval = evaluate(pos);
759+
760+
Value newEval =
761+
ss->staticEval
762+
+ thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] / 32;
763+
764+
ss->staticEval = eval = to_static_eval(newEval);
765+
766+
// Static evaluation is saved as it was before adjustment by correction history
767+
tte->save(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_NONE, MOVE_NONE,
768+
unadjustedStaticEval);
748769
}
749770

750771
// 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
753774
int bonus = std::clamp(-13 * int((ss - 1)->staticEval + ss->staticEval), -1652, 1546);
754775
thisThread->mainHistory[~us][from_to((ss - 1)->currentMove)] << bonus;
755776
if (type_of(pos.piece_on(prevSq)) != PAWN && type_of((ss - 1)->currentMove) != PROMOTION)
756-
thisThread->pawnHistory[pawn_structure(pos)][pos.piece_on(prevSq)][prevSq] << bonus / 4;
777+
thisThread->pawnHistory[pawn_structure_index(pos)][pos.piece_on(prevSq)][prevSq]
778+
<< bonus / 4;
757779
}
758780

759781
// 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
888910
{
889911
// Save ProbCut data into transposition table
890912
tte->save(posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER, depth - 3,
891-
move, ss->staticEval);
913+
move, unadjustedStaticEval);
892914
return std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY ? value - (probCutBeta - beta)
893915
: value;
894916
}
@@ -999,10 +1021,10 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
9991021
}
10001022
else
10011023
{
1002-
int history = (*contHist[0])[movedPiece][to_sq(move)]
1003-
+ (*contHist[1])[movedPiece][to_sq(move)]
1004-
+ (*contHist[3])[movedPiece][to_sq(move)]
1005-
+ thisThread->pawnHistory[pawn_structure(pos)][movedPiece][to_sq(move)];
1024+
int history =
1025+
(*contHist[0])[movedPiece][to_sq(move)] + (*contHist[1])[movedPiece][to_sq(move)]
1026+
+ (*contHist[3])[movedPiece][to_sq(move)]
1027+
+ thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][to_sq(move)];
10061028

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

13661388
// Write gathered information in transposition table
1389+
// Static evaluation is saved as it was before correction history
13671390
if (!excludedMove && !(rootNode && thisThread->pvIdx))
13681391
tte->save(posKey, value_to_tt(bestValue, ss->ply), ss->ttPv,
13691392
bestValue >= beta ? BOUND_LOWER
13701393
: PvNode && bestMove ? BOUND_EXACT
13711394
: BOUND_UPPER,
1372-
depth, bestMove, ss->staticEval);
1395+
depth, bestMove, unadjustedStaticEval);
1396+
1397+
// Adjust correction history
1398+
if (!ss->inCheck && (!bestMove || !pos.capture(bestMove))
1399+
&& !(bestValue >= beta && bestValue <= ss->staticEval)
1400+
&& !(!bestMove && bestValue >= ss->staticEval))
1401+
{
1402+
auto bonus = std::clamp(int(bestValue - ss->staticEval) * depth / 8,
1403+
-CORRECTION_HISTORY_LIMIT / 4, CORRECTION_HISTORY_LIMIT / 4);
1404+
thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] << bonus;
1405+
}
13731406

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

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

1486+
Value unadjustedStaticEval = VALUE_NONE;
1487+
14531488
// Step 4. Static evaluation of the position
14541489
if (ss->inCheck)
14551490
bestValue = futilityBase = -VALUE_INFINITE;
@@ -1458,25 +1493,39 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
14581493
if (ss->ttHit)
14591494
{
14601495
// Never assume anything about values stored in TT
1461-
if ((ss->staticEval = bestValue = tte->eval()) == VALUE_NONE)
1462-
ss->staticEval = bestValue = evaluate(pos);
1496+
if ((unadjustedStaticEval = ss->staticEval = bestValue = tte->eval()) == VALUE_NONE)
1497+
unadjustedStaticEval = ss->staticEval = bestValue = evaluate(pos);
1498+
1499+
Value newEval =
1500+
ss->staticEval
1501+
+ thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] / 32;
1502+
1503+
ss->staticEval = bestValue = to_static_eval(newEval);
14631504

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

1516+
Value newEval =
1517+
ss->staticEval
1518+
+ thisThread->correctionHistory[us][pawn_structure_index<Correction>(pos)] / 32;
1519+
1520+
ss->staticEval = bestValue = to_static_eval(newEval);
1521+
}
1522+
14741523
// Stand pat. Return immediately if static value is at least beta
14751524
if (bestValue >= beta)
14761525
{
14771526
if (!ss->ttHit)
14781527
tte->save(posKey, value_to_tt(bestValue, ss->ply), false, BOUND_LOWER, DEPTH_NONE,
1479-
MOVE_NONE, ss->staticEval);
1528+
MOVE_NONE, unadjustedStaticEval);
14801529

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

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

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

@@ -1720,15 +1771,17 @@ void update_all_stats(const Position& pos,
17201771

17211772
// Increase stats for the best move in case it was a quiet move
17221773
update_quiet_stats(pos, ss, bestMove, bestMoveBonus);
1723-
thisThread->pawnHistory[pawn_structure(pos)][moved_piece][to_sq(bestMove)]
1724-
<< quietMoveBonus;
1774+
1775+
int pIndex = pawn_structure_index(pos);
1776+
thisThread->pawnHistory[pIndex][moved_piece][to_sq(bestMove)] << quietMoveBonus;
17251777

17261778
// Decrease stats for all non-best quiet moves
17271779
for (int i = 0; i < quietCount; ++i)
17281780
{
1729-
thisThread->pawnHistory[pawn_structure(pos)][pos.moved_piece(quietsSearched[i])]
1730-
[to_sq(quietsSearched[i])]
1781+
thisThread
1782+
->pawnHistory[pIndex][pos.moved_piece(quietsSearched[i])][to_sq(quietsSearched[i])]
17311783
<< -quietMoveMalus;
1784+
17321785
thisThread->mainHistory[us][from_to(quietsSearched[i])] << -quietMoveMalus;
17331786
update_continuation_histories(ss, pos.moved_piece(quietsSearched[i]),
17341787
to_sq(quietsSearched[i]), -quietMoveMalus);

src/thread.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ void Thread::clear() {
7070
mainHistory.fill(0);
7171
captureHistory.fill(0);
7272
pawnHistory.fill(0);
73+
correctionHistory.fill(0);
7374

7475
for (bool inCheck : {false, true})
7576
for (StatsType c : {NoCaptures, Captures})

src/thread.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class Thread {
6969
CapturePieceToHistory captureHistory;
7070
ContinuationHistory continuationHistory[2][2];
7171
PawnHistory pawnHistory;
72+
CorrectionHistory correctionHistory;
7273
};
7374

7475

0 commit comments

Comments
 (0)