mirror of
https://github.com/official-stockfish/Stockfish.git
synced 2026-07-22 20:57:10 +00:00
Compare commits
19
Commits
@@ -0,0 +1,85 @@
|
||||
name: AVX2 Compiler Matrix
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
avx2-compiler-matrix:
|
||||
name: avx2 (${{ matrix.name }})
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { name: gcc-10, comp: gcc, cxx: g++, image: "gcc:10" }
|
||||
- { name: gcc-11, comp: gcc, cxx: g++, image: "gcc:11" }
|
||||
- { name: gcc-12, comp: gcc, cxx: g++, image: "gcc:12" }
|
||||
- { name: gcc-13, comp: gcc, cxx: g++, image: "gcc:13" }
|
||||
- { name: gcc-14, comp: gcc, cxx: g++, image: "gcc:14" }
|
||||
- { name: gcc-15, comp: gcc, cxx: g++, image: "gcc:15" }
|
||||
|
||||
# Using silkeh/clang for older versions
|
||||
- { name: clang-10, comp: clang, cxx: clang++, image: "silkeh/clang:10", is_clang: true, ver: "10" }
|
||||
- { name: clang-11, comp: clang, cxx: clang++, image: "silkeh/clang:11", is_clang: true, ver: "11" }
|
||||
- { name: clang-12, comp: clang, cxx: clang++, image: "silkeh/clang:12", is_clang: true, ver: "12" }
|
||||
- { name: clang-13, comp: clang, cxx: clang++, image: "silkeh/clang:13", is_clang: true, ver: "13" }
|
||||
- { name: clang-14, comp: clang, cxx: clang++, image: "silkeh/clang:14", is_clang: true, ver: "14" }
|
||||
- { name: clang-15, comp: clang, cxx: clang++, image: "silkeh/clang:15", is_clang: true, ver: "15" }
|
||||
- { name: clang-16, comp: clang, cxx: clang++, image: "silkeh/clang:16", is_clang: true, ver: "16" }
|
||||
- { name: clang-17, comp: clang, cxx: clang++, image: "silkeh/clang:17", is_clang: true, ver: "17" }
|
||||
|
||||
- { name: clang-18, comp: clang, cxx: clang++-18, image: "ubuntu:rolling", is_clang: true, ver: "18" }
|
||||
- { name: clang-19, comp: clang, cxx: clang++-19, image: "ubuntu:rolling", is_clang: true, ver: "19" }
|
||||
- { name: clang-20, comp: clang, cxx: clang++-20, image: "ubuntu:rolling", is_clang: true, ver: "20" }
|
||||
- { name: clang-21, comp: clang, cxx: clang++-21, image: "ubuntu:rolling", is_clang: true, ver: "21" }
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
if grep -q "buster" /etc/os-release; then
|
||||
echo "Debian Buster detected. Switching to archive repositories..."
|
||||
echo "deb http://archive.debian.org/debian buster main contrib non-free" > /etc/apt/sources.list
|
||||
echo "deb http://archive.debian.org/debian-security buster/updates main contrib non-free" >> /etc/apt/sources.list
|
||||
echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99-ignore-valid-until
|
||||
fi
|
||||
|
||||
apt-get update
|
||||
apt-get install -y curl git make
|
||||
|
||||
- name: Set up Clang
|
||||
if: ${{ matrix.is_clang && matrix.image == 'ubuntu:rolling' }}
|
||||
run: |
|
||||
if [ "${{ matrix.ver }}" -le 20 ]; then
|
||||
apt-get install -y clang-${{ matrix.ver }}
|
||||
else
|
||||
apt-get install -y \
|
||||
clang-${{ matrix.ver }} \
|
||||
llvm-${{ matrix.ver }}-dev \
|
||||
llvm-${{ matrix.ver }}-linker-tools \
|
||||
lld-${{ matrix.ver }}
|
||||
fi
|
||||
|
||||
- name: Download network
|
||||
working-directory: src
|
||||
run: make net
|
||||
|
||||
- name: Build avx2 binary
|
||||
working-directory: src
|
||||
run: |
|
||||
export CXXFLAGS="-Werror"
|
||||
if [ "${{ matrix.ver }}" -ge 20 ]; then
|
||||
export LDFLAGS="-fuse-ld=lld"
|
||||
apt install -y lld
|
||||
fi
|
||||
make clean
|
||||
make -j build ARCH=x86-64-avx2 COMP=${{ matrix.comp }} COMPCXX=${{ matrix.cxx }}
|
||||
|
||||
- name: Smoke test
|
||||
working-directory: src
|
||||
run: ./stockfish bench 16 1 6
|
||||
@@ -120,3 +120,5 @@ jobs:
|
||||
contents: write # For deleting/creating a (pre)release
|
||||
secrets:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
CompilerCheck:
|
||||
uses: ./.github/workflows/avx2_compilers.yml
|
||||
|
||||
@@ -111,6 +111,7 @@ Hongzhi Cheng
|
||||
Ivan Ivec (IIvec)
|
||||
Jacques B. (Timshel)
|
||||
Jake Senne (w1wwwwww)
|
||||
Jakub Ciolek (jake-ciolek)
|
||||
Jan Ondruš (hxim)
|
||||
Jared Kish (Kurtbusch, kurt22i)
|
||||
Jarrod Torriero (DU-jdto)
|
||||
|
||||
@@ -59,33 +59,6 @@ This distribution of Stockfish consists of the following files:
|
||||
* a file with the .nnue extension, storing the neural network for the NNUE
|
||||
evaluation. Binary distributions will have this file embedded.
|
||||
|
||||
## Stockfish on distributed memory systems
|
||||
|
||||
The cluster branch allows for running Stockfish on a cluster of servers (nodes)
|
||||
that are connected with a high-speed and low-latency network, using the message
|
||||
passing interface (MPI). In this case, one MPI process should be run per node,
|
||||
and UCI options can be used to set the number of threads/hash per node as usual.
|
||||
Typically, the engine will be invoked as
|
||||
```
|
||||
mpirun -np N /path/to/stockfish
|
||||
```
|
||||
where ```N``` stands for the number of MPI processes used (alternatives to ```mpirun```,
|
||||
include ```mpiexec```, ```srun```). Use 1 mpi rank per node, and employ threading
|
||||
according to the cores per node. To build the cluster
|
||||
branch, it is sufficient to specify ```COMPCXX=mpicxx``` (or e.g. CC depending on the name
|
||||
of the compiler providing MPI support) on the make command line, and do a clean build:
|
||||
```
|
||||
make -j ARCH=x86-64-modern clean build COMPCXX=mpicxx mpi=yes
|
||||
```
|
||||
Make sure that the MPI installation is configured to support ```MPI_THREAD_MULTIPLE```,
|
||||
this might require adding system specific compiler options to the Makefile. Stockfish employs
|
||||
non-blocking (asynchronous) communication, and benefits from an MPI
|
||||
implementation that efficiently supports this. Some MPI implentations might benefit
|
||||
from leaving 1 core/thread free for these asynchronous communications, and might require
|
||||
setting additional environment variables. ```mpirun``` should forward stdin/stdout
|
||||
to ```rank 0``` only (e.g. ```srun --input=0 --output=0```).
|
||||
Refer to your MPI documentation for more info.
|
||||
|
||||
## Contributing
|
||||
|
||||
__See [Contributing Guide](CONTRIBUTING.md).__
|
||||
|
||||
+2
-15
@@ -68,7 +68,7 @@ PGOBENCH = $(RUN_PREFIX) ./$(EXE) bench
|
||||
|
||||
### Source and object files
|
||||
SRCS = benchmark.cpp bitboard.cpp evaluate.cpp main.cpp \
|
||||
misc.cpp movegen.cpp movepick.cpp position.cpp cluster.cpp \
|
||||
misc.cpp movegen.cpp movepick.cpp position.cpp \
|
||||
search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp \
|
||||
nnue/nnue_accumulator.cpp nnue/nnue_misc.cpp nnue/network.cpp \
|
||||
nnue/features/half_ka_v2_hm.cpp nnue/features/full_threats.cpp \
|
||||
@@ -80,8 +80,7 @@ HEADERS = benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h history.
|
||||
nnue/layers/clipped_relu.h nnue/layers/sqr_clipped_relu.h nnue/nnue_accumulator.h \
|
||||
nnue/nnue_architecture.h nnue/nnue_common.h nnue/nnue_feature_transformer.h nnue/simd.h \
|
||||
position.h search.h syzygy/tbprobe.h thread.h thread_win32_osx.h timeman.h \
|
||||
tt.h tune.h types.h uci.h ucioption.h perft.h nnue/network.h engine.h score.h numa.h memory.h \
|
||||
cluster.h
|
||||
tt.h tune.h types.h uci.h ucioption.h perft.h nnue/network.h engine.h score.h numa.h memory.h
|
||||
|
||||
OBJS = $(notdir $(SRCS:.cpp=.o))
|
||||
|
||||
@@ -122,7 +121,6 @@ VPATH = syzygy:nnue:nnue/features
|
||||
# dotprod = yes/no --- -DUSE_NEON_DOTPROD --- Use ARM advanced SIMD Int8 dot product instructions
|
||||
# lsx = yes/no --- -mlsx --- Use Loongson SIMD eXtension
|
||||
# lasx = yes/no --- -mlasx --- use Loongson Advanced SIMD eXtension
|
||||
# mpi = yes/no --- -DUSE_MPI --- Use Message Passing Interface
|
||||
#
|
||||
# Note that Makefile is space sensitive, so when adding new architectures
|
||||
# or modifying existing flags, you have to make sure there are no extra spaces
|
||||
@@ -175,7 +173,6 @@ avx512icl = no
|
||||
altivec = no
|
||||
vsx = no
|
||||
neon = no
|
||||
mpi = no
|
||||
dotprod = no
|
||||
arm_version = 0
|
||||
lsx = no
|
||||
@@ -895,15 +892,6 @@ ifeq ($(OS), Android)
|
||||
LDFLAGS += -fPIE -pie
|
||||
endif
|
||||
|
||||
### 3.10 MPI
|
||||
ifneq (,$(findstring mpi, $(CXX)))
|
||||
mpi = yes
|
||||
endif
|
||||
ifeq ($(mpi),yes)
|
||||
CXXFLAGS += -DUSE_MPI -Wno-cast-qual -fexceptions
|
||||
DEPENDFLAGS += -DUSE_MPI
|
||||
endif
|
||||
|
||||
### ==========================================================================
|
||||
### Section 4. Public Targets
|
||||
### ==========================================================================
|
||||
@@ -1078,7 +1066,6 @@ config-sanity: net
|
||||
echo "altivec: '$(altivec)'" && \
|
||||
echo "vsx: '$(vsx)'" && \
|
||||
echo "neon: '$(neon)'" && \
|
||||
echo "mpi: '$(mpi)'" && \
|
||||
echo "dotprod: '$(dotprod)'" && \
|
||||
echo "arm_version: '$(arm_version)'" && \
|
||||
echo "lsx: '$(lsx)'" && \
|
||||
|
||||
+4
-1
@@ -49,12 +49,15 @@ std::string Bitboards::pretty(Bitboard b) {
|
||||
|
||||
std::string s = "+---+---+---+---+---+---+---+---+\n";
|
||||
|
||||
for (Rank r = RANK_8; r >= RANK_1; --r)
|
||||
for (Rank r = RANK_8;; --r)
|
||||
{
|
||||
for (File f = FILE_A; f <= FILE_H; ++f)
|
||||
s += b & make_square(f, r) ? "| X " : "| ";
|
||||
|
||||
s += "| " + std::to_string(1 + r) + "\n+---+---+---+---+---+---+---+---+\n";
|
||||
|
||||
if (r == RANK_1)
|
||||
break;
|
||||
}
|
||||
s += " a b c d e f g h\n";
|
||||
|
||||
|
||||
-490
@@ -1,490 +0,0 @@
|
||||
/*
|
||||
Stockfish, a UCI chess playing engine derived from Glaurung 2.1
|
||||
Copyright (C) 2004-2026 The Stockfish developers (see AUTHORS file)
|
||||
|
||||
Stockfish is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Stockfish is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifdef USE_MPI
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <istream>
|
||||
#include <map>
|
||||
#include <mpi.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "cluster.h"
|
||||
#include "thread.h"
|
||||
#include "timeman.h"
|
||||
#include "tt.h"
|
||||
#include "search.h"
|
||||
|
||||
namespace Stockfish {
|
||||
namespace Distributed {
|
||||
|
||||
// Total number of ranks and rank within the communicator
|
||||
static int world_rank = MPI_PROC_NULL;
|
||||
static int world_size = 0;
|
||||
|
||||
// Signals between ranks exchange basic info using a dedicated communicator
|
||||
static MPI_Comm signalsComm = MPI_COMM_NULL;
|
||||
static MPI_Request reqSignals = MPI_REQUEST_NULL;
|
||||
static uint64_t signalsCallCounter = 0;
|
||||
|
||||
// Signals are the number of nodes searched, stop, table base hits, transposition table saves
|
||||
enum Signals : int {
|
||||
SIG_NODES = 0,
|
||||
SIG_STOP = 1,
|
||||
SIG_TB = 2,
|
||||
SIG_TTS = 3,
|
||||
SIG_NB = 4
|
||||
};
|
||||
static uint64_t signalsSend[SIG_NB] = {};
|
||||
static uint64_t signalsRecv[SIG_NB] = {};
|
||||
static uint64_t nodesSearchedOthers = 0;
|
||||
static uint64_t tbHitsOthers = 0;
|
||||
static uint64_t TTsavesOthers = 0;
|
||||
static uint64_t stopSignalsPosted = 0;
|
||||
|
||||
// The UCI threads of each rank exchange use a dedicated communicator
|
||||
static MPI_Comm InputComm = MPI_COMM_NULL;
|
||||
|
||||
// bestMove requires MoveInfo communicators and data types
|
||||
static MPI_Comm MoveComm = MPI_COMM_NULL;
|
||||
static MPI_Datatype MIDatatype = MPI_DATATYPE_NULL;
|
||||
|
||||
// TT entries are communicated with a dedicated communicator.
|
||||
// The receive buffer is used to gather information from all ranks.
|
||||
// THe TTCacheCounter tracks the number of local elements that are ready to be sent.
|
||||
static MPI_Comm TTComm = MPI_COMM_NULL;
|
||||
static std::array<std::vector<KeyedTTEntry>, 2> TTSendRecvBuffs;
|
||||
static std::array<MPI_Request, 2> reqsTTSendRecv = {MPI_REQUEST_NULL, MPI_REQUEST_NULL};
|
||||
static uint64_t sendRecvPosted = 0;
|
||||
static std::atomic<uint64_t> TTCacheCounter = {};
|
||||
|
||||
/// Initialize MPI and associated data types. Note that the MPI library must be configured
|
||||
/// to support MPI_THREAD_MULTIPLE, since multiple threads access MPI simultaneously.
|
||||
void init() {
|
||||
|
||||
int thread_support;
|
||||
MPI_Init_thread(nullptr, nullptr, MPI_THREAD_MULTIPLE, &thread_support);
|
||||
if (thread_support < MPI_THREAD_MULTIPLE)
|
||||
{
|
||||
std::cerr << "Stockfish requires support for MPI_THREAD_MULTIPLE." << std::endl;
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
|
||||
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
|
||||
|
||||
const std::array<MPI_Aint, 5> MIdisps = {offsetof(MoveInfo, move), offsetof(MoveInfo, ponder),
|
||||
offsetof(MoveInfo, depth), offsetof(MoveInfo, score),
|
||||
offsetof(MoveInfo, rank)};
|
||||
MPI_Type_create_hindexed_block(5, 1, MIdisps.data(), MPI_INT, &MIDatatype);
|
||||
MPI_Type_commit(&MIDatatype);
|
||||
|
||||
MPI_Comm_dup(MPI_COMM_WORLD, &InputComm);
|
||||
MPI_Comm_dup(MPI_COMM_WORLD, &TTComm);
|
||||
MPI_Comm_dup(MPI_COMM_WORLD, &MoveComm);
|
||||
MPI_Comm_dup(MPI_COMM_WORLD, &signalsComm);
|
||||
}
|
||||
|
||||
/// Finalize MPI and free the associated data types.
|
||||
void finalize() {
|
||||
|
||||
MPI_Type_free(&MIDatatype);
|
||||
|
||||
MPI_Comm_free(&InputComm);
|
||||
MPI_Comm_free(&TTComm);
|
||||
MPI_Comm_free(&MoveComm);
|
||||
MPI_Comm_free(&signalsComm);
|
||||
|
||||
MPI_Finalize();
|
||||
}
|
||||
|
||||
/// Return the total number of ranks
|
||||
int size() { return world_size; }
|
||||
|
||||
/// Return the rank (index) of the process
|
||||
int rank() { return world_rank; }
|
||||
|
||||
/// The receive buffer depends on the number of MPI ranks and threads, resize as needed
|
||||
void ttSendRecvBuff_resize(size_t nThreads) {
|
||||
|
||||
for (int i : {0, 1})
|
||||
{
|
||||
TTSendRecvBuffs[i].resize(TTCacheSize * world_size * nThreads);
|
||||
std::fill(TTSendRecvBuffs[i].begin(), TTSendRecvBuffs[i].end(), KeyedTTEntry());
|
||||
}
|
||||
}
|
||||
|
||||
/// As input is only received by the root (rank 0) of the cluster, this input must be relayed
|
||||
/// to the UCI threads of all ranks, in order to setup the position, etc. We do this with a
|
||||
/// dedicated getline implementation, where the root broadcasts to all other ranks the received
|
||||
/// information.
|
||||
bool getline(std::istream& input, std::string& str) {
|
||||
|
||||
int size;
|
||||
std::vector<char> vec;
|
||||
int state;
|
||||
|
||||
if (is_root())
|
||||
{
|
||||
state = static_cast<bool>(std::getline(input, str));
|
||||
vec.assign(str.begin(), str.end());
|
||||
size = vec.size();
|
||||
}
|
||||
|
||||
// Some MPI implementations use busy-wait polling, while we need yielding as otherwise
|
||||
// the UCI thread on the non-root ranks would be consuming resources.
|
||||
static MPI_Request reqInput = MPI_REQUEST_NULL;
|
||||
MPI_Ibcast(&size, 1, MPI_INT, 0, InputComm, &reqInput);
|
||||
if (is_root())
|
||||
MPI_Wait(&reqInput, MPI_STATUS_IGNORE);
|
||||
else
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
int flag;
|
||||
MPI_Test(&reqInput, &flag, MPI_STATUS_IGNORE);
|
||||
if (flag)
|
||||
break;
|
||||
else
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast received string
|
||||
if (!is_root())
|
||||
vec.resize(size);
|
||||
MPI_Bcast(vec.data(), size, MPI_CHAR, 0, InputComm);
|
||||
if (!is_root())
|
||||
str.assign(vec.begin(), vec.end());
|
||||
MPI_Bcast(&state, 1, MPI_INT, 0, InputComm);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// Sending part of the signal communication loop
|
||||
namespace {
|
||||
void signals_send(const ThreadPool& threads) {
|
||||
|
||||
signalsSend[SIG_NODES] = threads.nodes_searched();
|
||||
signalsSend[SIG_TB] = threads.tb_hits();
|
||||
signalsSend[SIG_TTS] = threads.TT_saves();
|
||||
signalsSend[SIG_STOP] = threads.stop;
|
||||
MPI_Iallreduce(signalsSend, signalsRecv, SIG_NB, MPI_UINT64_T, MPI_SUM, signalsComm,
|
||||
&reqSignals);
|
||||
++signalsCallCounter;
|
||||
}
|
||||
|
||||
|
||||
/// Processing part of the signal communication loop.
|
||||
/// For some counters (e.g. nodes) we only keep their sum on the other nodes
|
||||
/// allowing to add local counters at any time for more fine grained process,
|
||||
/// which is useful to indicate progress during early iterations, and to have
|
||||
/// node counts that exactly match the non-MPI code in the single rank case.
|
||||
/// This call also propagates the stop signal between ranks.
|
||||
void signals_process(ThreadPool& threads) {
|
||||
|
||||
nodesSearchedOthers = signalsRecv[SIG_NODES] - signalsSend[SIG_NODES];
|
||||
tbHitsOthers = signalsRecv[SIG_TB] - signalsSend[SIG_TB];
|
||||
TTsavesOthers = signalsRecv[SIG_TTS] - signalsSend[SIG_TTS];
|
||||
stopSignalsPosted = signalsRecv[SIG_STOP];
|
||||
if (signalsRecv[SIG_STOP] > 0)
|
||||
threads.stop = true;
|
||||
}
|
||||
|
||||
void sendrecv_post() {
|
||||
|
||||
++sendRecvPosted;
|
||||
MPI_Irecv(TTSendRecvBuffs[sendRecvPosted % 2].data(),
|
||||
TTSendRecvBuffs[sendRecvPosted % 2].size() * sizeof(KeyedTTEntry), MPI_BYTE,
|
||||
(rank() + size() - 1) % size(), 42, TTComm, &reqsTTSendRecv[0]);
|
||||
MPI_Isend(TTSendRecvBuffs[(sendRecvPosted + 1) % 2].data(),
|
||||
TTSendRecvBuffs[(sendRecvPosted + 1) % 2].size() * sizeof(KeyedTTEntry), MPI_BYTE,
|
||||
(rank() + 1) % size(), 42, TTComm, &reqsTTSendRecv[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/// During search, most message passing is asynchronous, but at the end of
|
||||
/// search it makes sense to bring them to a common, finalized state.
|
||||
void signals_sync(ThreadPool& threads) {
|
||||
|
||||
while (stopSignalsPosted < uint64_t(size()))
|
||||
signals_poll(threads);
|
||||
|
||||
// Finalize outstanding messages of the signal loops.
|
||||
// We might have issued one call less than needed on some ranks.
|
||||
uint64_t globalCounter;
|
||||
MPI_Allreduce(&signalsCallCounter, &globalCounter, 1, MPI_UINT64_T, MPI_MAX, MoveComm);
|
||||
if (signalsCallCounter < globalCounter)
|
||||
{
|
||||
MPI_Wait(&reqSignals, MPI_STATUS_IGNORE);
|
||||
signals_send(threads);
|
||||
}
|
||||
assert(signalsCallCounter == globalCounter);
|
||||
MPI_Wait(&reqSignals, MPI_STATUS_IGNORE);
|
||||
signals_process(threads);
|
||||
|
||||
// Finalize outstanding messages in the sendRecv loop
|
||||
MPI_Allreduce(&sendRecvPosted, &globalCounter, 1, MPI_UINT64_T, MPI_MAX, MoveComm);
|
||||
while (sendRecvPosted < globalCounter)
|
||||
{
|
||||
MPI_Waitall(reqsTTSendRecv.size(), reqsTTSendRecv.data(), MPI_STATUSES_IGNORE);
|
||||
sendrecv_post();
|
||||
}
|
||||
assert(sendRecvPosted == globalCounter);
|
||||
MPI_Waitall(reqsTTSendRecv.size(), reqsTTSendRecv.data(), MPI_STATUSES_IGNORE);
|
||||
}
|
||||
|
||||
/// Initialize signal counters to zero.
|
||||
void signals_init() {
|
||||
|
||||
stopSignalsPosted = tbHitsOthers = TTsavesOthers = nodesSearchedOthers = 0;
|
||||
|
||||
signalsSend[SIG_NODES] = signalsRecv[SIG_NODES] = 0;
|
||||
signalsSend[SIG_TB] = signalsRecv[SIG_TB] = 0;
|
||||
signalsSend[SIG_TTS] = signalsRecv[SIG_TTS] = 0;
|
||||
signalsSend[SIG_STOP] = signalsRecv[SIG_STOP] = 0;
|
||||
}
|
||||
|
||||
/// Poll the signal loop, and start next round as needed.
|
||||
void signals_poll(ThreadPool& threads) {
|
||||
|
||||
int flag;
|
||||
MPI_Test(&reqSignals, &flag, MPI_STATUS_IGNORE);
|
||||
if (flag)
|
||||
{
|
||||
signals_process(threads);
|
||||
signals_send(threads);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide basic info related the cluster performance, in particular, the number of signals send,
|
||||
/// signals per sounds (sps), the number of gathers, the number of positions gathered (per node and per second, gpps)
|
||||
/// The number of TT saves and TT saves per second. If gpps equals approximately TTSavesps the gather loop has enough bandwidth.
|
||||
void cluster_info(const ThreadPool& threads, Depth depth, TimePoint elapsed) {
|
||||
|
||||
// TimePoint elapsed = Time.elapsed() + 1;
|
||||
uint64_t TTSaves = TT_saves(threads);
|
||||
|
||||
sync_cout << "info depth " << depth << " cluster "
|
||||
<< " signals " << signalsCallCounter << " sps " << signalsCallCounter * 1000 / elapsed
|
||||
<< " sendRecvs " << sendRecvPosted << " srpps "
|
||||
<< TTSendRecvBuffs[0].size() * sendRecvPosted * 1000 / elapsed << " TTSaves "
|
||||
<< TTSaves << " TTSavesps " << TTSaves * 1000 / elapsed << sync_endl;
|
||||
}
|
||||
|
||||
/// When a TT entry is saved, additional steps are taken if the entry is of sufficient depth.
|
||||
/// If sufficient entries has been collected, a communication is initiated.
|
||||
/// If a communication has been completed, the received results are saved to the TT.
|
||||
void save(TranspositionTable& TT,
|
||||
ThreadPool& threads,
|
||||
Search::Worker* thread,
|
||||
TTWriter ttWriter,
|
||||
Key k,
|
||||
Value v,
|
||||
bool PvHit,
|
||||
Bound b,
|
||||
Depth d,
|
||||
Move m,
|
||||
Value ev,
|
||||
uint8_t generation8) {
|
||||
|
||||
// Standard save to the TT
|
||||
ttWriter.write(k, v, PvHit, b, d, m, ev, generation8);
|
||||
|
||||
// If the entry is of sufficient depth to be worth communicating, take action.
|
||||
if (d > 3)
|
||||
{
|
||||
// count the TTsaves to information: this should be relatively similar
|
||||
// to the number of entries we can send/recv.
|
||||
thread->TTsaves.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
// Add to thread's send buffer, the locking here avoids races when the master thread
|
||||
// prepares the send buffer.
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(thread->ttCache.mutex);
|
||||
thread->ttCache.buffer.replace(KeyedTTEntry(k, TTData(m, v, ev, d, b, PvHit)));
|
||||
++TTCacheCounter;
|
||||
}
|
||||
|
||||
size_t recvBuffPerRankSize = threads.size() * TTCacheSize;
|
||||
|
||||
// Communicate on main search thread, as soon the threads combined have collected
|
||||
// sufficient data to fill the send buffers.
|
||||
if (thread == threads.main_thread()->worker.get() && TTCacheCounter > recvBuffPerRankSize)
|
||||
{
|
||||
// Test communication status
|
||||
int flag;
|
||||
MPI_Testall(reqsTTSendRecv.size(), reqsTTSendRecv.data(), &flag, MPI_STATUSES_IGNORE);
|
||||
|
||||
// Current communication is complete
|
||||
if (flag)
|
||||
{
|
||||
// Save all received entries to TT, and store our TTCaches, ready for the next round of communication
|
||||
for (size_t irank = 0; irank < size_t(size()); ++irank)
|
||||
{
|
||||
if (irank
|
||||
== size_t(
|
||||
rank())) // this is our part, fill the part of the buffer for sending
|
||||
{
|
||||
// Copy from the thread caches to the right spot in the buffer
|
||||
size_t i = irank * recvBuffPerRankSize;
|
||||
for (auto&& th : threads)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(th->worker->ttCache.mutex);
|
||||
|
||||
for (auto&& e : th->worker->ttCache.buffer)
|
||||
TTSendRecvBuffs[sendRecvPosted % 2][i++] = e;
|
||||
|
||||
// Reset thread's send buffer
|
||||
th->worker->ttCache.buffer = {};
|
||||
}
|
||||
|
||||
TTCacheCounter = 0;
|
||||
}
|
||||
else // process data received from the corresponding rank.
|
||||
for (size_t i = irank * recvBuffPerRankSize;
|
||||
i < (irank + 1) * recvBuffPerRankSize; ++i)
|
||||
{
|
||||
auto&& e = TTSendRecvBuffs[sendRecvPosted % 2][i];
|
||||
auto [ttHit, ttData, ttWriterForRecvd] = TT.probe(e.first);
|
||||
ttWriterForRecvd.write(e.first, e.second.value, e.second.is_pv,
|
||||
e.second.bound, e.second.depth, e.second.move,
|
||||
e.second.eval, TT.generation());
|
||||
}
|
||||
}
|
||||
|
||||
// Start next communication
|
||||
sendrecv_post();
|
||||
|
||||
// Force check of time on the next occasion, the above actions might have taken some time.
|
||||
thread->main_manager()->callsCnt = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Picks the bestMove across ranks, and send the associated info and PV to the root of the cluster.
|
||||
/// Note that this bestMove and PV must be output by the root, the guarantee proper ordering of output.
|
||||
/// TODO update to the scheme in master.. can this use aggregation of votes?
|
||||
void pick_moves(MoveInfo& mi, std::vector<std::vector<char>>& serializedInfo) {
|
||||
|
||||
MoveInfo* pMoveInfo = NULL;
|
||||
if (is_root())
|
||||
{
|
||||
pMoveInfo = (MoveInfo*) malloc(sizeof(MoveInfo) * size());
|
||||
}
|
||||
MPI_Gather(&mi, 1, MIDatatype, pMoveInfo, 1, MIDatatype, 0, MoveComm);
|
||||
|
||||
if (is_root())
|
||||
{
|
||||
std::map<int, int> votes;
|
||||
int minScore = pMoveInfo[0].score;
|
||||
for (int i = 0; i < size(); ++i)
|
||||
{
|
||||
minScore = std::min(minScore, pMoveInfo[i].score);
|
||||
votes[pMoveInfo[i].move] = 0;
|
||||
}
|
||||
for (int i = 0; i < size(); ++i)
|
||||
{
|
||||
votes[pMoveInfo[i].move] += pMoveInfo[i].score - minScore + pMoveInfo[i].depth;
|
||||
}
|
||||
int bestVote = votes[pMoveInfo[0].move];
|
||||
for (int i = 0; i < size(); ++i)
|
||||
{
|
||||
if (votes[pMoveInfo[i].move] > bestVote)
|
||||
{
|
||||
bestVote = votes[pMoveInfo[i].move];
|
||||
mi = pMoveInfo[i];
|
||||
}
|
||||
}
|
||||
free(pMoveInfo);
|
||||
}
|
||||
|
||||
// Send around the final result
|
||||
MPI_Bcast(&mi, 1, MIDatatype, 0, MoveComm);
|
||||
|
||||
// Send PV line to root as needed
|
||||
if (mi.rank != 0 && mi.rank == rank())
|
||||
{
|
||||
int numLines = serializedInfo.size();
|
||||
MPI_Send(&numLines, 1, MPI_INT, 0, 42, MoveComm);
|
||||
|
||||
for (const auto& serializedInfoOne : serializedInfo)
|
||||
{
|
||||
int size;
|
||||
size = serializedInfoOne.size();
|
||||
MPI_Send(&size, 1, MPI_INT, 0, 42, MoveComm);
|
||||
MPI_Send(serializedInfoOne.data(), size, MPI_CHAR, 0, 42, MoveComm);
|
||||
}
|
||||
}
|
||||
if (mi.rank != 0 && is_root())
|
||||
{
|
||||
serializedInfo.clear();
|
||||
|
||||
int numLines;
|
||||
MPI_Recv(&numLines, 1, MPI_INT, mi.rank, 42, MoveComm, MPI_STATUS_IGNORE);
|
||||
|
||||
for (int i = 0; i < numLines; ++i)
|
||||
{
|
||||
int size;
|
||||
std::vector<char> vec;
|
||||
MPI_Recv(&size, 1, MPI_INT, mi.rank, 42, MoveComm, MPI_STATUS_IGNORE);
|
||||
vec.resize(size);
|
||||
MPI_Recv(vec.data(), size, MPI_CHAR, mi.rank, 42, MoveComm, MPI_STATUS_IGNORE);
|
||||
serializedInfo.push_back(std::move(vec));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return nodes searched (lazily updated cluster wide in the signal loop)
|
||||
uint64_t nodes_searched(const ThreadPool& threads) {
|
||||
return nodesSearchedOthers + threads.nodes_searched();
|
||||
}
|
||||
|
||||
/// Return table base hits (lazily updated cluster wide in the signal loop)
|
||||
uint64_t tb_hits(const ThreadPool& threads) { return tbHitsOthers + threads.tb_hits(); }
|
||||
|
||||
/// Return the number of saves to the TT buffers, (lazily updated cluster wide in the signal loop)
|
||||
uint64_t TT_saves(const ThreadPool& threads) { return TTsavesOthers + threads.TT_saves(); }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#include "cluster.h"
|
||||
#include "thread.h"
|
||||
|
||||
namespace Stockfish {
|
||||
namespace Distributed {
|
||||
|
||||
uint64_t nodes_searched(const ThreadPool& threads) { return threads.nodes_searched(); }
|
||||
|
||||
uint64_t tb_hits(const ThreadPool& threads) { return threads.tb_hits(); }
|
||||
|
||||
uint64_t TT_saves(const ThreadPool& threads) { return threads.TT_saves(); }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_MPI
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
Stockfish, a UCI chess playing engine derived from Glaurung 2.1
|
||||
Copyright (C) 2004-2026 The Stockfish developers (see AUTHORS file)
|
||||
|
||||
Stockfish is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Stockfish is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef CLUSTER_H_INCLUDED
|
||||
#define CLUSTER_H_INCLUDED
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <istream>
|
||||
#include <string>
|
||||
|
||||
#include "misc.h"
|
||||
#include "tt.h"
|
||||
|
||||
namespace Stockfish {
|
||||
class Thread;
|
||||
class ThreadPool;
|
||||
|
||||
namespace Search {
|
||||
class Worker;
|
||||
}
|
||||
|
||||
/// The Distributed namespace contains functionality required to run on distributed
|
||||
/// memory architectures using MPI as the message passing interface. On a high level,
|
||||
/// a 'lazy SMP'-like scheme is implemented where TT saves of sufficient depth are
|
||||
/// collected on each rank and distributed to, and used by, all other ranks,
|
||||
/// which search essentially independently. The root (MPI rank 0) of the cluster
|
||||
/// is responsible for all I/O and time management, communicating this info to
|
||||
/// the other ranks as needed. UCI options such as Threads and Hash specify these
|
||||
/// quantities per MPI rank. It is recommended to have one rank (MPI process) per node.
|
||||
/// For the non-MPI case, wrappers that will be compiler-optimized away are provided.
|
||||
|
||||
namespace Distributed {
|
||||
|
||||
/// Basic info to find the cluster-wide bestMove
|
||||
struct MoveInfo {
|
||||
int move;
|
||||
int ponder;
|
||||
int depth;
|
||||
int score;
|
||||
int rank;
|
||||
};
|
||||
|
||||
#ifdef USE_MPI
|
||||
|
||||
// store the TTData with its (full) key, so it can be saved on the receiver side
|
||||
using KeyedTTEntry = std::pair<Key, TTData>;
|
||||
constexpr std::size_t TTCacheSize = 16;
|
||||
|
||||
// Threads locally cache their high-depth TT entries till a batch can be send by MPI
|
||||
template<std::size_t N>
|
||||
class TTCache: public std::array<KeyedTTEntry, N> {
|
||||
|
||||
struct Compare {
|
||||
inline bool operator()(const KeyedTTEntry& lhs, const KeyedTTEntry& rhs) {
|
||||
return lhs.second.depth > rhs.second.depth;
|
||||
}
|
||||
};
|
||||
Compare compare;
|
||||
|
||||
public:
|
||||
// Keep a heap of entries replacing low depth with high depth entries
|
||||
bool replace(const KeyedTTEntry& value) {
|
||||
|
||||
if (compare(value, this->front()))
|
||||
{
|
||||
std::pop_heap(this->begin(), this->end(), compare);
|
||||
this->back() = value;
|
||||
std::push_heap(this->begin(), this->end(), compare);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
void init();
|
||||
void finalize();
|
||||
bool getline(std::istream& input, std::string& str);
|
||||
int size();
|
||||
int rank();
|
||||
inline bool is_root() { return rank() == 0; }
|
||||
void save(TranspositionTable&,
|
||||
ThreadPool&,
|
||||
Search::Worker* thread,
|
||||
TTWriter ttWriter,
|
||||
Key k,
|
||||
Value v,
|
||||
bool PvHit,
|
||||
Bound b,
|
||||
Depth d,
|
||||
Move m,
|
||||
Value ev,
|
||||
uint8_t generation8);
|
||||
void pick_moves(MoveInfo& mi, std::vector<std::vector<char>>& PVLine);
|
||||
void ttSendRecvBuff_resize(size_t nThreads);
|
||||
uint64_t nodes_searched(const ThreadPool&);
|
||||
uint64_t tb_hits(const ThreadPool&);
|
||||
uint64_t TT_saves(const ThreadPool&);
|
||||
void cluster_info(const ThreadPool&, Depth depth, TimePoint elapsed);
|
||||
void signals_init();
|
||||
void signals_poll(ThreadPool& threads);
|
||||
void signals_sync(ThreadPool& threads);
|
||||
|
||||
#else
|
||||
|
||||
inline void init() {}
|
||||
inline void finalize() {}
|
||||
inline bool getline(std::istream& input, std::string& str) {
|
||||
return static_cast<bool>(std::getline(input, str));
|
||||
}
|
||||
constexpr int size() { return 1; }
|
||||
constexpr int rank() { return 0; }
|
||||
constexpr bool is_root() { return true; }
|
||||
inline void save(TranspositionTable&,
|
||||
ThreadPool&,
|
||||
Search::Worker*,
|
||||
TTWriter ttWriter,
|
||||
Key k,
|
||||
Value v,
|
||||
bool PvHit,
|
||||
Bound b,
|
||||
Depth d,
|
||||
Move m,
|
||||
Value ev,
|
||||
uint8_t generation8) {
|
||||
ttWriter.write(k, v, PvHit, b, d, m, ev, generation8);
|
||||
}
|
||||
inline void pick_moves(MoveInfo&, std::vector<std::vector<char>>&) {}
|
||||
inline void ttSendRecvBuff_resize(size_t) {}
|
||||
uint64_t nodes_searched(const ThreadPool&);
|
||||
uint64_t tb_hits(const ThreadPool&);
|
||||
uint64_t TT_saves(const ThreadPool&);
|
||||
inline void cluster_info(const ThreadPool&, Depth, TimePoint) {}
|
||||
inline void signals_init() {}
|
||||
inline void signals_poll(ThreadPool&) {}
|
||||
inline void signals_sync(ThreadPool&) {}
|
||||
|
||||
#endif /* USE_MPI */
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif // #ifndef CLUSTER_H_INCLUDED
|
||||
+13
-14
@@ -52,19 +52,21 @@ constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq
|
||||
constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048;
|
||||
int MaxThreads = std::max(1024, 4 * int(get_hardware_concurrency()));
|
||||
|
||||
// The default configuration will attempt to group L3 domains up to 32 threads.
|
||||
// This size was found to be a good balance between the Elo gain of increased
|
||||
// history sharing and the speed loss from more cross-cache accesses (see
|
||||
// PR#6526). The user can always explicitly override this behavior.
|
||||
constexpr NumaAutoPolicy DefaultNumaPolicy = BundledL3Policy{32};
|
||||
|
||||
Engine::Engine(std::optional<std::string> path) :
|
||||
binaryDirectory(path ? CommandLine::get_binary_directory(*path) : ""),
|
||||
numaContext(NumaConfig::from_system()),
|
||||
numaContext(NumaConfig::from_system(DefaultNumaPolicy)),
|
||||
states(new std::deque<StateInfo>(1)),
|
||||
threads(),
|
||||
networks(
|
||||
numaContext,
|
||||
// Heap-allocate because sizeof(NN::Networks) is large
|
||||
std::make_unique<NN::Networks>(
|
||||
std::make_unique<NN::NetworkBig>(NN::EvalFile{EvalFileDefaultNameBig, "None", ""},
|
||||
NN::EmbeddedNNUEType::BIG),
|
||||
std::make_unique<NN::NetworkSmall>(NN::EvalFile{EvalFileDefaultNameSmall, "None", ""},
|
||||
NN::EmbeddedNNUEType::SMALL))) {
|
||||
networks(numaContext,
|
||||
// Heap-allocate because sizeof(NN::Networks) is large
|
||||
std::make_unique<NN::Networks>(NN::EvalFile{EvalFileDefaultNameBig, "None", ""},
|
||||
NN::EvalFile{EvalFileDefaultNameSmall, "None", ""})) {
|
||||
|
||||
pos.set(StartFEN, false, &states->back());
|
||||
|
||||
@@ -217,12 +219,12 @@ void Engine::set_position(const std::string& fen, const std::vector<std::string>
|
||||
void Engine::set_numa_config_from_option(const std::string& o) {
|
||||
if (o == "auto" || o == "system")
|
||||
{
|
||||
numaContext.set_numa_config(NumaConfig::from_system());
|
||||
numaContext.set_numa_config(NumaConfig::from_system(DefaultNumaPolicy));
|
||||
}
|
||||
else if (o == "hardware")
|
||||
{
|
||||
// Don't respect affinity set in the system.
|
||||
numaContext.set_numa_config(NumaConfig::from_system(false));
|
||||
numaContext.set_numa_config(NumaConfig::from_system(DefaultNumaPolicy, false));
|
||||
}
|
||||
else if (o == "none")
|
||||
{
|
||||
@@ -251,9 +253,6 @@ void Engine::resize_threads() {
|
||||
void Engine::set_tt_size(size_t mb) {
|
||||
wait_for_search_finished();
|
||||
tt.resize(mb, threads);
|
||||
|
||||
// Adjust cluster buffers
|
||||
Distributed::ttSendRecvBuff_resize(threads.num_threads());
|
||||
}
|
||||
|
||||
void Engine::set_ponderhit(bool b) { threads.main_manager()->ponder = b; }
|
||||
|
||||
+1
-5
@@ -28,9 +28,7 @@
|
||||
using namespace Stockfish;
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
Distributed::init();
|
||||
if (Distributed::is_root())
|
||||
std::cout << engine_info() << std::endl;
|
||||
std::cout << engine_info() << std::endl;
|
||||
|
||||
Bitboards::init();
|
||||
Position::init();
|
||||
@@ -41,7 +39,5 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
uci->loop();
|
||||
|
||||
Distributed::finalize();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+12
-1
@@ -20,12 +20,12 @@
|
||||
#define MEMORY_H_INCLUDED
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <cstring>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
@@ -317,6 +317,17 @@ auto windows_try_with_large_page_priviliges([[maybe_unused]] FuncYesT&& fyes, Fu
|
||||
|
||||
#endif
|
||||
|
||||
template<typename T, typename ByteT>
|
||||
T load_as(const ByteT* buffer) {
|
||||
static_assert(std::is_trivially_copyable<T>::value, "Type must be trivially copyable");
|
||||
static_assert(sizeof(ByteT) == 1);
|
||||
|
||||
T value;
|
||||
std::memcpy(&value, buffer, sizeof(T));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace Stockfish
|
||||
|
||||
#endif // #ifndef MEMORY_H_INCLUDED
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ namespace Stockfish {
|
||||
namespace {
|
||||
|
||||
// Version number or dev.
|
||||
constexpr std::string_view version = "dev";
|
||||
constexpr std::string_view version = "18";
|
||||
|
||||
// Our fancy logging facility. The trick here is to replace cin.rdbuf() and
|
||||
// cout.rdbuf() with two Tie objects that tie cin and cout to a file stream. We
|
||||
|
||||
+28
-13
@@ -34,6 +34,7 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#define stringify2(x) #x
|
||||
@@ -305,24 +306,38 @@ inline uint64_t mul_hi64(uint64_t a, uint64_t b) {
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
inline void hash_combine(std::size_t& seed, const T& v) {
|
||||
std::hash<T> hasher;
|
||||
seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void hash_combine(std::size_t& seed, const std::size_t& v) {
|
||||
seed ^= v + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
inline std::uint64_t hash_bytes(const char* data, std::size_t size) {
|
||||
// FNV-1a 64-bit
|
||||
const char* p = data;
|
||||
std::uint64_t h = 14695981039346656037ull;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
h = (h ^ p[i]) * 1099511628211ull;
|
||||
return h;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline std::size_t get_raw_data_hash(const T& value) {
|
||||
return std::hash<std::string_view>{}(
|
||||
std::string_view(reinterpret_cast<const char*>(&value), sizeof(value)));
|
||||
// We must have no padding bytes because we're reinterpreting as char
|
||||
static_assert(std::has_unique_object_representations<T>());
|
||||
|
||||
return static_cast<std::size_t>(
|
||||
hash_bytes(reinterpret_cast<const char*>(&value), sizeof(value)));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline void hash_combine(std::size_t& seed, const T& v) {
|
||||
std::size_t x;
|
||||
// For primitive types we avoid using the default hasher, which may be
|
||||
// nondeterministic across program invocations
|
||||
if constexpr (std::is_integral<T>())
|
||||
x = v;
|
||||
else
|
||||
x = std::hash<T>{}(v);
|
||||
seed ^= x + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
|
||||
inline std::uint64_t hash_string(const std::string& sv) { return hash_bytes(sv.data(), sv.size()); }
|
||||
|
||||
template<std::size_t Capacity>
|
||||
class FixedString {
|
||||
public:
|
||||
@@ -450,7 +465,7 @@ void move_to_front(std::vector<T>& vec, Predicate pred) {
|
||||
template<std::size_t N>
|
||||
struct std::hash<Stockfish::FixedString<N>> {
|
||||
std::size_t operator()(const Stockfish::FixedString<N>& fstr) const noexcept {
|
||||
return std::hash<std::string_view>{}((std::string_view) fstr);
|
||||
return Stockfish::hash_bytes(fstr.data(), fstr.size());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+4
-4
@@ -47,10 +47,10 @@ template<Direction offset>
|
||||
inline Move* splat_pawn_moves(Move* moveList, Bitboard to_bb) {
|
||||
alignas(64) static constexpr auto SPLAT_TABLE = [] {
|
||||
std::array<Move, 64> table{};
|
||||
for (int8_t i = 0; i < 64; i++)
|
||||
for (int i = 0; i < 64; i++)
|
||||
{
|
||||
Square from{std::clamp<int8_t>(i - offset, 0, 63)};
|
||||
table[i] = {Move(from, Square{i})};
|
||||
Square from{uint8_t(std::clamp(i - offset, 0, 63))};
|
||||
table[i] = {Move(from, Square{uint8_t(i)})};
|
||||
}
|
||||
return table;
|
||||
}();
|
||||
@@ -68,7 +68,7 @@ inline Move* splat_pawn_moves(Move* moveList, Bitboard to_bb) {
|
||||
inline Move* splat_moves(Move* moveList, Square from, Bitboard to_bb) {
|
||||
alignas(64) static constexpr auto SPLAT_TABLE = [] {
|
||||
std::array<Move, 64> table{};
|
||||
for (int8_t i = 0; i < 64; i++)
|
||||
for (uint8_t i = 0; i < 64; i++)
|
||||
table[i] = {Move(SQUARE_ZERO, Square{i})};
|
||||
return table;
|
||||
}();
|
||||
|
||||
@@ -37,26 +37,6 @@ struct HelperOffsets {
|
||||
int cumulativePieceOffset, cumulativeOffset;
|
||||
};
|
||||
|
||||
// Information on a particular pair of pieces and whether they should be excluded
|
||||
struct PiecePairData {
|
||||
// Layout: bits 8..31 are the index contribution of this piece pair, bits 0 and 1 are exclusion info
|
||||
uint32_t data;
|
||||
|
||||
constexpr PiecePairData() :
|
||||
data(0) {}
|
||||
|
||||
constexpr PiecePairData(bool excluded_pair,
|
||||
bool semi_excluded_pair,
|
||||
IndexType feature_index_base) :
|
||||
data((uint32_t(excluded_pair) << 1) | (uint32_t(semi_excluded_pair && !excluded_pair))
|
||||
| (uint32_t(feature_index_base) << 8)) {}
|
||||
|
||||
// lsb: excluded if from < to; 2nd lsb: always excluded
|
||||
constexpr uint8_t excluded_pair_info() const { return static_cast<uint8_t>(data); }
|
||||
|
||||
constexpr IndexType feature_index_base() const { return static_cast<IndexType>(data >> 8); }
|
||||
};
|
||||
|
||||
constexpr std::array<Piece, 12> AllPieces = {
|
||||
W_PAWN, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING,
|
||||
B_PAWN, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING,
|
||||
@@ -173,7 +153,7 @@ constexpr auto helper_offsets = init_threat_offsets().first;
|
||||
constexpr auto offsets = init_threat_offsets().second;
|
||||
|
||||
constexpr auto init_index_luts() {
|
||||
std::array<std::array<PiecePairData, PIECE_NB>, PIECE_NB> indices{};
|
||||
std::array<std::array<std::array<uint32_t, 2>, PIECE_NB>, PIECE_NB> indices{};
|
||||
|
||||
for (Piece attacker : AllPieces)
|
||||
{
|
||||
@@ -189,8 +169,10 @@ constexpr auto init_index_luts() {
|
||||
+ (color_of(attacked) * (numValidTargets[attacker] / 2) + map)
|
||||
* helper_offsets[attacker].cumulativePieceOffset;
|
||||
|
||||
bool excluded = map < 0;
|
||||
indices[attacker][attacked] = PiecePairData(excluded, semi_excluded, feature);
|
||||
bool excluded = map < 0;
|
||||
indices[attacker][attacked][0] = excluded ? FullThreats::Dimensions : feature;
|
||||
indices[attacker][attacked][1] =
|
||||
excluded || semi_excluded ? FullThreats::Dimensions : feature;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +182,7 @@ constexpr auto init_index_luts() {
|
||||
// The final index is calculated from summing data found in these two LUTs, as well
|
||||
// as offsets[attacker][from]
|
||||
|
||||
// [attacker][attacked]
|
||||
// [attacker][attacked][from < to]
|
||||
constexpr auto index_lut1 = init_index_luts();
|
||||
// [attacker][from][to]
|
||||
constexpr auto index_lut2 = index_lut2_array();
|
||||
@@ -216,17 +198,9 @@ inline sf_always_inline IndexType FullThreats::make_index(
|
||||
unsigned attacker_oriented = attacker ^ swap;
|
||||
unsigned attacked_oriented = attacked ^ swap;
|
||||
|
||||
const auto piecePairData = index_lut1[attacker_oriented][attacked_oriented];
|
||||
|
||||
const bool less_than = from_oriented < to_oriented;
|
||||
if ((piecePairData.excluded_pair_info() + less_than) & 2)
|
||||
return FullThreats::Dimensions;
|
||||
|
||||
const IndexType index = piecePairData.feature_index_base()
|
||||
+ offsets[attacker_oriented][from_oriented]
|
||||
+ index_lut2[attacker_oriented][from_oriented][to_oriented];
|
||||
sf_assume(index < Dimensions);
|
||||
return index;
|
||||
return index_lut1[attacker_oriented][attacked_oriented][from_oriented < to_oriented]
|
||||
+ offsets[attacker_oriented][from_oriented]
|
||||
+ index_lut2[attacker_oriented][from_oriented][to_oriented];
|
||||
}
|
||||
|
||||
// Get a list of indices for active features in ascending order
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
#include "../../memory.h"
|
||||
#include "../nnue_common.h"
|
||||
#include "../simd.h"
|
||||
|
||||
@@ -95,7 +96,7 @@ static void affine_transform_non_ssse3(std::int32_t* output,
|
||||
#elif defined(USE_NEON)
|
||||
|
||||
int32x4_t sum = {biases[i]};
|
||||
const auto row = reinterpret_cast<const int8x8_t*>(&weights[offset]);
|
||||
const auto row = reinterpret_cast<const SIMD::vec_i8x8_t*>(&weights[offset]);
|
||||
for (IndexType j = 0; j < NumChunks; ++j)
|
||||
{
|
||||
int16x8_t product = vmull_s8(inputVector[j * 2], row[j * 2]);
|
||||
@@ -224,7 +225,6 @@ class AffineTransform {
|
||||
constexpr IndexType NumChunks = ceil_to_multiple<IndexType>(InputDimensions, 8) / 4;
|
||||
constexpr IndexType NumRegs = OutputDimensions / OutputSimdWidth;
|
||||
|
||||
const auto input32 = reinterpret_cast<const std::int32_t*>(input);
|
||||
const vec_t* biasvec = reinterpret_cast<const vec_t*>(biases);
|
||||
vec_t acc[NumRegs];
|
||||
for (IndexType k = 0; k < NumRegs; ++k)
|
||||
@@ -232,8 +232,9 @@ class AffineTransform {
|
||||
|
||||
for (IndexType i = 0; i < NumChunks; ++i)
|
||||
{
|
||||
const vec_t in0 = vec_set_32(input32[i]);
|
||||
const auto col0 =
|
||||
const vec_t in0 =
|
||||
vec_set_32(load_as<std::int32_t>(input + i * sizeof(std::int32_t)));
|
||||
const auto col0 =
|
||||
reinterpret_cast<const vec_t*>(&weights[i * OutputDimensions * 4]);
|
||||
|
||||
for (IndexType k = 0; k < NumRegs; ++k)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "../../bitboard.h"
|
||||
#include "../../memory.h"
|
||||
#include "../simd.h"
|
||||
#include "../nnue_common.h"
|
||||
|
||||
@@ -77,15 +78,17 @@ alignas(CacheLineSize) static constexpr struct OffsetIndices {
|
||||
#define RESTRICT
|
||||
#endif
|
||||
|
||||
// Find indices of nonzero numbers in an int32_t array
|
||||
// Find indices of nonzero 32-bit values in a packed byte buffer.
|
||||
// The input pointer addresses a sequence of 32-bit blocks stored in a
|
||||
// std::uint8_t array.
|
||||
template<const IndexType InputDimensions>
|
||||
void find_nnz(const std::int32_t* RESTRICT input,
|
||||
void find_nnz(const std::uint8_t* RESTRICT input,
|
||||
std::uint16_t* RESTRICT out,
|
||||
IndexType& count_out) {
|
||||
|
||||
#if defined(USE_AVX512ICL)
|
||||
|
||||
constexpr IndexType SimdWidthIn = 16; // 512 bits / 32 bits
|
||||
constexpr IndexType SimdWidthIn = 64; // 512 bits
|
||||
constexpr IndexType SimdWidthOut = 32; // 512 bits / 16 bits
|
||||
constexpr IndexType NumChunks = InputDimensions / SimdWidthOut;
|
||||
const __m512i increment = _mm512_set1_epi16(SimdWidthOut);
|
||||
@@ -122,7 +125,7 @@ void find_nnz(const std::int32_t* RESTRICT input,
|
||||
IndexType count = 0;
|
||||
for (IndexType i = 0; i < NumChunks; ++i)
|
||||
{
|
||||
const __m512i inputV = _mm512_load_si512(input + i * SimdWidth);
|
||||
const __m512i inputV = _mm512_load_si512(input + i * SimdWidth * sizeof(std::uint32_t));
|
||||
|
||||
// Get a bitmask and gather non zero indices
|
||||
const __mmask16 nnzMask = _mm512_test_epi32_mask(inputV, inputV);
|
||||
@@ -293,10 +296,8 @@ class AffineTransformSparseInput {
|
||||
std::uint16_t nnz[NumChunks];
|
||||
IndexType count;
|
||||
|
||||
const auto input32 = reinterpret_cast<const std::int32_t*>(input);
|
||||
|
||||
// Find indices of nonzero 32-bit blocks
|
||||
find_nnz<NumChunks>(input32, nnz, count);
|
||||
find_nnz<NumChunks>(input, nnz, count);
|
||||
|
||||
const outvec_t* biasvec = reinterpret_cast<const outvec_t*>(biases);
|
||||
outvec_t acc[NumRegs];
|
||||
@@ -314,13 +315,16 @@ class AffineTransformSparseInput {
|
||||
|
||||
while (start < end - 2)
|
||||
{
|
||||
const std::ptrdiff_t i0 = *start++;
|
||||
const std::ptrdiff_t i1 = *start++;
|
||||
const std::ptrdiff_t i2 = *start++;
|
||||
const invec_t in0 = vec_set_32(input32[i0]);
|
||||
const invec_t in1 = vec_set_32(input32[i1]);
|
||||
const invec_t in2 = vec_set_32(input32[i2]);
|
||||
const auto col0 =
|
||||
const std::ptrdiff_t i0 = *start++;
|
||||
const std::ptrdiff_t i1 = *start++;
|
||||
const std::ptrdiff_t i2 = *start++;
|
||||
const invec_t in0 =
|
||||
vec_set_32(load_as<std::int32_t>(input + i0 * sizeof(std::int32_t)));
|
||||
const invec_t in1 =
|
||||
vec_set_32(load_as<std::int32_t>(input + i1 * sizeof(std::int32_t)));
|
||||
const invec_t in2 =
|
||||
vec_set_32(load_as<std::int32_t>(input + i2 * sizeof(std::int32_t)));
|
||||
const auto col0 =
|
||||
reinterpret_cast<const invec_t*>(&weights_cp[i0 * OutputDimensions * ChunkSize]);
|
||||
const auto col1 =
|
||||
reinterpret_cast<const invec_t*>(&weights_cp[i1 * OutputDimensions * ChunkSize]);
|
||||
@@ -338,9 +342,9 @@ class AffineTransformSparseInput {
|
||||
#endif
|
||||
while (start < end)
|
||||
{
|
||||
const std::ptrdiff_t i = *start++;
|
||||
const invec_t in = vec_set_32(input32[i]);
|
||||
const auto col =
|
||||
const std::ptrdiff_t i = *start++;
|
||||
const invec_t in = vec_set_32(load_as<std::int32_t>(input + i * sizeof(std::int32_t)));
|
||||
const auto col =
|
||||
reinterpret_cast<const invec_t*>(&weights_cp[i * OutputDimensions * ChunkSize]);
|
||||
for (IndexType k = 0; k < NumAccums; ++k)
|
||||
vec_add_dpbusd_32(acc[k], in, col[k]);
|
||||
|
||||
@@ -141,10 +141,10 @@ class ClippedReLU {
|
||||
constexpr IndexType Start = NumChunks * SimdWidth;
|
||||
|
||||
#elif defined(USE_NEON)
|
||||
constexpr IndexType NumChunks = InputDimensions / (SimdWidth / 2);
|
||||
const int8x8_t Zero = {0};
|
||||
const auto in = reinterpret_cast<const int32x4_t*>(input);
|
||||
const auto out = reinterpret_cast<int8x8_t*>(output);
|
||||
constexpr IndexType NumChunks = InputDimensions / (SimdWidth / 2);
|
||||
const SIMD::vec_i8x8_t Zero = {0};
|
||||
const auto in = reinterpret_cast<const SIMD::vec_i32x4_t*>(input);
|
||||
const auto out = reinterpret_cast<SIMD::vec_i8x8_t*>(output);
|
||||
for (IndexType i = 0; i < NumChunks; ++i)
|
||||
{
|
||||
int16x8_t shifted;
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#define INCBIN_SILENCE_BITCODE_WARNING
|
||||
#include "../incbin/incbin.h"
|
||||
|
||||
#include "../cluster.h"
|
||||
#include "../evaluate.h"
|
||||
#include "../misc.h"
|
||||
#include "../position.h"
|
||||
@@ -222,12 +221,11 @@ void Network<Arch, Transformer>::verify(std::string
|
||||
if (f)
|
||||
{
|
||||
size_t size = sizeof(featureTransformer) + sizeof(Arch) * LayerStacks;
|
||||
if (Distributed::is_root())
|
||||
f("NNUE evaluation using " + evalfilePath + " (" + std::to_string(size / (1024 * 1024))
|
||||
+ "MiB, (" + std::to_string(featureTransformer.TotalInputDimensions) + ", "
|
||||
+ std::to_string(network[0].TransformedFeatureDimensions) + ", "
|
||||
+ std::to_string(network[0].FC_0_OUTPUTS) + ", "
|
||||
+ std::to_string(network[0].FC_1_OUTPUTS) + ", 1))");
|
||||
f("NNUE evaluation using " + evalfilePath + " (" + std::to_string(size / (1024 * 1024))
|
||||
+ "MiB, (" + std::to_string(featureTransformer.TotalInputDimensions) + ", "
|
||||
+ std::to_string(network[0].TransformedFeatureDimensions) + ", "
|
||||
+ std::to_string(network[0].FC_0_OUTPUTS) + ", " + std::to_string(network[0].FC_1_OUTPUTS)
|
||||
+ ", 1))");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -28,7 +28,6 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include "../misc.h"
|
||||
#include "../types.h"
|
||||
@@ -130,9 +129,9 @@ using NetworkSmall = Network<SmallNetworkArchitecture, SmallFeatureTransformer>;
|
||||
|
||||
|
||||
struct Networks {
|
||||
Networks(std::unique_ptr<NetworkBig>&& nB, std::unique_ptr<NetworkSmall>&& nS) :
|
||||
big(std::move(*nB)),
|
||||
small(std::move(*nS)) {}
|
||||
Networks(EvalFile bigFile, EvalFile smallFile) :
|
||||
big(bigFile, EmbeddedNNUEType::BIG),
|
||||
small(smallFile, EmbeddedNNUEType::SMALL) {}
|
||||
|
||||
NetworkBig big;
|
||||
NetworkSmall small;
|
||||
|
||||
+40
-36
@@ -169,52 +169,56 @@ inline void write_little_endian(std::ostream& stream, const IntType* values, std
|
||||
write_little_endian<IntType>(stream, values[i]);
|
||||
}
|
||||
|
||||
|
||||
// Read N signed integers from the stream s, putting them in the array out.
|
||||
// The stream is assumed to be compressed using the signed LEB128 format.
|
||||
// See https://en.wikipedia.org/wiki/LEB128 for a description of the compression scheme.
|
||||
template<typename IntType, std::size_t Count>
|
||||
inline void read_leb_128(std::istream& stream, std::array<IntType, Count>& out) {
|
||||
template<typename BufType, typename IntType, std::size_t Count>
|
||||
inline void read_leb_128_detail(std::istream& stream,
|
||||
std::array<IntType, Count>& out,
|
||||
std::uint32_t& bytes_left,
|
||||
BufType& buf,
|
||||
std::uint32_t& buf_pos) {
|
||||
|
||||
static_assert(std::is_signed_v<IntType>, "Not implemented for unsigned types");
|
||||
static_assert(sizeof(IntType) <= 4, "Not implemented for types larger than 32 bit");
|
||||
|
||||
IntType result = 0;
|
||||
size_t shift = 0, i = 0;
|
||||
while (i < Count)
|
||||
{
|
||||
if (buf_pos == buf.size())
|
||||
{
|
||||
stream.read(reinterpret_cast<char*>(buf.data()),
|
||||
std::min(std::size_t(bytes_left), buf.size()));
|
||||
buf_pos = 0;
|
||||
}
|
||||
|
||||
std::uint8_t byte = buf[buf_pos++];
|
||||
--bytes_left;
|
||||
result |= (byte & 0x7f) << (shift % 32);
|
||||
shift += 7;
|
||||
|
||||
if ((byte & 0x80) == 0)
|
||||
{
|
||||
out[i++] = (shift >= 32 || (byte & 0x40) == 0) ? result : result | ~((1 << shift) - 1);
|
||||
result = 0;
|
||||
shift = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename... Arrays>
|
||||
inline void read_leb_128(std::istream& stream, Arrays&... outs) {
|
||||
// Check the presence of our LEB128 magic string
|
||||
char leb128MagicString[Leb128MagicStringSize];
|
||||
stream.read(leb128MagicString, Leb128MagicStringSize);
|
||||
assert(strncmp(Leb128MagicString, leb128MagicString, Leb128MagicStringSize) == 0);
|
||||
|
||||
static_assert(std::is_signed_v<IntType>, "Not implemented for unsigned types");
|
||||
auto bytes_left = read_little_endian<std::uint32_t>(stream);
|
||||
std::array<std::uint8_t, 8192> buf;
|
||||
std::uint32_t buf_pos = buf.size();
|
||||
|
||||
const std::uint32_t BUF_SIZE = 4096;
|
||||
std::uint8_t buf[BUF_SIZE];
|
||||
|
||||
auto bytes_left = read_little_endian<std::uint32_t>(stream);
|
||||
|
||||
std::uint32_t buf_pos = BUF_SIZE;
|
||||
for (std::size_t i = 0; i < Count; ++i)
|
||||
{
|
||||
IntType result = 0;
|
||||
size_t shift = 0;
|
||||
do
|
||||
{
|
||||
if (buf_pos == BUF_SIZE)
|
||||
{
|
||||
stream.read(reinterpret_cast<char*>(buf), std::min(bytes_left, BUF_SIZE));
|
||||
buf_pos = 0;
|
||||
}
|
||||
|
||||
std::uint8_t byte = buf[buf_pos++];
|
||||
--bytes_left;
|
||||
result |= (byte & 0x7f) << shift;
|
||||
shift += 7;
|
||||
|
||||
if ((byte & 0x80) == 0)
|
||||
{
|
||||
out[i] = (sizeof(IntType) * 8 <= shift || (byte & 0x40) == 0)
|
||||
? result
|
||||
: result | ~((1 << shift) - 1);
|
||||
break;
|
||||
}
|
||||
} while (shift < sizeof(IntType) * 8);
|
||||
}
|
||||
(read_leb_128_detail(stream, outs, bytes_left, buf, buf_pos), ...);
|
||||
|
||||
assert(bytes_left == 0);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class FeatureTransformer {
|
||||
permute<16>(biases, PackusEpi16Order);
|
||||
permute<16>(weights, PackusEpi16Order);
|
||||
|
||||
if (UseThreats)
|
||||
if constexpr (UseThreats)
|
||||
permute<8>(threatWeights, PackusEpi16Order);
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class FeatureTransformer {
|
||||
permute<16>(biases, InversePackusEpi16Order);
|
||||
permute<16>(weights, InversePackusEpi16Order);
|
||||
|
||||
if (UseThreats)
|
||||
if constexpr (UseThreats)
|
||||
permute<8>(threatWeights, InversePackusEpi16Order);
|
||||
}
|
||||
|
||||
@@ -152,40 +152,26 @@ class FeatureTransformer {
|
||||
}
|
||||
|
||||
// Read network parameters
|
||||
// TODO: This is ugly. Currently LEB128 on the entire L1 necessitates
|
||||
// reading the weights into a combined array, and then splitting.
|
||||
bool read_parameters(std::istream& stream) {
|
||||
read_leb_128<BiasType>(stream, biases);
|
||||
read_leb_128(stream, biases);
|
||||
|
||||
if (UseThreats)
|
||||
if constexpr (UseThreats)
|
||||
{
|
||||
read_little_endian<ThreatWeightType>(stream, threatWeights.data(),
|
||||
ThreatInputDimensions * HalfDimensions);
|
||||
read_leb_128<WeightType>(stream, weights);
|
||||
read_leb_128(stream, weights);
|
||||
|
||||
auto combinedPsqtWeights =
|
||||
std::make_unique<std::array<PSQTWeightType, TotalInputDimensions * PSQTBuckets>>();
|
||||
|
||||
read_leb_128<PSQTWeightType>(stream, *combinedPsqtWeights);
|
||||
|
||||
std::copy(combinedPsqtWeights->begin(),
|
||||
combinedPsqtWeights->begin() + ThreatInputDimensions * PSQTBuckets,
|
||||
std::begin(threatPsqtWeights));
|
||||
|
||||
std::copy(combinedPsqtWeights->begin() + ThreatInputDimensions * PSQTBuckets,
|
||||
combinedPsqtWeights->begin()
|
||||
+ (ThreatInputDimensions + InputDimensions) * PSQTBuckets,
|
||||
std::begin(psqtWeights));
|
||||
read_leb_128(stream, threatPsqtWeights, psqtWeights);
|
||||
}
|
||||
else
|
||||
{
|
||||
read_leb_128<WeightType>(stream, weights);
|
||||
read_leb_128<PSQTWeightType>(stream, psqtWeights);
|
||||
read_leb_128(stream, weights);
|
||||
read_leb_128(stream, psqtWeights);
|
||||
}
|
||||
|
||||
permute_weights();
|
||||
|
||||
if (!UseThreats)
|
||||
if constexpr (!UseThreats)
|
||||
scale_weights(true);
|
||||
|
||||
return !stream.fail();
|
||||
@@ -197,12 +183,12 @@ class FeatureTransformer {
|
||||
|
||||
copy->unpermute_weights();
|
||||
|
||||
if (!UseThreats)
|
||||
if constexpr (!UseThreats)
|
||||
copy->scale_weights(false);
|
||||
|
||||
write_leb_128<BiasType>(stream, copy->biases);
|
||||
|
||||
if (UseThreats)
|
||||
if constexpr (UseThreats)
|
||||
{
|
||||
write_little_endian<ThreatWeightType>(stream, copy->threatWeights.data(),
|
||||
ThreatInputDimensions * HalfDimensions);
|
||||
@@ -256,7 +242,7 @@ class FeatureTransformer {
|
||||
auto psqt =
|
||||
(psqtAccumulation[perspectives[0]][bucket] - psqtAccumulation[perspectives[1]][bucket]);
|
||||
|
||||
if (UseThreats)
|
||||
if constexpr (UseThreats)
|
||||
{
|
||||
const auto& threatPsqtAccumulation =
|
||||
(threatAccumulatorState.acc<HalfDimensions>()).psqtAccumulation;
|
||||
@@ -348,7 +334,7 @@ class FeatureTransformer {
|
||||
#else
|
||||
6;
|
||||
#endif
|
||||
if (UseThreats)
|
||||
if constexpr (UseThreats)
|
||||
{
|
||||
const vec_t* tin0 =
|
||||
reinterpret_cast<const vec_t*>(&(threatAccumulation[perspectives[p]][0]));
|
||||
@@ -400,7 +386,7 @@ class FeatureTransformer {
|
||||
BiasType sum1 =
|
||||
accumulation[static_cast<int>(perspectives[p])][j + HalfDimensions / 2];
|
||||
|
||||
if (UseThreats)
|
||||
if constexpr (UseThreats)
|
||||
{
|
||||
BiasType sum0t = threatAccumulation[static_cast<int>(perspectives[p])][j + 0];
|
||||
BiasType sum1t =
|
||||
|
||||
+11
-5
@@ -181,11 +181,17 @@ inline __m128i vec_convert_8_16(uint64_t x) {
|
||||
#define MaxChunkSize 16
|
||||
|
||||
#elif USE_NEON
|
||||
using vec_t = int16x8_t;
|
||||
using vec_i8_t = int8x16_t;
|
||||
using psqt_vec_t = int32x4_t;
|
||||
using vec128_t = uint16x8_t;
|
||||
using vec_uint_t = uint32x4_t;
|
||||
using vec_i8x8_t __attribute__((may_alias)) = int8x8_t;
|
||||
using vec_i16x8_t __attribute__((may_alias)) = int16x8_t;
|
||||
using vec_i8x16_t __attribute__((may_alias)) = int8x16_t;
|
||||
using vec_u16x8_t __attribute__((may_alias)) = uint16x8_t;
|
||||
using vec_i32x4_t __attribute__((may_alias)) = int32x4_t;
|
||||
|
||||
using vec_t __attribute__((may_alias)) = int16x8_t;
|
||||
using vec_i8_t __attribute__((may_alias)) = int8x16_t;
|
||||
using psqt_vec_t __attribute__((may_alias)) = int32x4_t;
|
||||
using vec128_t __attribute__((may_alias)) = uint16x8_t;
|
||||
using vec_uint_t __attribute__((may_alias)) = uint32x4_t;
|
||||
#define vec_load(a) (*(a))
|
||||
#define vec_store(a, b) *(a) = (b)
|
||||
#define vec_add_16(a, b) vaddq_s16(a, b)
|
||||
|
||||
+334
-98
@@ -371,6 +371,50 @@ inline WindowsAffinity get_process_affinity() {
|
||||
return affinity;
|
||||
}
|
||||
|
||||
// Type machinery used to emulate Cache->GroupCount
|
||||
|
||||
template<typename T, typename = void>
|
||||
struct HasGroupCount: std::false_type {};
|
||||
|
||||
template<typename T>
|
||||
struct HasGroupCount<T, std::void_t<decltype(std::declval<T>().Cache.GroupCount)>>: std::true_type {
|
||||
};
|
||||
|
||||
template<typename T, typename Pred, std::enable_if_t<HasGroupCount<T>::value, bool> = true>
|
||||
std::set<CpuIndex> readCacheMembers(const T* info, Pred&& is_cpu_allowed) {
|
||||
std::set<CpuIndex> cpus;
|
||||
// On Windows 10 this will read a 0 because GroupCount doesn't exist
|
||||
int groupCount = std::max(info->Cache.GroupCount, WORD(1));
|
||||
for (WORD procGroup = 0; procGroup < groupCount; ++procGroup)
|
||||
{
|
||||
for (BYTE number = 0; number < WIN_PROCESSOR_GROUP_SIZE; ++number)
|
||||
{
|
||||
WORD groupNumber = info->Cache.GroupMasks[procGroup].Group;
|
||||
const CpuIndex c = static_cast<CpuIndex>(groupNumber) * WIN_PROCESSOR_GROUP_SIZE
|
||||
+ static_cast<CpuIndex>(number);
|
||||
if (!(info->Cache.GroupMasks[procGroup].Mask & (1ULL << number)) || !is_cpu_allowed(c))
|
||||
continue;
|
||||
cpus.insert(c);
|
||||
}
|
||||
}
|
||||
return cpus;
|
||||
}
|
||||
|
||||
template<typename T, typename Pred, std::enable_if_t<!HasGroupCount<T>::value, bool> = true>
|
||||
std::set<CpuIndex> readCacheMembers(const T* info, Pred&& is_cpu_allowed) {
|
||||
std::set<CpuIndex> cpus;
|
||||
for (BYTE number = 0; number < WIN_PROCESSOR_GROUP_SIZE; ++number)
|
||||
{
|
||||
WORD groupNumber = info->Cache.GroupMask.Group;
|
||||
const CpuIndex c = static_cast<CpuIndex>(groupNumber) * WIN_PROCESSOR_GROUP_SIZE
|
||||
+ static_cast<CpuIndex>(number);
|
||||
if (!(info->Cache.GroupMask.Mask & (1ULL << number)) || !is_cpu_allowed(c))
|
||||
continue;
|
||||
cpus.insert(c);
|
||||
}
|
||||
return cpus;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
@@ -447,14 +491,39 @@ class NumaReplicatedAccessToken {
|
||||
NumaIndex n;
|
||||
};
|
||||
|
||||
struct L3Domain {
|
||||
NumaIndex systemNumaIndex{};
|
||||
std::set<CpuIndex> cpus{};
|
||||
};
|
||||
|
||||
// Use system NUMA nodes
|
||||
struct SystemNumaPolicy {};
|
||||
// Use system-reported L3 domains
|
||||
struct L3DomainsPolicy {};
|
||||
// Group system-reported L3 domains until they reach bundleSize
|
||||
struct BundledL3Policy {
|
||||
size_t bundleSize;
|
||||
};
|
||||
|
||||
using NumaAutoPolicy = std::variant<SystemNumaPolicy, L3DomainsPolicy, BundledL3Policy>;
|
||||
|
||||
// Designed as immutable, because there is no good reason to alter an already
|
||||
// existing config in a way that doesn't require recreating it completely, and
|
||||
// it would be complex and expensive to maintain class invariants.
|
||||
// The CPU (processor) numbers always correspond to the actual numbering used
|
||||
// by the system. The NUMA node numbers MAY NOT correspond to the system's
|
||||
// numbering of the NUMA nodes. In particular, empty nodes may be removed, or
|
||||
// the user may create custom nodes. It is guaranteed that NUMA nodes are NOT
|
||||
// empty: every node exposed by NumaConfig has at least one processor assigned.
|
||||
// numbering of the NUMA nodes. In particular, by default, if the processor has
|
||||
// non-uniform cache access within a NUMA node (i.e., a non-unified L3 cache structure),
|
||||
// then L3 domains within a system NUMA node will be used to subdivide it
|
||||
// into multiple logical NUMA nodes in the config. Additionally, empty nodes may
|
||||
// be removed, or the user may create custom nodes.
|
||||
//
|
||||
// As a special case, when performing system-wide replication of read-only data
|
||||
// (i.e., LazyNumaReplicatedSystemWide), the system NUMA node is used, rather than
|
||||
// custom or L3-aware nodes. See that class's get_discriminator() function.
|
||||
//
|
||||
// It is guaranteed that NUMA nodes are NOT empty: every node exposed by NumaConfig
|
||||
// has at least one processor assigned.
|
||||
//
|
||||
// We use startup affinities so as not to modify its own behaviour in time.
|
||||
//
|
||||
@@ -469,78 +538,19 @@ class NumaConfig {
|
||||
add_cpu_range_to_node(NumaIndex{0}, CpuIndex{0}, numCpus - 1);
|
||||
}
|
||||
|
||||
// This function queries the system for the mapping of processors to NUMA nodes.
|
||||
// On Linux we read from standardized kernel sysfs, with a fallback to single NUMA
|
||||
// node. On Windows we utilize GetNumaProcessorNodeEx, which has its quirks, see
|
||||
// comment for Windows implementation of get_process_affinity.
|
||||
static NumaConfig from_system([[maybe_unused]] bool respectProcessAffinity = true) {
|
||||
// This function gets a NumaConfig based on the system's provided information.
|
||||
// The available policies are documented above.
|
||||
static NumaConfig from_system([[maybe_unused]] const NumaAutoPolicy& policy,
|
||||
bool respectProcessAffinity = true) {
|
||||
NumaConfig cfg = empty();
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#if !((defined(__linux__) && !defined(__ANDROID__)) || defined(_WIN64))
|
||||
// Fallback for unsupported systems.
|
||||
for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c)
|
||||
cfg.add_cpu_to_node(NumaIndex{0}, c);
|
||||
#else
|
||||
|
||||
std::set<CpuIndex> allowedCpus;
|
||||
|
||||
if (respectProcessAffinity)
|
||||
allowedCpus = STARTUP_PROCESSOR_AFFINITY;
|
||||
|
||||
auto is_cpu_allowed = [respectProcessAffinity, &allowedCpus](CpuIndex c) {
|
||||
return !respectProcessAffinity || allowedCpus.count(c) == 1;
|
||||
};
|
||||
|
||||
// On Linux things are straightforward, since there's no processor groups and
|
||||
// any thread can be scheduled on all processors.
|
||||
// We try to gather this information from the sysfs first
|
||||
// https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-devices-node
|
||||
|
||||
bool useFallback = false;
|
||||
auto fallback = [&]() {
|
||||
useFallback = true;
|
||||
cfg = empty();
|
||||
};
|
||||
|
||||
// /sys/devices/system/node/online contains information about active NUMA nodes
|
||||
auto nodeIdsStr = read_file_to_string("/sys/devices/system/node/online");
|
||||
if (!nodeIdsStr.has_value() || nodeIdsStr->empty())
|
||||
{
|
||||
fallback();
|
||||
}
|
||||
else
|
||||
{
|
||||
remove_whitespace(*nodeIdsStr);
|
||||
for (size_t n : indices_from_shortened_string(*nodeIdsStr))
|
||||
{
|
||||
// /sys/devices/system/node/node.../cpulist
|
||||
std::string path =
|
||||
std::string("/sys/devices/system/node/node") + std::to_string(n) + "/cpulist";
|
||||
auto cpuIdsStr = read_file_to_string(path);
|
||||
// Now, we only bail if the file does not exist. Some nodes may be
|
||||
// empty, that's fine. An empty node still has a file that appears
|
||||
// to have some whitespace, so we need to handle that.
|
||||
if (!cpuIdsStr.has_value())
|
||||
{
|
||||
fallback();
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
remove_whitespace(*cpuIdsStr);
|
||||
for (size_t c : indices_from_shortened_string(*cpuIdsStr))
|
||||
{
|
||||
if (is_cpu_allowed(c))
|
||||
cfg.add_cpu_to_node(n, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useFallback)
|
||||
{
|
||||
for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c)
|
||||
if (is_cpu_allowed(c))
|
||||
cfg.add_cpu_to_node(NumaIndex{0}, c);
|
||||
}
|
||||
|
||||
#elif defined(_WIN64)
|
||||
#if defined(_WIN64)
|
||||
|
||||
std::optional<std::set<CpuIndex>> allowedCpus;
|
||||
|
||||
@@ -555,28 +565,38 @@ class NumaConfig {
|
||||
return !allowedCpus.has_value() || allowedCpus->count(c) == 1;
|
||||
};
|
||||
|
||||
WORD numProcGroups = GetActiveProcessorGroupCount();
|
||||
for (WORD procGroup = 0; procGroup < numProcGroups; ++procGroup)
|
||||
{
|
||||
for (BYTE number = 0; number < WIN_PROCESSOR_GROUP_SIZE; ++number)
|
||||
{
|
||||
PROCESSOR_NUMBER procnum;
|
||||
procnum.Group = procGroup;
|
||||
procnum.Number = number;
|
||||
procnum.Reserved = 0;
|
||||
USHORT nodeNumber;
|
||||
#elif defined(__linux__) && !defined(__ANDROID__)
|
||||
|
||||
const BOOL status = GetNumaProcessorNodeEx(&procnum, &nodeNumber);
|
||||
const CpuIndex c = static_cast<CpuIndex>(procGroup) * WIN_PROCESSOR_GROUP_SIZE
|
||||
+ static_cast<CpuIndex>(number);
|
||||
if (status != 0 && nodeNumber != std::numeric_limits<USHORT>::max()
|
||||
&& is_cpu_allowed(c))
|
||||
{
|
||||
cfg.add_cpu_to_node(nodeNumber, c);
|
||||
}
|
||||
std::set<CpuIndex> allowedCpus;
|
||||
|
||||
if (respectProcessAffinity)
|
||||
allowedCpus = STARTUP_PROCESSOR_AFFINITY;
|
||||
|
||||
auto is_cpu_allowed = [respectProcessAffinity, &allowedCpus](CpuIndex c) {
|
||||
return !respectProcessAffinity || allowedCpus.count(c) == 1;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
bool l3Success = false;
|
||||
if (!std::holds_alternative<SystemNumaPolicy>(policy))
|
||||
{
|
||||
size_t l3BundleSize = 0;
|
||||
if (const auto* v = std::get_if<BundledL3Policy>(&policy))
|
||||
{
|
||||
l3BundleSize = v->bundleSize;
|
||||
}
|
||||
if (auto l3Cfg =
|
||||
try_get_l3_aware_config(respectProcessAffinity, l3BundleSize, is_cpu_allowed))
|
||||
{
|
||||
cfg = std::move(*l3Cfg);
|
||||
l3Success = true;
|
||||
}
|
||||
}
|
||||
if (!l3Success)
|
||||
cfg = from_system_numa(respectProcessAffinity, is_cpu_allowed);
|
||||
|
||||
#if defined(_WIN64)
|
||||
// Split the NUMA nodes to be contained within a group if necessary.
|
||||
// This is needed between Windows 10 Build 20348 and Windows 11, because
|
||||
// the new NUMA allocation behaviour was introduced while there was
|
||||
@@ -623,12 +643,7 @@ class NumaConfig {
|
||||
|
||||
cfg = std::move(splitCfg);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Fallback for unsupported systems.
|
||||
for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c)
|
||||
cfg.add_cpu_to_node(NumaIndex{0}, c);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1041,6 +1056,227 @@ class NumaConfig {
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
// This function queries the system for the mapping of processors to NUMA nodes.
|
||||
// On Linux we read from standardized kernel sysfs, with a fallback to single NUMA
|
||||
// node. On Windows we utilize GetNumaProcessorNodeEx, which has its quirks, see
|
||||
// comment for Windows implementation of get_process_affinity.
|
||||
template<typename Pred>
|
||||
static NumaConfig from_system_numa([[maybe_unused]] bool respectProcessAffinity,
|
||||
[[maybe_unused]] Pred&& is_cpu_allowed) {
|
||||
NumaConfig cfg = empty();
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
|
||||
// On Linux things are straightforward, since there's no processor groups and
|
||||
// any thread can be scheduled on all processors.
|
||||
// We try to gather this information from the sysfs first
|
||||
// https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-devices-node
|
||||
|
||||
bool useFallback = false;
|
||||
auto fallback = [&]() {
|
||||
useFallback = true;
|
||||
cfg = empty();
|
||||
};
|
||||
|
||||
// /sys/devices/system/node/online contains information about active NUMA nodes
|
||||
auto nodeIdsStr = read_file_to_string("/sys/devices/system/node/online");
|
||||
if (!nodeIdsStr.has_value() || nodeIdsStr->empty())
|
||||
{
|
||||
fallback();
|
||||
}
|
||||
else
|
||||
{
|
||||
remove_whitespace(*nodeIdsStr);
|
||||
for (size_t n : indices_from_shortened_string(*nodeIdsStr))
|
||||
{
|
||||
// /sys/devices/system/node/node.../cpulist
|
||||
std::string path =
|
||||
std::string("/sys/devices/system/node/node") + std::to_string(n) + "/cpulist";
|
||||
auto cpuIdsStr = read_file_to_string(path);
|
||||
// Now, we only bail if the file does not exist. Some nodes may be
|
||||
// empty, that's fine. An empty node still has a file that appears
|
||||
// to have some whitespace, so we need to handle that.
|
||||
if (!cpuIdsStr.has_value())
|
||||
{
|
||||
fallback();
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
remove_whitespace(*cpuIdsStr);
|
||||
for (size_t c : indices_from_shortened_string(*cpuIdsStr))
|
||||
{
|
||||
if (is_cpu_allowed(c))
|
||||
cfg.add_cpu_to_node(n, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useFallback)
|
||||
{
|
||||
for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c)
|
||||
if (is_cpu_allowed(c))
|
||||
cfg.add_cpu_to_node(NumaIndex{0}, c);
|
||||
}
|
||||
|
||||
#elif defined(_WIN64)
|
||||
|
||||
WORD numProcGroups = GetActiveProcessorGroupCount();
|
||||
for (WORD procGroup = 0; procGroup < numProcGroups; ++procGroup)
|
||||
{
|
||||
for (BYTE number = 0; number < WIN_PROCESSOR_GROUP_SIZE; ++number)
|
||||
{
|
||||
PROCESSOR_NUMBER procnum;
|
||||
procnum.Group = procGroup;
|
||||
procnum.Number = number;
|
||||
procnum.Reserved = 0;
|
||||
USHORT nodeNumber;
|
||||
|
||||
const BOOL status = GetNumaProcessorNodeEx(&procnum, &nodeNumber);
|
||||
const CpuIndex c = static_cast<CpuIndex>(procGroup) * WIN_PROCESSOR_GROUP_SIZE
|
||||
+ static_cast<CpuIndex>(number);
|
||||
if (status != 0 && nodeNumber != std::numeric_limits<USHORT>::max()
|
||||
&& is_cpu_allowed(c))
|
||||
{
|
||||
cfg.add_cpu_to_node(nodeNumber, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
abort(); // should not reach here
|
||||
|
||||
#endif
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
template<typename Pred>
|
||||
static std::optional<NumaConfig> try_get_l3_aware_config(
|
||||
bool respectProcessAffinity, size_t bundleSize, [[maybe_unused]] Pred&& is_cpu_allowed) {
|
||||
// Get the normal system configuration so we know to which NUMA node
|
||||
// each L3 domain belongs.
|
||||
NumaConfig systemConfig =
|
||||
NumaConfig::from_system(SystemNumaPolicy{}, respectProcessAffinity);
|
||||
std::vector<L3Domain> l3Domains;
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
|
||||
std::set<CpuIndex> seenCpus;
|
||||
auto nextUnseenCpu = [&seenCpus]() {
|
||||
for (CpuIndex i = 0;; ++i)
|
||||
if (!seenCpus.count(i))
|
||||
return i;
|
||||
};
|
||||
|
||||
while (true)
|
||||
{
|
||||
CpuIndex next = nextUnseenCpu();
|
||||
auto siblingsStr =
|
||||
read_file_to_string("/sys/devices/system/cpu/cpu" + std::to_string(next)
|
||||
+ "/cache/index3/shared_cpu_list");
|
||||
|
||||
if (!siblingsStr.has_value() || siblingsStr->empty())
|
||||
{
|
||||
break; // we have read all available CPUs
|
||||
}
|
||||
|
||||
L3Domain domain;
|
||||
for (size_t c : indices_from_shortened_string(*siblingsStr))
|
||||
{
|
||||
if (is_cpu_allowed(c))
|
||||
{
|
||||
domain.systemNumaIndex = systemConfig.nodeByCpu.at(c);
|
||||
domain.cpus.insert(c);
|
||||
}
|
||||
seenCpus.insert(c);
|
||||
}
|
||||
if (!domain.cpus.empty())
|
||||
{
|
||||
l3Domains.emplace_back(std::move(domain));
|
||||
}
|
||||
}
|
||||
|
||||
#elif defined(_WIN64)
|
||||
|
||||
DWORD bufSize = 0;
|
||||
GetLogicalProcessorInformationEx(RelationCache, nullptr, &bufSize);
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
|
||||
return std::nullopt;
|
||||
|
||||
std::vector<char> buffer(bufSize);
|
||||
auto info = reinterpret_cast<PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>(buffer.data());
|
||||
if (!GetLogicalProcessorInformationEx(RelationCache, info, &bufSize))
|
||||
return std::nullopt;
|
||||
|
||||
while (reinterpret_cast<char*>(info) < buffer.data() + bufSize)
|
||||
{
|
||||
info = std::launder(info);
|
||||
if (info->Relationship == RelationCache && info->Cache.Level == 3)
|
||||
{
|
||||
L3Domain domain{};
|
||||
domain.cpus = readCacheMembers(info, is_cpu_allowed);
|
||||
if (!domain.cpus.empty())
|
||||
{
|
||||
domain.systemNumaIndex = systemConfig.nodeByCpu.at(*domain.cpus.begin());
|
||||
l3Domains.push_back(std::move(domain));
|
||||
}
|
||||
}
|
||||
// Variable length data structure, advance to next
|
||||
info = reinterpret_cast<PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>(
|
||||
reinterpret_cast<char*>(info) + info->Size);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!l3Domains.empty())
|
||||
return {NumaConfig::from_l3_info(std::move(l3Domains), bundleSize)};
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
static NumaConfig from_l3_info(std::vector<L3Domain>&& domains, size_t bundleSize) {
|
||||
assert(!domains.empty());
|
||||
|
||||
std::map<NumaIndex, std::vector<L3Domain>> list;
|
||||
for (auto& d : domains)
|
||||
list[d.systemNumaIndex].emplace_back(std::move(d));
|
||||
|
||||
NumaConfig cfg = empty();
|
||||
NumaIndex n = 0;
|
||||
for (auto& [_, ds] : list)
|
||||
{
|
||||
bool changed;
|
||||
// Scan through pairs and merge them. With roughly equal L3 sizes, should give
|
||||
// a decent distribution.
|
||||
do
|
||||
{
|
||||
changed = false;
|
||||
for (size_t j = 0; j + 1 < ds.size(); ++j)
|
||||
{
|
||||
if (ds[j].cpus.size() + ds[j + 1].cpus.size() <= bundleSize)
|
||||
{
|
||||
changed = true;
|
||||
ds[j].cpus.merge(ds[j + 1].cpus);
|
||||
ds.erase(ds.begin() + j + 1);
|
||||
}
|
||||
}
|
||||
// ds.size() has decreased if changed is true, so this loop will terminate
|
||||
} while (changed);
|
||||
for (const L3Domain& d : ds)
|
||||
{
|
||||
const NumaIndex dn = n++;
|
||||
for (CpuIndex cpu : d.cpus)
|
||||
{
|
||||
cfg.add_cpu_to_node(dn, cpu);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
};
|
||||
|
||||
class NumaReplicationContext;
|
||||
@@ -1345,12 +1581,12 @@ class LazyNumaReplicatedSystemWide: public NumaReplicatedBase {
|
||||
|
||||
std::size_t get_discriminator(NumaIndex idx) const {
|
||||
const NumaConfig& cfg = get_numa_config();
|
||||
const NumaConfig& cfg_sys = NumaConfig::from_system(false);
|
||||
const NumaConfig& cfg_sys = NumaConfig::from_system(SystemNumaPolicy{}, false);
|
||||
// as a discriminator, locate the hardware/system numadomain this cpuindex belongs to
|
||||
CpuIndex cpu = *cfg.nodes[idx].begin(); // get a CpuIndex from NumaIndex
|
||||
NumaIndex sys_idx = cfg_sys.is_cpu_assigned(cpu) ? cfg_sys.nodeByCpu.at(cpu) : 0;
|
||||
std::string s = cfg_sys.to_string() + "$" + std::to_string(sys_idx);
|
||||
return std::hash<std::string>{}(s);
|
||||
return static_cast<std::size_t>(hash_string(s));
|
||||
}
|
||||
|
||||
void ensure_present(NumaIndex idx) const {
|
||||
|
||||
+1
-2
@@ -21,7 +21,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "cluster.h"
|
||||
#include "movegen.h"
|
||||
#include "position.h"
|
||||
#include "types.h"
|
||||
@@ -50,7 +49,7 @@ uint64_t perft(Position& pos, Depth depth) {
|
||||
nodes += cnt;
|
||||
pos.undo_move(m);
|
||||
}
|
||||
if (Root && Distributed::is_root())
|
||||
if (Root)
|
||||
sync_cout << UCIEngine::move(m, pos.is_chess960()) << ": " << cnt << sync_endl;
|
||||
}
|
||||
return nodes;
|
||||
|
||||
+12
-5
@@ -66,12 +66,15 @@ std::ostream& operator<<(std::ostream& os, const Position& pos) {
|
||||
|
||||
os << "\n +---+---+---+---+---+---+---+---+\n";
|
||||
|
||||
for (Rank r = RANK_8; r >= RANK_1; --r)
|
||||
for (Rank r = RANK_8;; --r)
|
||||
{
|
||||
for (File f = FILE_A; f <= FILE_H; ++f)
|
||||
os << " | " << PieceToChar[pos.piece_on(make_square(f, r))];
|
||||
|
||||
os << " | " << (1 + r) << "\n +---+---+---+---+---+---+---+---+\n";
|
||||
|
||||
if (r == RANK_1)
|
||||
break;
|
||||
}
|
||||
|
||||
os << " a b c d e f g h\n"
|
||||
@@ -416,7 +419,7 @@ string Position::fen() const {
|
||||
int emptyCnt;
|
||||
std::ostringstream ss;
|
||||
|
||||
for (Rank r = RANK_8; r >= RANK_1; --r)
|
||||
for (Rank r = RANK_8;; --r)
|
||||
{
|
||||
for (File f = FILE_A; f <= FILE_H; ++f)
|
||||
{
|
||||
@@ -430,8 +433,9 @@ string Position::fen() const {
|
||||
ss << PieceToChar[piece_on(make_square(f, r))];
|
||||
}
|
||||
|
||||
if (r > RANK_1)
|
||||
ss << '/';
|
||||
if (r == RANK_1)
|
||||
break;
|
||||
ss << '/';
|
||||
}
|
||||
|
||||
ss << (sideToMove == WHITE ? " w " : " b ");
|
||||
@@ -1477,10 +1481,13 @@ void Position::flip() {
|
||||
string f, token;
|
||||
std::stringstream ss(fen());
|
||||
|
||||
for (Rank r = RANK_8; r >= RANK_1; --r) // Piece placement
|
||||
for (Rank r = RANK_8;; --r) // Piece placement
|
||||
{
|
||||
std::getline(ss, token, r > RANK_1 ? '/' : ' ');
|
||||
f.insert(0, token + (f.empty() ? " " : "/"));
|
||||
|
||||
if (r == RANK_1)
|
||||
break;
|
||||
}
|
||||
|
||||
ss >> token; // Active color
|
||||
|
||||
+46
-175
@@ -34,7 +34,6 @@
|
||||
#include <utility>
|
||||
|
||||
#include "bitboard.h"
|
||||
#include "cluster.h"
|
||||
#include "evaluate.h"
|
||||
#include "history.h"
|
||||
#include "misc.h"
|
||||
@@ -199,9 +198,8 @@ void Search::Worker::start_searching() {
|
||||
if (rootMoves.empty())
|
||||
{
|
||||
rootMoves.emplace_back(Move::none());
|
||||
if (Distributed::is_root())
|
||||
main_manager()->updates.onUpdateNoMoves(
|
||||
{0, {rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos}});
|
||||
main_manager()->updates.onUpdateNoMoves(
|
||||
{0, {rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos}});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -215,24 +213,19 @@ void Search::Worker::start_searching() {
|
||||
// GUI sends a "stop" or "ponderhit" command. We therefore simply wait here
|
||||
// until the GUI sends one of those commands.
|
||||
while (!threads.stop && (main_manager()->ponder || limits.infinite))
|
||||
{
|
||||
Distributed::signals_poll(threads);
|
||||
} // Busy wait for a stop or a ponder reset
|
||||
{} // Busy wait for a stop or a ponder reset
|
||||
|
||||
// Stop the threads if not already stopped (also raise the stop if
|
||||
// "ponderhit" just reset threads.ponder)
|
||||
threads.stop = true;
|
||||
|
||||
// Signal and synchronize all other ranks
|
||||
Distributed::signals_sync(threads);
|
||||
|
||||
// Wait until all threads have finished
|
||||
threads.wait_for_search_finished();
|
||||
|
||||
// When playing in 'nodes as time' mode, subtract the searched nodes from
|
||||
// the available ones before exiting.
|
||||
if (limits.npmsec)
|
||||
main_manager()->tm.advance_nodes_time(Distributed::nodes_searched(threads)
|
||||
main_manager()->tm.advance_nodes_time(threads.nodes_searched()
|
||||
- limits.inc[rootPos.side_to_move()]);
|
||||
|
||||
Worker* bestThread = this;
|
||||
@@ -246,53 +239,18 @@ void Search::Worker::start_searching() {
|
||||
main_manager()->bestPreviousScore = bestThread->rootMoves[0].score;
|
||||
main_manager()->bestPreviousAverageScore = bestThread->rootMoves[0].averageScore;
|
||||
|
||||
// Temporarily switch out onUpdateFull to capture the PV information that we need,
|
||||
// so that we can exchange it through MPI. (We may end up not actually printing
|
||||
// it out.)
|
||||
auto oldOnUpdateFull = std::move(main_manager()->updates.onUpdateFull);
|
||||
std::vector<std::vector<char>> serializedInfo; // One for each MultiPV.
|
||||
main_manager()->updates.onUpdateFull = [&](const InfoFull& info) {
|
||||
serializedInfo.push_back(info.serialize());
|
||||
};
|
||||
main_manager()->pv(*bestThread, threads, tt, bestThread->completedDepth);
|
||||
assert(!serializedInfo.empty());
|
||||
main_manager()->updates.onUpdateFull = std::move(oldOnUpdateFull);
|
||||
// Send again PV info if we have a new best thread
|
||||
if (bestThread != this)
|
||||
main_manager()->pv(*bestThread, threads, tt, bestThread->completedDepth);
|
||||
|
||||
std::string ponder;
|
||||
|
||||
Move bestMove = bestThread->rootMoves[0].pv[0];
|
||||
Move ponderMove = Move::none();
|
||||
if (bestThread->rootMoves[0].pv.size() > 1
|
||||
|| bestThread->rootMoves[0].extract_ponder_from_tt(tt, rootPos))
|
||||
ponderMove = bestThread->rootMoves[0].pv[1];
|
||||
ponder = UCIEngine::move(bestThread->rootMoves[0].pv[1], rootPos.is_chess960());
|
||||
|
||||
// Exchange info as needed
|
||||
Distributed::MoveInfo mi{bestMove.raw(), ponderMove.raw(), bestThread->completedDepth,
|
||||
bestThread->rootMoves[0].score, Distributed::rank()};
|
||||
Distributed::pick_moves(mi, serializedInfo);
|
||||
|
||||
main_manager()->bestPreviousScore = static_cast<Value>(mi.score);
|
||||
|
||||
if (Distributed::is_root())
|
||||
{
|
||||
// Send again PV info if we have a new best thread/rank
|
||||
if (bestThread != this || mi.rank != 0)
|
||||
{
|
||||
for (const auto& serializedInfoOne : serializedInfo)
|
||||
{
|
||||
Search::InfoFull info = Search::InfoFull::unserialize(serializedInfoOne);
|
||||
main_manager()->updates.onUpdateFull(info);
|
||||
}
|
||||
}
|
||||
|
||||
bestMove = static_cast<Move>(mi.move);
|
||||
ponderMove = static_cast<Move>(mi.ponder);
|
||||
|
||||
std::string ponder;
|
||||
if (ponderMove != Move::none())
|
||||
ponder = UCIEngine::move(ponderMove, rootPos.is_chess960());
|
||||
|
||||
auto bestmove = UCIEngine::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960());
|
||||
main_manager()->updates.onBestmove(bestmove, ponder);
|
||||
}
|
||||
auto bestmove = UCIEngine::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960());
|
||||
main_manager()->updates.onBestmove(bestmove, ponder);
|
||||
}
|
||||
|
||||
// Main iterative deepening loop. It calls search()
|
||||
@@ -362,7 +320,7 @@ void Search::Worker::iterative_deepening() {
|
||||
|
||||
// Iterative deepening loop until requested to stop or the target depth is reached
|
||||
while (++rootDepth < MAX_PLY && !threads.stop
|
||||
&& !(limits.depth && mainThread && Distributed::is_root() && rootDepth > limits.depth))
|
||||
&& !(limits.depth && mainThread && rootDepth > limits.depth))
|
||||
{
|
||||
// Age out PV variability metric
|
||||
if (mainThread)
|
||||
@@ -433,12 +391,9 @@ void Search::Worker::iterative_deepening() {
|
||||
// When failing high/low give some update before a re-search. To avoid
|
||||
// excessive output that could hang GUIs like Fritz 19, only start
|
||||
// at nodes > 10M (rather than depth N, which can be reached quickly)
|
||||
if (Distributed::is_root() && mainThread && multiPV == 1
|
||||
&& (bestValue <= alpha || bestValue >= beta) && nodes > 10000000)
|
||||
{
|
||||
if (mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta)
|
||||
&& nodes > 10000000)
|
||||
main_manager()->pv(*this, threads, tt, rootDepth);
|
||||
Distributed::cluster_info(threads, rootDepth, elapsed());
|
||||
}
|
||||
|
||||
// In case of failing low/high increase aspiration window and re-search,
|
||||
// otherwise exit the loop.
|
||||
@@ -468,7 +423,7 @@ void Search::Worker::iterative_deepening() {
|
||||
// Sort the PV lines searched so far and update the GUI
|
||||
std::stable_sort(rootMoves.begin() + pvFirst, rootMoves.begin() + pvIdx + 1);
|
||||
|
||||
if (Distributed::is_root() && mainThread
|
||||
if (mainThread
|
||||
&& (threads.stop || pvIdx + 1 == multiPV || nodes > 10000000)
|
||||
// A thread that aborted search can have mated-in/TB-loss PV and
|
||||
// score that cannot be trusted, i.e. it can be delayed or refuted
|
||||
@@ -476,10 +431,7 @@ void Search::Worker::iterative_deepening() {
|
||||
// we suppress this output and below pick a proven score/PV for this
|
||||
// thread (from the previous iteration).
|
||||
&& !(threads.abortedSearch && is_loss(rootMoves[0].uciScore)))
|
||||
{
|
||||
main_manager()->pv(*this, threads, tt, rootDepth);
|
||||
Distributed::cluster_info(threads, rootDepth, elapsed() + 1);
|
||||
}
|
||||
|
||||
if (threads.stop)
|
||||
break;
|
||||
@@ -785,8 +737,8 @@ Value Search::Worker::search(
|
||||
ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, correctionValue);
|
||||
|
||||
// Static evaluation is saved as it was before adjustment by correction history
|
||||
Distributed::save(tt, threads, this, ttWriter, posKey, VALUE_NONE, ss->ttPv, BOUND_NONE,
|
||||
DEPTH_UNSEARCHED, Move::none(), unadjustedStaticEval, tt.generation());
|
||||
ttWriter.write(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_UNSEARCHED, Move::none(),
|
||||
unadjustedStaticEval, tt.generation());
|
||||
}
|
||||
|
||||
// Set up the improving flag, which is true if current static evaluation is
|
||||
@@ -883,9 +835,9 @@ Value Search::Worker::search(
|
||||
|
||||
if (b == BOUND_EXACT || (b == BOUND_LOWER ? value >= beta : value <= alpha))
|
||||
{
|
||||
Distributed::save(
|
||||
tt, threads, this, ttWriter, posKey, value_to_tt(value, ss->ply), ss->ttPv, b,
|
||||
std::min(MAX_PLY - 1, depth + 6), Move::none(), VALUE_NONE, tt.generation());
|
||||
ttWriter.write(posKey, value_to_tt(value, ss->ply), ss->ttPv, b,
|
||||
std::min(MAX_PLY - 1, depth + 6), Move::none(), VALUE_NONE,
|
||||
tt.generation());
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -1019,9 +971,8 @@ Value Search::Worker::search(
|
||||
if (value >= probCutBeta)
|
||||
{
|
||||
// Save ProbCut data into transposition table
|
||||
Distributed::save(tt, threads, this, ttWriter, posKey, value_to_tt(value, ss->ply),
|
||||
ss->ttPv, BOUND_LOWER, probCutDepth + 1, move,
|
||||
unadjustedStaticEval, tt.generation());
|
||||
ttWriter.write(posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER,
|
||||
probCutDepth + 1, move, unadjustedStaticEval, tt.generation());
|
||||
|
||||
if (!is_decisive(value))
|
||||
return value - (probCutBeta - beta);
|
||||
@@ -1070,7 +1021,7 @@ moves_loop: // When in check, search starts here
|
||||
|
||||
ss->moveCount = ++moveCount;
|
||||
|
||||
if (rootNode && Distributed::is_root() && is_mainthread() && nodes > 10000000)
|
||||
if (rootNode && is_mainthread() && nodes > 10000000)
|
||||
{
|
||||
main_manager()->updates.onIter(
|
||||
{depth, UCIEngine::move(move, pos.is_chess960()), moveCount + pvIdx});
|
||||
@@ -1255,8 +1206,7 @@ moves_loop: // When in check, search starts here
|
||||
|
||||
// Increase reduction if next ply has a lot of fail high
|
||||
if ((ss + 1)->cutoffCnt > 1)
|
||||
r += 120 + 1024 * ((ss + 1)->cutoffCnt > 2) + 100 * ((ss + 1)->cutoffCnt > 3)
|
||||
+ 1024 * allNode;
|
||||
r += 256 + 1024 * ((ss + 1)->cutoffCnt > 2) + 1024 * allNode;
|
||||
|
||||
// For first picked move (ttMove) reduce reduction
|
||||
if (move == ttData.move)
|
||||
@@ -1482,6 +1432,7 @@ moves_loop: // When in check, search starts here
|
||||
|
||||
bonusScale = std::max(bonusScale, 0);
|
||||
|
||||
// scaledBonus ranges from 0 to roughly 2.3M, overflows happen for multipliers larger than 900
|
||||
const int scaledBonus = std::min(141 * depth - 87, 1351) * bonusScale;
|
||||
|
||||
update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq,
|
||||
@@ -1490,8 +1441,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)
|
||||
sharedHistory.pawn_entry(pos)[pos.piece_on(prevSq)][prevSq]
|
||||
<< scaledBonus * 1160 / 32768;
|
||||
sharedHistory.pawn_entry(pos)[pos.piece_on(prevSq)][prevSq] << scaledBonus * 290 / 8192;
|
||||
}
|
||||
|
||||
// Bonus for prior capture countermove that caused the fail low
|
||||
@@ -1513,13 +1463,12 @@ moves_loop: // When in check, search starts here
|
||||
// Write gathered information in transposition table. Note that the
|
||||
// static evaluation is saved as it was before correction history.
|
||||
if (!excludedMove && !(rootNode && pvIdx))
|
||||
Distributed::save(tt, threads, this, ttWriter, posKey, value_to_tt(bestValue, ss->ply),
|
||||
ss->ttPv,
|
||||
bestValue >= beta ? BOUND_LOWER
|
||||
: PvNode && bestMove ? BOUND_EXACT
|
||||
: BOUND_UPPER,
|
||||
moveCount != 0 ? depth : std::min(MAX_PLY - 1, depth + 6), bestMove,
|
||||
unadjustedStaticEval, tt.generation());
|
||||
ttWriter.write(posKey, value_to_tt(bestValue, ss->ply), ss->ttPv,
|
||||
bestValue >= beta ? BOUND_LOWER
|
||||
: PvNode && bestMove ? BOUND_EXACT
|
||||
: BOUND_UPPER,
|
||||
moveCount != 0 ? depth : std::min(MAX_PLY - 1, depth + 6), bestMove,
|
||||
unadjustedStaticEval, tt.generation());
|
||||
|
||||
// Adjust correction history if the best move is not a capture
|
||||
// and the error direction matches whether we are above/below bounds.
|
||||
@@ -1643,11 +1592,9 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta)
|
||||
bestValue = (bestValue + beta) / 2;
|
||||
|
||||
if (!ss->ttHit)
|
||||
Distributed::save(tt, threads, this, ttWriter, posKey,
|
||||
value_to_tt(bestValue, ss->ply), false, BOUND_LOWER,
|
||||
DEPTH_UNSEARCHED, Move::none(), unadjustedStaticEval,
|
||||
tt.generation());
|
||||
|
||||
ttWriter.write(posKey, value_to_tt(bestValue, ss->ply), false, BOUND_LOWER,
|
||||
DEPTH_UNSEARCHED, Move::none(), unadjustedStaticEval,
|
||||
tt.generation());
|
||||
return bestValue;
|
||||
}
|
||||
|
||||
@@ -1705,7 +1652,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta)
|
||||
// we can prune this move.
|
||||
if (!pos.see_ge(move, alpha - futilityBase))
|
||||
{
|
||||
bestValue = std::min(alpha, futilityBase);
|
||||
bestValue = std::max(bestValue, std::min(alpha, futilityBase));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1776,9 +1723,9 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta)
|
||||
|
||||
// Save gathered info in transposition table. The static evaluation
|
||||
// is saved as it was before adjustment by correction history.
|
||||
Distributed::save(tt, threads, this, ttWriter, posKey, value_to_tt(bestValue, ss->ply), pvHit,
|
||||
bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, DEPTH_QS, bestMove,
|
||||
unadjustedStaticEval, tt.generation());
|
||||
ttWriter.write(posKey, value_to_tt(bestValue, ss->ply), pvHit,
|
||||
bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, DEPTH_QS, bestMove,
|
||||
unadjustedStaticEval, tt.generation());
|
||||
|
||||
assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE);
|
||||
|
||||
@@ -1799,7 +1746,7 @@ Depth Search::Worker::reduction(bool i, Depth d, int mn, int delta) const {
|
||||
// This function is intended for use only when printing PV outputs, and not used
|
||||
// for making decisions within the search algorithm itself.
|
||||
TimePoint Search::Worker::elapsed() const {
|
||||
return main_manager()->tm.elapsed([this]() { return Distributed::nodes_searched(threads); });
|
||||
return main_manager()->tm.elapsed([this]() { return threads.nodes_searched(); });
|
||||
}
|
||||
|
||||
TimePoint Search::Worker::elapsed_time() const { return main_manager()->tm.elapsed_time(); }
|
||||
@@ -2002,9 +1949,8 @@ void SearchManager::check_time(Search::Worker& worker) {
|
||||
|
||||
static TimePoint lastInfoTime = now();
|
||||
|
||||
TimePoint elapsed =
|
||||
tm.elapsed([&worker]() { return Distributed::nodes_searched(worker.threads); });
|
||||
TimePoint tick = worker.limits.startTime + elapsed;
|
||||
TimePoint elapsed = tm.elapsed([&worker]() { return worker.threads.nodes_searched(); });
|
||||
TimePoint tick = worker.limits.startTime + elapsed;
|
||||
|
||||
if (tick - lastInfoTime >= 1000)
|
||||
{
|
||||
@@ -2012,9 +1958,6 @@ void SearchManager::check_time(Search::Worker& worker) {
|
||||
dbg_print();
|
||||
}
|
||||
|
||||
// poll on MPI signals
|
||||
Distributed::signals_poll(worker.threads);
|
||||
|
||||
// We should not stop pondering until told so by the GUI
|
||||
if (ponder)
|
||||
return;
|
||||
@@ -2025,8 +1968,7 @@ void SearchManager::check_time(Search::Worker& worker) {
|
||||
worker.completedDepth >= 1
|
||||
&& ((worker.limits.use_time_management() && (elapsed > tm.maximum() || stopOnPonderhit))
|
||||
|| (worker.limits.movetime && elapsed >= worker.limits.movetime)
|
||||
|| (worker.limits.nodes
|
||||
&& Distributed::nodes_searched(worker.threads) >= worker.limits.nodes)))
|
||||
|| (worker.limits.nodes && worker.threads.nodes_searched() >= worker.limits.nodes)))
|
||||
worker.threads.stop = worker.threads.abortedSearch = true;
|
||||
}
|
||||
|
||||
@@ -2173,13 +2115,12 @@ void SearchManager::pv(Search::Worker& worker,
|
||||
const TranspositionTable& tt,
|
||||
Depth depth) {
|
||||
|
||||
const auto nodes = Distributed::nodes_searched(threads);
|
||||
const auto nodes = threads.nodes_searched();
|
||||
auto& rootMoves = worker.rootMoves;
|
||||
auto& pos = worker.rootPos;
|
||||
size_t pvIdx = worker.pvIdx;
|
||||
size_t multiPV = std::min(size_t(worker.options["MultiPV"]), rootMoves.size());
|
||||
uint64_t tbHits =
|
||||
Distributed::tb_hits(threads) + (worker.tbConfig.rootInTB ? rootMoves.size() : 0);
|
||||
uint64_t tbHits = threads.tb_hits() + (worker.tbConfig.rootInTB ? rootMoves.size() : 0);
|
||||
|
||||
for (size_t i = 0; i < multiPV; ++i)
|
||||
{
|
||||
@@ -2265,75 +2206,5 @@ bool RootMove::extract_ponder_from_tt(const TranspositionTable& tt, Position& po
|
||||
return pv.size() > 1;
|
||||
}
|
||||
|
||||
std::vector<char> Search::InfoFull::serialize() const {
|
||||
std::vector<char> vec;
|
||||
vec.resize(sizeof(*this) + 3 * sizeof(size_t) + wdl.size() + bound.size() + pv.size());
|
||||
char* ptr = vec.data();
|
||||
|
||||
// The base struct.
|
||||
memcpy(ptr, this, sizeof(*this));
|
||||
ptr += sizeof(*this);
|
||||
|
||||
// All string lengths.
|
||||
size_t wdl_len = wdl.size();
|
||||
memcpy(ptr, &wdl_len, sizeof(wdl_len));
|
||||
ptr += sizeof(wdl_len);
|
||||
|
||||
size_t bound_len = bound.size();
|
||||
memcpy(ptr, &bound_len, sizeof(bound_len));
|
||||
ptr += sizeof(bound_len);
|
||||
|
||||
size_t pv_len = pv.size();
|
||||
memcpy(ptr, &pv_len, sizeof(pv_len));
|
||||
ptr += sizeof(pv_len);
|
||||
|
||||
// The string data itself.
|
||||
memcpy(ptr, wdl.data(), wdl_len);
|
||||
ptr += wdl_len;
|
||||
|
||||
memcpy(ptr, bound.data(), bound_len);
|
||||
ptr += bound_len;
|
||||
|
||||
memcpy(ptr, pv.data(), pv_len);
|
||||
ptr += pv_len;
|
||||
|
||||
assert(ptr == vec.data() + vec.size());
|
||||
return vec;
|
||||
}
|
||||
|
||||
InfoFull Search::InfoFull::unserialize(const std::vector<char>& buf) {
|
||||
InfoFull info;
|
||||
const char* ptr = buf.data();
|
||||
|
||||
// The base struct.
|
||||
memcpy(&info, ptr, sizeof(info));
|
||||
ptr += sizeof(info);
|
||||
|
||||
// All string lengths.
|
||||
size_t wdl_len;
|
||||
memcpy(&wdl_len, ptr, sizeof(wdl_len));
|
||||
ptr += sizeof(wdl_len);
|
||||
|
||||
size_t bound_len;
|
||||
memcpy(&bound_len, ptr, sizeof(bound_len));
|
||||
ptr += sizeof(bound_len);
|
||||
|
||||
size_t pv_len;
|
||||
memcpy(&pv_len, ptr, sizeof(pv_len));
|
||||
ptr += sizeof(pv_len);
|
||||
|
||||
// The string data itself.
|
||||
info.wdl = std::string_view(ptr, wdl_len);
|
||||
ptr += wdl_len;
|
||||
|
||||
info.bound = std::string_view(ptr, bound_len);
|
||||
ptr += bound_len;
|
||||
|
||||
info.pv = std::string_view(ptr, pv_len);
|
||||
ptr += pv_len;
|
||||
|
||||
assert(ptr == buf.data() + buf.size());
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace Stockfish
|
||||
|
||||
+3
-37
@@ -28,12 +28,10 @@
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "cluster.h"
|
||||
#include "history.h"
|
||||
#include "misc.h"
|
||||
#include "nnue/network.h"
|
||||
@@ -123,9 +121,7 @@ struct LimitsType {
|
||||
ponderMode = false;
|
||||
}
|
||||
|
||||
bool use_time_management() const {
|
||||
return Distributed::is_root() && (time[WHITE] || time[BLACK]);
|
||||
}
|
||||
bool use_time_management() const { return time[WHITE] || time[BLACK]; }
|
||||
|
||||
std::vector<std::string> searchmoves;
|
||||
TimePoint time[COLOR_NB], inc[COLOR_NB], npmsec, movetime, startTime;
|
||||
@@ -182,14 +178,6 @@ struct InfoFull: InfoShort {
|
||||
size_t tbHits;
|
||||
std::string_view pv;
|
||||
int hashfull;
|
||||
|
||||
std::vector<char> serialize() const;
|
||||
|
||||
// NOTE: The returned InfoFull will contain string_views that point
|
||||
// into the given buffer, so it must live (and not be reallocated)
|
||||
// (and not be reallocated) for at least as long as you want to
|
||||
// access fields in the InfoFull.
|
||||
static InfoFull unserialize(const std::vector<char>& buf);
|
||||
};
|
||||
|
||||
struct InfoIteration {
|
||||
@@ -243,7 +231,7 @@ class SearchManager: public ISearchManager {
|
||||
};
|
||||
|
||||
|
||||
SearchManager(UpdateContext& updateContext) :
|
||||
SearchManager(const UpdateContext& updateContext) :
|
||||
updates(updateContext) {}
|
||||
|
||||
void check_time(Search::Worker& worker) override;
|
||||
@@ -266,7 +254,7 @@ class SearchManager: public ISearchManager {
|
||||
|
||||
size_t id;
|
||||
|
||||
UpdateContext& updates;
|
||||
const UpdateContext& updates;
|
||||
};
|
||||
|
||||
class NullSearchManager: public ISearchManager {
|
||||
@@ -306,28 +294,6 @@ class Worker {
|
||||
ContinuationHistory continuationHistory[2][2];
|
||||
CorrectionHistory<Continuation> continuationCorrectionHistory;
|
||||
|
||||
#ifdef USE_MPI
|
||||
struct {
|
||||
std::mutex mutex;
|
||||
Distributed::TTCache<Distributed::TTCacheSize> buffer = {};
|
||||
} ttCache;
|
||||
#endif
|
||||
|
||||
std::atomic<uint64_t> TTsaves;
|
||||
|
||||
friend void Distributed::save(TranspositionTable&,
|
||||
ThreadPool&,
|
||||
Search::Worker*,
|
||||
TTWriter ttWriter,
|
||||
Key k,
|
||||
Value v,
|
||||
bool PvHit,
|
||||
Bound b,
|
||||
Depth d,
|
||||
Move m,
|
||||
Value ev,
|
||||
uint8_t generation8);
|
||||
|
||||
TTMoveHistory ttMoveHistory;
|
||||
SharedHistories& sharedHistory;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#define SHM_H_INCLUDED
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -35,7 +36,7 @@
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#if !defined(_WIN32) && !defined(__ANDROID__)
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#include "shm_linux.h"
|
||||
#endif
|
||||
|
||||
@@ -59,7 +60,7 @@
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#elif defined(__linux__)
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
@@ -406,7 +407,7 @@ class SharedMemoryBackend {
|
||||
std::string last_error_message;
|
||||
};
|
||||
|
||||
#elif !defined(__ANDROID__)
|
||||
#elif defined(__linux__) && !defined(__ANDROID__)
|
||||
|
||||
template<typename T>
|
||||
class SharedMemoryBackend {
|
||||
@@ -512,12 +513,9 @@ template<typename T>
|
||||
struct SystemWideSharedConstant {
|
||||
private:
|
||||
static std::string createHashString(const std::string& input) {
|
||||
size_t hash = std::hash<std::string>{}(input);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0') << hash;
|
||||
|
||||
return ss.str();
|
||||
char buf[1024];
|
||||
std::snprintf(buf, sizeof(buf), "%016" PRIx64, hash_string(input));
|
||||
return buf;
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -534,13 +532,13 @@ struct SystemWideSharedConstant {
|
||||
// that are not present in the content, for example NUMA node allocation.
|
||||
SystemWideSharedConstant(const T& value, std::size_t discriminator = 0) {
|
||||
std::size_t content_hash = std::hash<T>{}(value);
|
||||
std::size_t executable_hash = std::hash<std::string>{}(getExecutablePathHash());
|
||||
std::size_t executable_hash = hash_string(getExecutablePathHash());
|
||||
|
||||
std::string shm_name = std::string("Local\\sf_") + std::to_string(content_hash) + "$"
|
||||
+ std::to_string(executable_hash) + "$"
|
||||
+ std::to_string(discriminator);
|
||||
char buf[1024];
|
||||
std::snprintf(buf, sizeof(buf), "Local\\sf_%zu$%zu$%zu", content_hash, executable_hash, discriminator);
|
||||
std::string shm_name = buf;
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
// POSIX shared memory names must start with a slash
|
||||
shm_name = "/sf_" + createHashString(shm_name);
|
||||
|
||||
|
||||
+43
-30
@@ -19,6 +19,10 @@
|
||||
#ifndef SHM_LINUX_H_INCLUDED
|
||||
#define SHM_LINUX_H_INCLUDED
|
||||
|
||||
#if !defined(__linux__) || defined(__ANDROID__)
|
||||
#error shm_linux.h should not be included on this platform.
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
@@ -33,7 +37,6 @@
|
||||
#include <string>
|
||||
#include <inttypes.h>
|
||||
#include <type_traits>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
@@ -41,16 +44,10 @@
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#define SF_MAX_SEM_NAME_LEN NAME_MAX
|
||||
|
||||
#if defined(__NetBSD__) || defined(__DragonFly__) || defined(__linux__)
|
||||
#include <limits.h>
|
||||
#define SF_MAX_SEM_NAME_LEN NAME_MAX
|
||||
#elif defined(__APPLE__)
|
||||
#define SF_MAX_SEM_NAME_LEN 31
|
||||
#else
|
||||
#define SF_MAX_SEM_NAME_LEN 255
|
||||
#endif
|
||||
|
||||
#include "misc.h"
|
||||
|
||||
namespace Stockfish::shm {
|
||||
|
||||
@@ -67,48 +64,60 @@ struct ShmHeader {
|
||||
class SharedMemoryBase {
|
||||
public:
|
||||
virtual ~SharedMemoryBase() = default;
|
||||
virtual void close() noexcept = 0;
|
||||
virtual void close(bool skip_unmap = false) noexcept = 0;
|
||||
virtual const std::string& name() const noexcept = 0;
|
||||
};
|
||||
|
||||
class SharedMemoryRegistry {
|
||||
private:
|
||||
static std::mutex registry_mutex_;
|
||||
static std::unordered_set<SharedMemoryBase*> active_instances_;
|
||||
static std::vector<SharedMemoryBase*> active_instances_;
|
||||
|
||||
public:
|
||||
static void register_instance(SharedMemoryBase* instance) {
|
||||
std::scoped_lock lock(registry_mutex_);
|
||||
active_instances_.insert(instance);
|
||||
active_instances_.push_back(instance);
|
||||
}
|
||||
|
||||
static void unregister_instance(SharedMemoryBase* instance) {
|
||||
std::scoped_lock lock(registry_mutex_);
|
||||
active_instances_.erase(instance);
|
||||
active_instances_.erase(
|
||||
std::remove(active_instances_.begin(), active_instances_.end(), instance), active_instances_.end());
|
||||
}
|
||||
|
||||
static void cleanup_all() noexcept {
|
||||
static void cleanup_all(bool skip_unmap = false) noexcept {
|
||||
std::scoped_lock lock(registry_mutex_);
|
||||
for (auto* instance : active_instances_)
|
||||
instance->close();
|
||||
instance->close(skip_unmap);
|
||||
active_instances_.clear();
|
||||
}
|
||||
};
|
||||
|
||||
inline std::mutex SharedMemoryRegistry::registry_mutex_;
|
||||
inline std::unordered_set<SharedMemoryBase*> SharedMemoryRegistry::active_instances_;
|
||||
inline std::vector<SharedMemoryBase*> SharedMemoryRegistry::active_instances_;
|
||||
|
||||
class CleanupHooks {
|
||||
private:
|
||||
static std::once_flag register_once_;
|
||||
|
||||
static void handle_signal(int sig) noexcept {
|
||||
SharedMemoryRegistry::cleanup_all();
|
||||
_Exit(128 + sig);
|
||||
// Search threads may still be running, so skip munmap (but still perform
|
||||
// other cleanup actions). The memory mappings will be released on exit.
|
||||
SharedMemoryRegistry::cleanup_all(true);
|
||||
|
||||
// Invoke the default handler, which will exit
|
||||
struct sigaction sa;
|
||||
sa.sa_handler = SIG_DFL;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
if (sigaction(sig, &sa, nullptr) == -1)
|
||||
_Exit(128 + sig);
|
||||
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
static void register_signal_handlers() noexcept {
|
||||
std::atexit([]() { SharedMemoryRegistry::cleanup_all(); });
|
||||
std::atexit([]() { SharedMemoryRegistry::cleanup_all(true); });
|
||||
|
||||
constexpr int signals[] = {SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGABRT, SIGFPE,
|
||||
SIGSEGV, SIGTERM, SIGBUS, SIGSYS, SIGXCPU, SIGXFSZ};
|
||||
@@ -170,9 +179,10 @@ class SharedMemory: public detail::SharedMemoryBase {
|
||||
}
|
||||
|
||||
static std::string make_sentinel_base(const std::string& name) {
|
||||
uint64_t hash = std::hash<std::string>{}(name);
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "sfshm_%016" PRIx64, static_cast<uint64_t>(hash));
|
||||
// Using std::to_string here causes non-deterministic PGO builds.
|
||||
// snprintf, being part of libc, is insensitive to the formatted values.
|
||||
std::snprintf(buf, sizeof(buf), "sfshm_%016" PRIu64, hash_string(name));
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -318,7 +328,7 @@ class SharedMemory: public detail::SharedMemoryBase {
|
||||
}
|
||||
}
|
||||
|
||||
void close() noexcept override {
|
||||
void close(bool skip_unmap = false) noexcept override {
|
||||
if (fd_ == -1 && mapped_ptr_ == nullptr)
|
||||
return;
|
||||
|
||||
@@ -345,7 +355,10 @@ class SharedMemory: public detail::SharedMemoryBase {
|
||||
decrement_refcount_relaxed();
|
||||
}
|
||||
|
||||
unmap_region();
|
||||
if (skip_unmap)
|
||||
mapped_ptr_ = nullptr;
|
||||
else
|
||||
unmap_region();
|
||||
|
||||
if (remove_region)
|
||||
shm_unlink(name_.c_str());
|
||||
@@ -359,7 +372,8 @@ class SharedMemory: public detail::SharedMemoryBase {
|
||||
fd_ = -1;
|
||||
}
|
||||
|
||||
reset();
|
||||
if (!skip_unmap)
|
||||
reset();
|
||||
}
|
||||
|
||||
const std::string& name() const noexcept override { return name_; }
|
||||
@@ -427,11 +441,10 @@ class SharedMemory: public detail::SharedMemoryBase {
|
||||
}
|
||||
|
||||
std::string sentinel_full_path(pid_t pid) const {
|
||||
std::string path = "/dev/shm/";
|
||||
path += sentinel_base_;
|
||||
path.push_back('.');
|
||||
path += std::to_string(pid);
|
||||
return path;
|
||||
char buf[1024];
|
||||
// See above snprintf comment
|
||||
std::snprintf(buf, sizeof(buf), "/dev/shm/%s.%ld", sentinel_base_.c_str(), long(pid));
|
||||
return buf;
|
||||
}
|
||||
|
||||
void decrement_refcount_relaxed() noexcept {
|
||||
|
||||
+18
-15
@@ -38,7 +38,6 @@
|
||||
#include <array>
|
||||
|
||||
#include "../bitboard.h"
|
||||
#include "../cluster.h"
|
||||
#include "../misc.h"
|
||||
#include "../movegen.h"
|
||||
#include "../position.h"
|
||||
@@ -495,9 +494,8 @@ class TBTables {
|
||||
}
|
||||
|
||||
void info() const {
|
||||
if (Distributed::is_root())
|
||||
sync_cout << "info string Found " << foundWDLFiles << " WDL and " << foundDTZFiles
|
||||
<< " DTZ tablebase files (up to " << MaxCardinality << "-man)." << sync_endl;
|
||||
sync_cout << "info string Found " << foundWDLFiles << " WDL and " << foundDTZFiles
|
||||
<< " DTZ tablebase files (up to " << MaxCardinality << "-man)." << sync_endl;
|
||||
}
|
||||
|
||||
void add(const std::vector<PieceType>& pieces);
|
||||
@@ -711,15 +709,11 @@ int map_score(TBTable<DTZ>* entry, File f, int value, WDLScore wdl) {
|
||||
return value + 1;
|
||||
}
|
||||
|
||||
// A temporary fix for the compiler bug with AVX-512. (#4450)
|
||||
#ifdef USE_AVX512
|
||||
#if defined(__clang__) && defined(__clang_major__) && __clang_major__ >= 15
|
||||
#define CLANG_AVX512_BUG_FIX __attribute__((optnone))
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef CLANG_AVX512_BUG_FIX
|
||||
#define CLANG_AVX512_BUG_FIX
|
||||
// A temporary fix for the compiler bug with vectorization. (#4450)
|
||||
#if defined(__clang__) && defined(__clang_major__) && __clang_major__ >= 15
|
||||
#define DISABLE_CLANG_LOOP_VEC _Pragma("clang loop vectorize(disable)")
|
||||
#else
|
||||
#define DISABLE_CLANG_LOOP_VEC
|
||||
#endif
|
||||
|
||||
// Compute a unique index out of a position and use it to probe the TB file. To
|
||||
@@ -729,8 +723,7 @@ int map_score(TBTable<DTZ>* entry, File f, int value, WDLScore wdl) {
|
||||
// idx = Binomial[1][s1] + Binomial[2][s2] + ... + Binomial[k][sk]
|
||||
//
|
||||
template<typename T, typename Ret = typename T::Ret>
|
||||
CLANG_AVX512_BUG_FIX Ret
|
||||
do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* result) {
|
||||
Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* result) {
|
||||
|
||||
Square squares[TBPIECES];
|
||||
Piece pieces[TBPIECES];
|
||||
@@ -814,8 +807,11 @@ do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* result)
|
||||
// Now we map again the squares so that the square of the lead piece is in
|
||||
// the triangle A1-D1-D4.
|
||||
if (file_of(squares[0]) > FILE_D)
|
||||
{
|
||||
DISABLE_CLANG_LOOP_VEC
|
||||
for (int i = 0; i < size; ++i)
|
||||
squares[i] = flip_file(squares[i]);
|
||||
}
|
||||
|
||||
// Encode leading pawns starting with the one with minimum MapPawns[] and
|
||||
// proceeding in ascending order.
|
||||
@@ -834,19 +830,26 @@ do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* result)
|
||||
// In positions without pawns, we further flip the squares to ensure leading
|
||||
// piece is below RANK_5.
|
||||
if (rank_of(squares[0]) > RANK_4)
|
||||
{
|
||||
DISABLE_CLANG_LOOP_VEC
|
||||
for (int i = 0; i < size; ++i)
|
||||
squares[i] = flip_rank(squares[i]);
|
||||
}
|
||||
|
||||
// Look for the first piece of the leading group not on the A1-D4 diagonal
|
||||
// and ensure it is mapped below the diagonal.
|
||||
DISABLE_CLANG_LOOP_VEC
|
||||
for (int i = 0; i < d->groupLen[0]; ++i)
|
||||
{
|
||||
if (!off_A1H8(squares[i]))
|
||||
continue;
|
||||
|
||||
if (off_A1H8(squares[i]) > 0) // A1-H8 diagonal flip: SQ_A3 -> SQ_C1
|
||||
{
|
||||
DISABLE_CLANG_LOOP_VEC
|
||||
for (int j = i; j < size; ++j)
|
||||
squares[j] = Square(((squares[j] >> 3) | (squares[j] << 3)) & 63);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
+26
-22
@@ -28,10 +28,8 @@
|
||||
#include <utility>
|
||||
|
||||
#include "bitboard.h"
|
||||
#include "cluster.h"
|
||||
#include "history.h"
|
||||
#include "memory.h"
|
||||
#include "misc.h"
|
||||
#include "movegen.h"
|
||||
#include "search.h"
|
||||
#include "syzygy/tbprobe.h"
|
||||
@@ -142,16 +140,15 @@ Search::SearchManager* ThreadPool::main_manager() { return main_thread()->worker
|
||||
|
||||
uint64_t ThreadPool::nodes_searched() const { return accumulate(&Search::Worker::nodes); }
|
||||
uint64_t ThreadPool::tb_hits() const { return accumulate(&Search::Worker::tbHits); }
|
||||
uint64_t ThreadPool::TT_saves() const { return accumulate(&Search::Worker::TTsaves); }
|
||||
|
||||
static size_t next_power_of_two(uint64_t count) { return count > 1 ? (2ULL << msb(count - 1)) : 1; }
|
||||
|
||||
// Creates/destroys threads to match the requested number.
|
||||
// Created and launched threads will immediately go to sleep in idle_loop.
|
||||
// Upon resizing, threads are recreated to allow for binding if necessary.
|
||||
void ThreadPool::set(const NumaConfig& numaConfig,
|
||||
Search::SharedState sharedState,
|
||||
Search::SearchManager::UpdateContext& updateContext) {
|
||||
void ThreadPool::set(const NumaConfig& numaConfig,
|
||||
Search::SharedState sharedState,
|
||||
const Search::SearchManager::UpdateContext& updateContext) {
|
||||
|
||||
if (threads.size() > 0) // destroy any existing thread(s)
|
||||
{
|
||||
@@ -216,22 +213,31 @@ void ThreadPool::set(const NumaConfig& numaConfig,
|
||||
|
||||
while (threads.size() < requested)
|
||||
{
|
||||
const size_t threadId = threads.size();
|
||||
const NumaIndex numaId = doBindThreads ? boundThreadToNumaNode[threadId] : 0;
|
||||
auto manager = threadId == 0 ? std::unique_ptr<Search::ISearchManager>(
|
||||
std::make_unique<Search::SearchManager>(updateContext))
|
||||
: std::make_unique<Search::NullSearchManager>();
|
||||
const size_t threadId = threads.size();
|
||||
const NumaIndex numaId = doBindThreads ? boundThreadToNumaNode[threadId] : 0;
|
||||
auto create_thread = [&]() {
|
||||
auto manager = threadId == 0
|
||||
? std::unique_ptr<Search::ISearchManager>(
|
||||
std::make_unique<Search::SearchManager>(updateContext))
|
||||
: std::make_unique<Search::NullSearchManager>();
|
||||
|
||||
// When not binding threads we want to force all access to happen
|
||||
// from the same NUMA node, because in case of NUMA replicated memory
|
||||
// accesses we don't want to trash cache in case the threads get scheduled
|
||||
// on the same NUMA node.
|
||||
auto binder = doBindThreads ? OptionalThreadToNumaNodeBinder(numaConfig, numaId)
|
||||
: OptionalThreadToNumaNodeBinder(numaId);
|
||||
// When not binding threads we want to force all access to happen
|
||||
// from the same NUMA node, because in case of NUMA replicated memory
|
||||
// accesses we don't want to trash cache in case the threads get scheduled
|
||||
// on the same NUMA node.
|
||||
auto binder = doBindThreads ? OptionalThreadToNumaNodeBinder(numaConfig, numaId)
|
||||
: OptionalThreadToNumaNodeBinder(numaId);
|
||||
|
||||
threads.emplace_back(std::make_unique<Thread>(sharedState, std::move(manager), threadId,
|
||||
counts[numaId]++, threadsPerNode[numaId],
|
||||
binder));
|
||||
threads.emplace_back(std::make_unique<Thread>(sharedState, std::move(manager),
|
||||
threadId, counts[numaId]++,
|
||||
threadsPerNode[numaId], binder));
|
||||
};
|
||||
|
||||
// Ensure the worker thread inherits the intended NUMA affinity at creation.
|
||||
if (doBindThreads)
|
||||
numaConfig.execute_on_numa_node(numaId, create_thread);
|
||||
else
|
||||
create_thread();
|
||||
}
|
||||
|
||||
clear();
|
||||
@@ -335,8 +341,6 @@ void ThreadPool::start_thinking(const OptionsMap& options,
|
||||
for (auto&& th : threads)
|
||||
th->wait_for_search_finished();
|
||||
|
||||
Distributed::signals_init();
|
||||
|
||||
main_thread()->start_searching();
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -29,7 +29,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "memory.h"
|
||||
#include "movepick.h"
|
||||
#include "numa.h"
|
||||
#include "position.h"
|
||||
#include "search.h"
|
||||
@@ -138,14 +137,14 @@ class ThreadPool {
|
||||
void wait_on_thread(size_t threadId);
|
||||
size_t num_threads() const;
|
||||
void clear();
|
||||
void
|
||||
set(const NumaConfig& numaConfig, Search::SharedState, Search::SearchManager::UpdateContext&);
|
||||
void set(const NumaConfig& numaConfig,
|
||||
Search::SharedState,
|
||||
const Search::SearchManager::UpdateContext&);
|
||||
|
||||
Search::SearchManager* main_manager();
|
||||
Thread* main_thread() const { return threads.front().get(); }
|
||||
uint64_t nodes_searched() const;
|
||||
uint64_t tb_hits() const;
|
||||
uint64_t TT_saves() const;
|
||||
Thread* get_best_thread() const;
|
||||
void start_searching();
|
||||
void wait_for_search_finished() const;
|
||||
|
||||
+1
-2
@@ -21,13 +21,12 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "cluster.h"
|
||||
#include "misc.h"
|
||||
|
||||
namespace Stockfish {
|
||||
|
||||
class OptionsMap;
|
||||
enum Color : int8_t;
|
||||
enum Color : uint8_t;
|
||||
|
||||
namespace Search {
|
||||
struct LimitsType;
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
|
||||
namespace Stockfish {
|
||||
|
||||
namespace Distributed {
|
||||
void init();
|
||||
}
|
||||
|
||||
class ThreadPool;
|
||||
struct TTEntry;
|
||||
struct Cluster;
|
||||
@@ -56,12 +52,7 @@ struct TTData {
|
||||
Bound bound;
|
||||
bool is_pv;
|
||||
|
||||
#ifdef USE_MPI
|
||||
// We need this for TTCache to be constructible.
|
||||
TTData() = default;
|
||||
#else
|
||||
TTData() = delete;
|
||||
#endif
|
||||
|
||||
// clang-format off
|
||||
TTData(Move m, Value v, Value ev, Depth d, Bound b, bool pv) :
|
||||
@@ -82,8 +73,6 @@ struct TTWriter {
|
||||
|
||||
private:
|
||||
friend class TranspositionTable;
|
||||
friend void Distributed::init();
|
||||
|
||||
TTEntry* entry;
|
||||
TTWriter(TTEntry* tte);
|
||||
};
|
||||
@@ -91,8 +80,6 @@ struct TTWriter {
|
||||
|
||||
class TranspositionTable {
|
||||
|
||||
friend void Distributed::init();
|
||||
|
||||
public:
|
||||
~TranspositionTable() { aligned_large_pages_free(table); }
|
||||
|
||||
|
||||
+9
-9
@@ -117,13 +117,13 @@ using Bitboard = uint64_t;
|
||||
constexpr int MAX_MOVES = 256;
|
||||
constexpr int MAX_PLY = 246;
|
||||
|
||||
enum Color : int8_t {
|
||||
enum Color : uint8_t {
|
||||
WHITE,
|
||||
BLACK,
|
||||
COLOR_NB = 2
|
||||
};
|
||||
|
||||
enum CastlingRights : int8_t {
|
||||
enum CastlingRights : uint8_t {
|
||||
NO_CASTLING,
|
||||
WHITE_OO,
|
||||
WHITE_OOO = WHITE_OO << 1,
|
||||
@@ -139,7 +139,7 @@ enum CastlingRights : int8_t {
|
||||
CASTLING_RIGHT_NB = 16
|
||||
};
|
||||
|
||||
enum Bound : int8_t {
|
||||
enum Bound : uint8_t {
|
||||
BOUND_NONE,
|
||||
BOUND_UPPER,
|
||||
BOUND_LOWER,
|
||||
@@ -190,13 +190,13 @@ constexpr Value QueenValue = 2538;
|
||||
|
||||
|
||||
// clang-format off
|
||||
enum PieceType : std::int8_t {
|
||||
enum PieceType : std::uint8_t {
|
||||
NO_PIECE_TYPE, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING,
|
||||
ALL_PIECES = 0,
|
||||
PIECE_TYPE_NB = 8
|
||||
};
|
||||
|
||||
enum Piece : std::int8_t {
|
||||
enum Piece : std::uint8_t {
|
||||
NO_PIECE,
|
||||
W_PAWN = PAWN, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING,
|
||||
B_PAWN = PAWN + 8, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING,
|
||||
@@ -227,7 +227,7 @@ constexpr Depth DEPTH_UNSEARCHED = -2;
|
||||
constexpr Depth DEPTH_ENTRY_OFFSET = -3;
|
||||
|
||||
// clang-format off
|
||||
enum Square : int8_t {
|
||||
enum Square : uint8_t {
|
||||
SQ_A1, SQ_B1, SQ_C1, SQ_D1, SQ_E1, SQ_F1, SQ_G1, SQ_H1,
|
||||
SQ_A2, SQ_B2, SQ_C2, SQ_D2, SQ_E2, SQ_F2, SQ_G2, SQ_H2,
|
||||
SQ_A3, SQ_B3, SQ_C3, SQ_D3, SQ_E3, SQ_F3, SQ_G3, SQ_H3,
|
||||
@@ -255,7 +255,7 @@ enum Direction : int8_t {
|
||||
NORTH_WEST = NORTH + WEST
|
||||
};
|
||||
|
||||
enum File : int8_t {
|
||||
enum File : uint8_t {
|
||||
FILE_A,
|
||||
FILE_B,
|
||||
FILE_C,
|
||||
@@ -267,7 +267,7 @@ enum File : int8_t {
|
||||
FILE_NB
|
||||
};
|
||||
|
||||
enum Rank : int8_t {
|
||||
enum Rank : uint8_t {
|
||||
RANK_1,
|
||||
RANK_2,
|
||||
RANK_3,
|
||||
@@ -406,7 +406,7 @@ constexpr Key make_key(uint64_t seed) {
|
||||
}
|
||||
|
||||
|
||||
enum MoveType {
|
||||
enum MoveType : uint16_t {
|
||||
NORMAL,
|
||||
PROMOTION = 1 << 14,
|
||||
EN_PASSANT = 2 << 14,
|
||||
|
||||
+19
-26
@@ -30,7 +30,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "benchmark.h"
|
||||
#include "cluster.h"
|
||||
#include "engine.h"
|
||||
#include "memory.h"
|
||||
#include "movegen.h"
|
||||
@@ -95,8 +94,7 @@ void UCIEngine::loop() {
|
||||
do
|
||||
{
|
||||
if (cli.argc == 1
|
||||
&& !Distributed::getline(std::cin,
|
||||
cmd)) // Wait for an input or an end-of-file (EOF) indication
|
||||
&& !getline(std::cin, cmd)) // Wait for an input or an end-of-file (EOF) indication
|
||||
cmd = "quit";
|
||||
|
||||
std::istringstream is(cmd);
|
||||
@@ -114,7 +112,7 @@ void UCIEngine::loop() {
|
||||
else if (token == "ponderhit")
|
||||
engine.set_ponderhit(false);
|
||||
|
||||
else if (token == "uci" && Distributed::is_root())
|
||||
else if (token == "uci")
|
||||
{
|
||||
sync_cout << "id name " << engine_info(true) << "\n"
|
||||
<< engine.get_options() << sync_endl;
|
||||
@@ -135,7 +133,7 @@ void UCIEngine::loop() {
|
||||
position(is);
|
||||
else if (token == "ucinewgame")
|
||||
engine.search_clear();
|
||||
else if (token == "isready" && Distributed::is_root())
|
||||
else if (token == "isready")
|
||||
sync_cout << "readyok" << sync_endl;
|
||||
|
||||
// Add custom non-UCI commands, mainly for debugging purposes.
|
||||
@@ -146,13 +144,13 @@ void UCIEngine::loop() {
|
||||
bench(is);
|
||||
else if (token == BenchmarkCommand)
|
||||
benchmark(is);
|
||||
else if (token == "d" && Distributed::is_root())
|
||||
else if (token == "d")
|
||||
sync_cout << engine.visualize() << sync_endl;
|
||||
else if (token == "eval" && Distributed::is_root())
|
||||
else if (token == "eval")
|
||||
engine.trace_eval();
|
||||
else if (token == "compiler" && Distributed::is_root())
|
||||
else if (token == "compiler")
|
||||
sync_cout << compiler_info() << sync_endl;
|
||||
else if (token == "export_net" && Distributed::is_root())
|
||||
else if (token == "export_net")
|
||||
{
|
||||
std::pair<std::optional<std::string>, std::string> files[2];
|
||||
|
||||
@@ -164,9 +162,7 @@ void UCIEngine::loop() {
|
||||
|
||||
engine.save_network(files);
|
||||
}
|
||||
else if ((token == "--help" || token == "help" || token == "--license"
|
||||
|| token == "license")
|
||||
&& Distributed::is_root())
|
||||
else if (token == "--help" || token == "help" || token == "--license" || token == "license")
|
||||
sync_cout
|
||||
<< "\nStockfish is a powerful chess engine for playing and analyzing."
|
||||
"\nIt is released as free software licensed under the GNU GPLv3 License."
|
||||
@@ -175,7 +171,7 @@ void UCIEngine::loop() {
|
||||
"\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme"
|
||||
"\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n"
|
||||
<< sync_endl;
|
||||
else if (!token.empty() && token[0] != '#' && Distributed::is_root())
|
||||
else if (!token.empty() && token[0] != '#')
|
||||
sync_cout << "Unknown command: '" << cmd << "'. Type help for more information."
|
||||
<< sync_endl;
|
||||
|
||||
@@ -256,9 +252,8 @@ void UCIEngine::bench(std::istream& args) {
|
||||
|
||||
if (token == "go" || token == "eval")
|
||||
{
|
||||
if (Distributed::is_root())
|
||||
std::cerr << "\nPosition: " << cnt++ << '/' << num << " (" << engine.fen() << ")"
|
||||
<< std::endl;
|
||||
std::cerr << "\nPosition: " << cnt++ << '/' << num << " (" << engine.fen() << ")"
|
||||
<< std::endl;
|
||||
if (token == "go")
|
||||
{
|
||||
Search::LimitsType limits = parse_limits(is);
|
||||
@@ -274,7 +269,7 @@ void UCIEngine::bench(std::istream& args) {
|
||||
nodes += nodesSearched;
|
||||
nodesSearched = 0;
|
||||
}
|
||||
else if (Distributed::is_root())
|
||||
else
|
||||
engine.trace_eval();
|
||||
}
|
||||
else if (token == "setoption")
|
||||
@@ -292,11 +287,10 @@ void UCIEngine::bench(std::istream& args) {
|
||||
|
||||
dbg_print();
|
||||
|
||||
if (Distributed::is_root())
|
||||
std::cerr << "\n===========================" //
|
||||
<< "\nTotal time (ms) : " << elapsed //
|
||||
<< "\nNodes searched : " << nodes //
|
||||
<< "\nNodes/second : " << 1000 * nodes / elapsed << std::endl;
|
||||
std::cerr << "\n===========================" //
|
||||
<< "\nTotal time (ms) : " << elapsed //
|
||||
<< "\nNodes searched : " << nodes //
|
||||
<< "\nNodes/second : " << 1000 * nodes / elapsed << std::endl;
|
||||
|
||||
// reset callback, to not capture a dangling reference to nodesSearched
|
||||
engine.set_on_update_full([&](const auto& i) { on_update_full(i, options["UCI_ShowWDL"]); });
|
||||
@@ -467,8 +461,7 @@ void UCIEngine::setoption(std::istringstream& is) {
|
||||
|
||||
std::uint64_t UCIEngine::perft(const Search::LimitsType& limits) {
|
||||
auto nodes = engine.perft(engine.fen(), limits.perft, engine.get_options()["UCI_Chess960"]);
|
||||
if (Distributed::is_root())
|
||||
sync_cout << "\nNodes searched: " << nodes << "\n" << sync_endl;
|
||||
sync_cout << "\nNodes searched: " << nodes << "\n" << sync_endl;
|
||||
return nodes;
|
||||
}
|
||||
|
||||
@@ -514,8 +507,8 @@ WinRateParams win_rate_params(const Position& pos) {
|
||||
double m = std::clamp(material, 17, 78) / 58.0;
|
||||
|
||||
// Return a = p_a(material) and b = p_b(material), see github.com/official-stockfish/WDL_model
|
||||
constexpr double as[] = {-13.50030198, 40.92780883, -36.82753545, 386.83004070};
|
||||
constexpr double bs[] = {96.53354896, -165.79058388, 90.89679019, 49.29561889};
|
||||
constexpr double as[] = {-72.32565836, 185.93832038, -144.58862193, 416.44950446};
|
||||
constexpr double bs[] = {83.86794042, -136.06112997, 69.98820887, 47.62901433};
|
||||
|
||||
double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3];
|
||||
double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3];
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Stockfish {
|
||||
class Position;
|
||||
class Move;
|
||||
class Score;
|
||||
enum Square : int8_t;
|
||||
enum Square : uint8_t;
|
||||
using Value = int;
|
||||
|
||||
class UCIEngine {
|
||||
|
||||
+1
-2
@@ -26,7 +26,6 @@
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
#include "cluster.h"
|
||||
#include "misc.h"
|
||||
|
||||
namespace Stockfish {
|
||||
@@ -55,7 +54,7 @@ void OptionsMap::setoption(std::istringstream& is) {
|
||||
|
||||
if (options_map.count(name))
|
||||
options_map[name] = value;
|
||||
else if (Distributed::is_root())
|
||||
else
|
||||
sync_cout << "No such option: " << name << sync_endl;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user