Compare commits

..
Author SHA1 Message Date
DisservinandTimothy Herchen 43a2be0186 Fix Strict Aliasing Violation
Even though this failed with non regression bounds on fishtest, merging
it as is to improve the current situation.

Failed Non Regression:
https://tests.stockfishchess.org/tests/view/6988814db0f3ca5200aafb50
LLR: -2.94 (-2.94,2.94) <-1.75,0.25>
Total: 185792 W: 48109 L: 48565 D: 89118
Ptnml(0-2): 553, 21228, 49811, 20730, 574

Previously we had code similar to this
```
alignas(64) std::uint8_t foo[1024];
const auto* int_ptr = reinterpret_cast<const std::int32_t*>(foo);
```

Which is a strict aliasing violation once we dereference int_ptr, which
invokes UB and causes newer gcc version to emit incorrect code.

fixes https://github.com/official-stockfish/Stockfish/issues/6598
fixes https://github.com/official-stockfish/Stockfish/issues/4932

This violation was introduced in PR #3287 and merged in commit 23c385e.
Due to aggressive compiler optimizations, users may need to include
CXXFLAGS=-fno-strict-aliasing as a build argument to ensure a correctly
functioning binary.

Also thanks to @anematode for writing parts of this and realizing this
strict aliasing violation.

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

Bench: 2668754

Co-Authored-By: Timothy Herchen <timothy.herchen@gmail.com>
2026-02-08 16:08:09 +01:00
Joost VandeVondele cb3d4ee9b4 Stockfish 18
Official release version of Stockfish 18

Bench: 2050811

---

Stockfish 18

Today, we have the pleasure of announcing Stockfish 18, a new major release. As
always, you can freely download it at [stockfishchess.org/download][1] and use
it as a drop-in replacement in the [GUI of your choice][2] to benefit from
stronger play and more accurate analysis.

Whether you can spare hours or days of CPU time, your help matters for the
ongoing development of Stockfish. Find out how you can help at
[stockfishchess.org/get-involved][3]. Join our [Discord server][4] to get in
touch with the community of developers and users of the project!

Quality of chess play

In tests against Stockfish 17, this new release brings an Elo gain of up to [46
points][5], and wins [four times as many game pairs][6] as it loses. Quality
improved throughout, including in [Fischer Random Chess][7].

Stockfish is stronger than any human, even when running on older or low-end
hardware. On the highest-end hardware, where Stockfish can search over [500
million positions per second][8], it continues to [dominate chess engine
tournaments][9].

Update highlights

Next-Generation Evaluation

This release introduces the SFNNv10 network architecture. The network’s input
layer has been augmented with 'Threat Inputs' features as part of a massive
community effort. These features allow the engine to 'see' which pieces are
threatened more naturally, resulting in more accurate evaluations.

Hardware and Performance Optimizations

A key highlight is the new 'Shared Memory' implementation, which allows
different Stockfish processes to share the same memory space for neural network
weights. This makes it the most efficient version for cloud analysis and
high-concurrency testing.

Significant efforts have also been made to utilize hardware more effectively by
adapting the code to make use of modern CPU instructions and refining how
threads interact during a search.

Search Improvements

Stockfish 18 features a heavily refined search, utilizing 'Correction History'
to dynamically adjust evaluations based on patterns found during the search
itself. These and other refinements allow the engine to detect stalemates and
evaluate fortresses significantly better than previous versions. A particularly
rare issue, involving threefold repetition detection, en passant, and pins, was
also fixed.

Refactored Training Workflow

The training of Stockfish's neural networks has transitioned to an automated
and reproducible model. This new framework allows the project to employ
standardized recipes to chain complex training stages together. This transition
facilitates the training of networks using over 100 billion positions of [Lc0
evaluation data][10].

Thank you

In this release in particular, we are deeply grateful to the contributors who
shared their research and ideas to help develop the new threat-input network
architecture.

The Stockfish project builds on a thriving community of enthusiasts (thanks to
everybody!) who contribute their expertise, time, and resources to build a free
and open-source chess engine that is robust, widely available, and very strong.

We would like to express our gratitude for the [14.6k stars][11] that light up
our GitHub project. Thank you for your support and encouragement – your
recognition means a lot to us. Programmers can contribute to the project either
directly to [Stockfish][12] (C++), to [Fishtest][13] (HTML, CSS, JavaScript,
and Python), to our trainer [nnue-pytorch][14] (C++ and Python), or to our
[website][15] (HTML, CSS/SCSS, and JavaScript).

The Stockfish team

