Shared pawn history

[Passed STC SMP](https://tests.stockfishchess.org/tests/view/694e506c572093c1986d7276):
```
LLR: 2.97 (-2.94,2.94) <0.00,2.00>
Total: 14992 W: 3924 L: 3653 D: 7415
Ptnml(0-2): 20, 1547, 4090, 1820, 19
```

[Passed LTC SMP](https://tests.stockfishchess.org/tests/live_elo/694ead61572093c1986d7365):
```
LLR: 2.94 (-2.94,2.94) <0.50,2.50>
Total: 41146 W: 10654 L: 10342 D: 20150
Ptnml(0-2): 17, 3999, 12225, 4319, 13
```

[Passed a sanity check STC SMP post-refactoring](https://tests.stockfishchess.org/tests/view/69503997572093c1986d763a):
```
LLR: 2.94 (-2.94,2.94) <0.00,2.00>
Total: 46728 W: 12178 L: 11863 D: 22687
Ptnml(0-2): 82, 5093, 12685, 5436, 68
```

(The large gain of the first STC was probably a fluke, and this result
is more reasonable!)

After shared correction history, Viz suggested we try sharing other
histories, especially `pawnHistory`. As far as we're aware, sharing
history besides correction history (like Caissa does) is novel. The
implementation follows the same pattern as shared correction history –
the size of the history table is scaled with
`next_power_of_two(threadsInNumaNode)` and the entry is prefetched in
`do_move`.

A bit of refactoring was done to accommodate this new history. Note that
we prefetch `&history->pawn_entry(*this)[pc][to]` rather than
`&history->pawn_entry(*this)` because unlike the other entries, each
entry contains multiple cache lines.

closes https://github.com/official-stockfish/Stockfish/pull/6498

Bench: 2503391

Co-authored-by: Michael Chaly <Vizvezdenec@gmail.com>
This commit is contained in:
anematode
2025-12-28 14:59:09 +01:00
committed by Disservin
co-authored by Michael Chaly
parent 1780c1fd6e
commit 969285fa5d
6 changed files with 49 additions and 34 deletions
+34 -14
View File
@@ -35,22 +35,18 @@
namespace Stockfish {
constexpr int PAWN_HISTORY_SIZE = 8192; // has to be a power of 2
constexpr int PAWN_HISTORY_BASE_SIZE = 8192; // has to be a power of 2
constexpr int UINT_16_HISTORY_SIZE = std::numeric_limits<uint16_t>::max() + 1;
constexpr int CORRHIST_BASE_SIZE = UINT_16_HISTORY_SIZE;
constexpr int CORRECTION_HISTORY_LIMIT = 1024;
constexpr int LOW_PLY_HISTORY_SIZE = 5;
static_assert((PAWN_HISTORY_SIZE & (PAWN_HISTORY_SIZE - 1)) == 0,
"PAWN_HISTORY_SIZE has to be a power of 2");
static_assert((PAWN_HISTORY_BASE_SIZE & (PAWN_HISTORY_BASE_SIZE - 1)) == 0,
"PAWN_HISTORY_BASE_SIZE has to be a power of 2");
static_assert((CORRHIST_BASE_SIZE & (CORRHIST_BASE_SIZE - 1)) == 0,
"CORRHIST_BASE_SIZE has to be a power of 2");
inline int pawn_history_index(const Position& pos) {
return pos.pawn_key() & (PAWN_HISTORY_SIZE - 1);
}
// StatsEntry is the container of various numerical statistics. We use a class
// instead of a naked value to directly call history update operator<<() on
// the entry. The first template parameter T is the base type of the array,
@@ -96,6 +92,9 @@ enum StatsType {
template<typename T, int D, std::size_t... Sizes>
using Stats = MultiArray<StatsEntry<T, D>, Sizes...>;
template<typename T, int D, std::size_t... Sizes>
using AtomicStats = MultiArray<StatsEntry<T, D, true>, Sizes...>;
// DynStats is a dynamically sized array of Stats, used for thread-shared histories
// which should scale with the total number of threads. The SizeMultiplier gives
// the per-thread allocation count of T.
@@ -106,11 +105,13 @@ struct DynStats {
data = make_unique_large_page<T[]>(size);
}
// Sets all values in the range to 0
void clear_range(size_t start, size_t end) {
void clear_range(int value, size_t threadIdx) {
size_t start = threadIdx * SizeMultiplier;
assert(start < size);
assert(end <= size);
T* fill_start = &(*this)[start];
memset(reinterpret_cast<char*>(fill_start), 0, sizeof(T) * (end - start));
size_t end = std::min(start + SizeMultiplier, size);
while (start < end)
data[start++].fill(value);
}
size_t get_size() const { return size; }
T& operator[](size_t index) {
@@ -149,7 +150,8 @@ using PieceToHistory = Stats<std::int16_t, 30000, PIECE_NB, SQUARE_NB>;
using ContinuationHistory = MultiArray<PieceToHistory, PIECE_NB, SQUARE_NB>;
// PawnHistory is addressed by the pawn structure and a move's [piece][to]
using PawnHistory = Stats<std::int16_t, 8192, PAWN_HISTORY_SIZE, PIECE_NB, SQUARE_NB>;
using PawnHistory =
DynStats<AtomicStats<std::int16_t, 8192, PIECE_NB, SQUARE_NB>, PAWN_HISTORY_BASE_SIZE>;
// Correction histories record differences between the static evaluation of
// positions and their search score. It is used to improve the static evaluation
@@ -169,6 +171,13 @@ struct CorrectionBundle {
StatsEntry<T, D, true> minor;
StatsEntry<T, D, true> nonPawnWhite;
StatsEntry<T, D, true> nonPawnBlack;
void operator=(T val) {
pawn = val;
minor = val;
nonPawnWhite = val;
nonPawnBlack = val;
}
};
namespace Detail {
@@ -212,13 +221,22 @@ using TTMoveHistory = StatsEntry<std::int16_t, 8192>;
// the indexing more efficient.
struct SharedHistories {
SharedHistories(size_t threadCount) :
correctionHistory(threadCount) {
correctionHistory(threadCount),
pawnHistory(threadCount) {
assert((threadCount & (threadCount - 1)) == 0 && threadCount != 0);
sizeMinus1 = correctionHistory.get_size() - 1;
pawnHistSizeMinus1 = pawnHistory.get_size() - 1;
}
size_t get_size() const { return sizeMinus1 + 1; }
auto& pawn_entry(const Position& pos) {
return pawnHistory[pos.pawn_key() & pawnHistSizeMinus1];
}
const auto& pawn_entry(const Position& pos) const {
return pawnHistory[pos.pawn_key() & pawnHistSizeMinus1];
}
auto& pawn_correction_entry(const Position& pos) {
return correctionHistory[pos.pawn_key() & sizeMinus1];
}
@@ -243,9 +261,11 @@ struct SharedHistories {
}
UnifiedCorrectionHistory correctionHistory;
PawnHistory pawnHistory;
private:
size_t sizeMinus1;
size_t sizeMinus1, pawnHistSizeMinus1;
};
} // namespace Stockfish
+3 -3
View File
@@ -87,14 +87,14 @@ MovePicker::MovePicker(const Position& p,
const LowPlyHistory* lph,
const CapturePieceToHistory* cph,
const PieceToHistory** ch,
const PawnHistory* ph,
const SharedHistories* sh,
int pl) :
pos(p),
mainHistory(mh),
lowPlyHistory(lph),
captureHistory(cph),
continuationHistory(ch),
pawnHistory(ph),
sharedHistory(sh),
ttMove(ttm),
depth(d),
ply(pl) {
@@ -159,7 +159,7 @@ ExtMove* MovePicker::score(MoveList<Type>& ml) {
{
// histories
m.value = 2 * (*mainHistory)[us][m.raw()];
m.value += 2 * (*pawnHistory)[pawn_history_index(pos)][pc][to];
m.value += 2 * sharedHistory->pawn_entry(pos)[pc][to];
m.value += (*continuationHistory[0])[pc][to];
m.value += (*continuationHistory[1])[pc][to];
m.value += (*continuationHistory[2])[pc][to];
+2 -2
View File
@@ -45,7 +45,7 @@ class MovePicker {
const LowPlyHistory*,
const CapturePieceToHistory*,
const PieceToHistory**,
const PawnHistory*,
const SharedHistories*,
int);
MovePicker(const Position&, Move, int, const CapturePieceToHistory*);
Move next_move();
@@ -64,7 +64,7 @@ class MovePicker {
const LowPlyHistory* lowPlyHistory;
const CapturePieceToHistory* captureHistory;
const PieceToHistory** continuationHistory;
const PawnHistory* pawnHistory;
const SharedHistories* sharedHistory;
Move ttMove;
ExtMove * cur, *endCur, *endBadCaptures, *endCaptures, *endGenerated;
int stage;
+1
View File
@@ -885,6 +885,7 @@ void Position::do_move(Move m,
if (history)
{
prefetch(&history->pawn_entry(*this)[pc][to]);
prefetch(&history->pawn_correction_entry(*this));
prefetch(&history->minor_piece_correction_entry(*this));
prefetch(&history->nonpawn_correction_entry<WHITE>(*this));
+8 -13
View File
@@ -580,14 +580,10 @@ void Search::Worker::undo_null_move(Position& pos) { pos.undo_null_move(); }
void Search::Worker::clear() {
mainHistory.fill(68);
captureHistory.fill(-689);
pawnHistory.fill(-1238);
// Each thread is responsible for clearing their part of shared history
size_t len = sharedHistory.get_size() / numaTotal;
size_t start = numaThreadIdx * len;
size_t end = std::min(start + len, sharedHistory.get_size());
sharedHistory.correctionHistory.clear_range(start, end);
sharedHistory.correctionHistory.clear_range(0, numaThreadIdx);
sharedHistory.pawnHistory.clear_range(-1238, numaThreadIdx);
ttMoveHistory = 0;
@@ -861,7 +857,7 @@ Value Search::Worker::search(
mainHistory[~us][((ss - 1)->currentMove).raw()] << evalDiff * 9;
if (!ttHit && type_of(pos.piece_on(prevSq)) != PAWN
&& ((ss - 1)->currentMove).type_of() != PROMOTION)
pawnHistory[pawn_history_index(pos)][pos.piece_on(prevSq)][prevSq] << evalDiff * 13;
sharedHistory.pawn_entry(pos)[pos.piece_on(prevSq)][prevSq] << evalDiff * 13;
}
@@ -992,7 +988,7 @@ moves_loop: // When in check, search starts here
MovePicker mp(pos, ttData.move, depth, &mainHistory, &lowPlyHistory, &captureHistory, contHist,
&pawnHistory, ss->ply);
&sharedHistory, ss->ply);
value = bestValue;
@@ -1081,7 +1077,7 @@ moves_loop: // When in check, search starts here
{
int history = (*contHist[0])[movedPiece][move.to_sq()]
+ (*contHist[1])[movedPiece][move.to_sq()]
+ pawnHistory[pawn_history_index(pos)][movedPiece][move.to_sq()];
+ sharedHistory.pawn_entry(pos)[movedPiece][move.to_sq()];
// Continuation history based pruning
if (history < -4083 * depth)
@@ -1439,7 +1435,7 @@ moves_loop: // When in check, search starts here
mainHistory[~us][((ss - 1)->currentMove).raw()] << scaledBonus * 243 / 32768;
if (type_of(pos.piece_on(prevSq)) != PAWN && ((ss - 1)->currentMove).type_of() != PROMOTION)
pawnHistory[pawn_history_index(pos)][pos.piece_on(prevSq)][prevSq]
sharedHistory.pawn_entry(pos)[pos.piece_on(prevSq)][prevSq]
<< scaledBonus * 1160 / 32768;
}
@@ -1611,7 +1607,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta)
// the moves. We presently use two stages of move generator in quiescence search:
// captures, or evasions only when in check.
MovePicker mp(pos, ttData.move, DEPTH_QS, &mainHistory, &lowPlyHistory, &captureHistory,
contHist, &pawnHistory, ss->ply);
contHist, &sharedHistory, ss->ply);
// Step 5. Loop through all pseudo-legal moves until no moves remain or a beta
// cutoff occurs.
@@ -1900,8 +1896,7 @@ void update_quiet_histories(
update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus * 896 / 1024);
int pIndex = pawn_history_index(pos);
workerThread.pawnHistory[pIndex][pos.moved_piece(move)][move.to_sq()]
workerThread.sharedHistory.pawn_entry(pos)[pos.moved_piece(move)][move.to_sq()]
<< bonus * (bonus > 0 ? 905 : 505) / 1024;
}
-1
View File
@@ -292,7 +292,6 @@ class Worker {
CapturePieceToHistory captureHistory;
ContinuationHistory continuationHistory[2][2];
PawnHistory pawnHistory;
CorrectionHistory<Continuation> continuationCorrectionHistory;
TTMoveHistory ttMoveHistory;