[1]: https://stockfishchess.org/download
[2]: https://official-stockfish.github.io/docs/stockfish-wiki/Download-and-usage.html#download-a-chess-gui
[3]: https://stockfishchess.org/get-involved/
[4]: https://discord.gg/GWDRS3kU6R
[5]: https://tests.stockfishchess.org/tests/view/696a9e15cec152c6220c1d19
[6]: https://tests.stockfishchess.org/tests/view/696a9e4dcec152c6220c1d1b
[7]: https://tests.stockfishchess.org/tests/view/696a9e83cec152c6220c1d1d
[8]: https://openbenchmarking.org/test/pts/stockfish
[9]: https://en.wikipedia.org/wiki/Stockfish_(chess)#Competition_results
[10]: https://lczero.org/blog/2021/06/the-importance-of-open-data/
[11]: https://github.com/official-stockfish/Stockfish/stargazers
[12]: https://github.com/official-stockfish/Stockfish
[13]: https://github.com/official-stockfish/fishtest
[14]: https://github.com/official-stockfish/nnue-pytorch
[15]: https://github.com/official-stockfish/stockfish-web
2026-01-31 12:46:49 +01:00
anematodeandDisservin 253aaefbc0 Fix compilation on BSD/macOS
fixes github.com/official-stockfish/Stockfish/issues/6571

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

No functional change
2026-01-28 20:28:27 +01:00
anematodeandJoost VandeVondele c0f245303b Make PGO builds deterministic again
some environment dependent execution (e.g. pid) were being std::hash'ed, and
net filenames put in unordered maps.  Also uses sprintf instead of
std::to_string.  Depending on precise content, this could lead to different
PGO'ed binaries. This is mitigated by using a basic hash function.

This also fixes a potential issue in net filename generation, in cases where
std::hash would use invocation dependent salt, which is not the cases today for
typical std libraries.

Closes https://github.com/official-stockfish/Stockfish/pull/6562

No functional change
2026-01-22 19:15:17 +01:00
anematodeandJoost VandeVondele f61d4317a3 use default signal handler after cleanup
With the current setup, on Linux, SIGILL (and SIGSEGV/SIGBUS etc., in the case
of a program bug) lead to no feedback if they occur after the signal handlers
are installed, they just exit silently. By invoking the default signal handler,
we can still get the appropriate feedback. This is particularly important for
feedback if someone downloads the wrong SF binary and runs it on Linux.

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

No functional change
2026-01-18 08:53:06 +01:00
Robert Nurnberg @ elitebookandJoost VandeVondele 71f53b92c7 update the WDL model
This PR updates the internal WDL model, using data from 3.1M games played by
the revisions since 44d5467. Note that the normalizing constant increases only
moderately from 377 to 385.

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

No functional change
2026-01-15 22:19:50 +01:00
anematodeandJoost VandeVondele 5b8b304ebd Skip munmap when exiting via a signal
avoid munmap of memory when exiting via signal, which avoids side effects such
as triggering asserts or (caught) segfaults while the process exists.

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

No functional change
2026-01-15 22:17:14 +01:00
Joost VandeVondele 0b9068d510 Fix integer overflow.
scaledBonus can reach rather large values,
which lead to an int overflow as detected anematode using ubsan.
(see https://github.com/official-stockfish/Stockfish/issues/6505#issuecomment-3696988889)

It can be fixed by scaling nominator and denominator appropriately,
which doesn't change the bench, as long as there is no overflow.

First overflow/bench change happens at depth 26

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

Bench: 2050811
2026-01-15 22:13:26 +01:00
Timothy HerchenandJoost VandeVondele eb5a65aeb0 Fix RelationCache on Windows 10 compiles
Windows 10 is missing the GroupMasks and GroupCount members, this breaks compiles on Windows 10.
Windows 11 builds, including the official ones, run fine on Windows 10/11.
To support developers/testers on Windows 10, fallback conditionally to the Windows 10 struct definition.

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

No functional change
2026-01-11 09:13:37 +01:00
KazAppsandJoost VandeVondele e9b2864579 Simplify make_index
Refactor index LUT construction to simplify make_index.

Passed STC Non-Regression:
LLR: 2.93 (-2.94,2.94) <-1.75,0.25>
Total: 62432 W: 16193 L: 16006 D: 30233
Ptnml(0-2): 189, 6950, 16764, 7111, 202
https://tests.stockfishchess.org/tests/view/6959985ad844c1ce7cc7eac8

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

No functional change.
2026-01-11 09:09:55 +01:00
KazAppsandJoost VandeVondele d852a9195e Make enums unsigned
Speed up by using unsigned enums.

Passed STC:
LLR: 2.98 (-2.94,2.94) <0.00,2.00>
Total: 49248 W: 12894 L: 12568 D: 23786
Ptnml(0-2): 119, 5353, 13397, 5593, 16
https://tests.stockfishchess.org/tests/view/695e3e5002d0182a589fe965

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

No functional change
2026-01-10 16:06:18 +01:00
FauziAkramandJoost VandeVondele 9b8c5c9f75 Simplify fail high reduction formula
Passed STC:
LLR: 2.97 (-2.94,2.94) <-1.75,0.25>
Total: 165792 W: 42849 L: 42768 D: 80175
Ptnml(0-2): 512, 19499, 42800, 19566, 519
https://tests.stockfishchess.org/tests/view/695cdd95912b7ff140de60c2

Passed LTC:
LLR: 2.95 (-2.94,2.94) <-1.75,0.25>
Total: 80448 W: 20619 L: 20459 D: 39370
Ptnml(0-2): 47, 8693, 22596, 8829, 59
https://tests.stockfishchess.org/tests/view/695f7e84ca95f52e4b852536

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

bench: 2050811
2026-01-10 15:47:11 +01:00
anematodeandJoost VandeVondele b4d4eecfb2 Make shared history allocation aware of non-uniform cache access
Although shared history has been successful overall, it led to some speed
issues with large numbers of threads. Originally we just split by NUMA node,
but on systems with non-unified L3 caches (most AMD workstation and server
CPUs, and some Intel E-core based server CPUs), this can still lead to a speed
penalty at the default config. Thus, we decided to further subdivide the shared
history based on the L3 cache structure.

Based on this test, the original SPRTs, and speed experiments, we decided that
grouping L3 domains to reach 32 threads per SharedHistories was a reasonable
balance for affected systems – but we may revisit this in the future. See the
PR for full details.

In an extreme case, a single-socket EPYC 9755 configured with 1 numa domain per socket,
the nps increases from:
Nodes/second               : 182827480
to
Nodes/second               : 229118365

In many cases, when L3 caches are shared between many threads, or when several
numa nodes are already configured per socket, this patch does not influence the
default. This default setting can adjusted with the existing NumaPolicy option.

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

No functional change.
2026-01-10 15:46:01 +01:00
DisservinandJoost VandeVondele 1928ef9571 Compiler Check
Compiles and Runs Stockfish on all supported gcc & clang compilers.
Only linux and avx2 currently.

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

No functional change
2026-01-10 15:36:52 +01:00
DisservinandJoost VandeVondele 5d5e795746 Fix Compiler Warning
Only the one on line 158 is actually required but doesn't hurt to add constexpr where applicable here.

Warning was

"comparison of unsigned expression in '< 0' is always false"

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

No functional change
2026-01-10 15:34:37 +01:00
DisservinandJoost VandeVondele d39bfb61a2 Fix Clang Tbprobe Miscompilation
Recent changes to the Square enum (reducing it from int32_t to int8_t)
now allow the compiler to vectorize loops that were previously too wide
for targets below AVX-512. However, this vectorization which Clang
performs is not correct and causes a miscompilation.

Disable this vectorization.

This particular issue was noticable with Clang 15 and Clang 19,
on avx2 as well as applie-silicon.

Ref: #6063
Original Clang Issue: llvm/llvm-project#80494

First reported by #6528, though misinterpreted.

closes #6529

No functional change
2026-01-10 15:33:36 +01:00
Jakub CiolekandJoost VandeVondele c27c1747e3 qsearch: prevent bestValue from going down
The bestValue can sometimes go down. This happens 2% of the time or so.
This fix stops it from decreasing.

Failed gainer STC:
LLR: -2.94 (-2.94,2.94) <0.00,2.00>
Total: 146176 W: 37930 L: 37976 D: 70270
Ptnml(0-2): 480, 17422, 37366, 17304, 516
https://tests.stockfishchess.org/tests/view/6953be19572093c1986da66a

Passed Non-regression LTC:
LLR: 2.95 (-2.94,2.94) <-1.75,0.25>
Total: 257796 W: 65662 L: 65683 D: 126451
Ptnml(0-2): 164, 28247, 72087, 28246, 154
https://tests.stockfishchess.org/tests/view/69554ff0d844c1ce7cc7e333

closes https://github.com/official-stockfish/Stockfish/pull/6520
fixes https://github.com/official-stockfish/Stockfish/issues/6519

Bench: 2477446
2026-01-06 12:08:08 +01:00
anematodeandJoost VandeVondele 8be6b14218 Network loading refactoring
closes https://github.com/official-stockfish/Stockfish/pull/6523

No functional change
2026-01-06 12:02:37 +01:00
Syine MinetaandJoost VandeVondele d678f839d8 Fix remote access bug across NUMA nodes
Ensure that thread-local data is created within the correct NUMA
context, so that thread stacks or thread-local storage are allocated
to proper NUMA nodes.

refs https://github.com/official-stockfish/Stockfish/issues/6516

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

No functional change
2026-01-06 12:01:01 +01:00
39 changed files with 794 additions and 1324 deletions
+85
View File
@@ -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
+2
View File
@@ -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
+1
View File
@@ -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)
-27
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}();
+9 -35
View File
@@ -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
+5 -4
View File
@@ -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)
+21 -17
View File
@@ -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]);
+4 -4
View File
@@ -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;
+5 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+14 -28
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+12 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
-13
View File
@@ -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
View File
@@ -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
View File
@@ -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];
+1 -1
View File
@@ -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
View File
@@ -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;
}