diff --git a/.clang-format b/.clang-format index c71f0368e..a470cc0b8 100644 --- a/.clang-format +++ b/.clang-format @@ -9,14 +9,14 @@ AllowAllParametersOfDeclarationOnNextLine: true AllowShortCaseLabelsOnASingleLine: false AllowShortEnumsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false -AlwaysBreakTemplateDeclarations: Yes +BreakTemplateDeclarations: Yes BasedOnStyle: WebKit BitFieldColonSpacing: After BinPackParameters: false BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Custom BraceWrapping: - AfterFunction: false + AfterFunction: false AfterClass: false AfterControlStatement: true BeforeElse: true diff --git a/.github/ci/arm_matrix.json b/.github/ci/arm_matrix.json index 70f2efaa2..b53fe03af 100644 --- a/.github/ci/arm_matrix.json +++ b/.github/ci/arm_matrix.json @@ -4,7 +4,7 @@ "name": "Android NDK aarch64", "os": "ubuntu-22.04", "simple_name": "android", - "compiler": "aarch64-linux-android21-clang++", + "compiler": "aarch64-linux-android29-clang++", "emu": "qemu-aarch64", "comp": "ndk", "shell": "bash", @@ -14,7 +14,7 @@ "name": "Android NDK arm", "os": "ubuntu-22.04", "simple_name": "android", - "compiler": "armv7a-linux-androideabi21-clang++", + "compiler": "armv7a-linux-androideabi29-clang++", "emu": "qemu-arm", "comp": "ndk", "shell": "bash", @@ -26,25 +26,25 @@ { "binaries": "armv8-dotprod", "config": { - "compiler": "armv7a-linux-androideabi21-clang++" + "compiler": "armv7a-linux-androideabi29-clang++" } }, { "binaries": "armv8", "config": { - "compiler": "armv7a-linux-androideabi21-clang++" + "compiler": "armv7a-linux-androideabi29-clang++" } }, { "binaries": "armv7", "config": { - "compiler": "aarch64-linux-android21-clang++" + "compiler": "aarch64-linux-android29-clang++" } }, { "binaries": "armv7-neon", "config": { - "compiler": "aarch64-linux-android21-clang++" + "compiler": "aarch64-linux-android29-clang++" } } ] diff --git a/.github/ci/libcxx17.imp b/.github/ci/libcxx17.imp index 7bdcf5bc2..d3a262b54 100644 --- a/.github/ci/libcxx17.imp +++ b/.github/ci/libcxx17.imp @@ -7,6 +7,7 @@ { include: [ "<__fwd/sstream.h>", private, "", public ] }, { include: [ "<__fwd/streambuf.h>", private, "", public ] }, { include: [ "<__fwd/string_view.h>", private, "", public ] }, + { include: [ "<__system_error/errc.h>", private, "", public ] }, # Mappings for includes between public headers { include: [ "", public, "", public ] }, diff --git a/.github/ci/matrix.json b/.github/ci/matrix.json index c6563eadf..e9215269b 100644 --- a/.github/ci/matrix.json +++ b/.github/ci/matrix.json @@ -1,18 +1,18 @@ { "config": [ { - "name": "Ubuntu 20.04 GCC", - "os": "ubuntu-20.04", + "name": "Ubuntu 22.04 GCC", + "os": "ubuntu-22.04", "simple_name": "ubuntu", "compiler": "g++", "comp": "gcc", "shell": "bash", "archive_ext": "tar", - "sde": "/home/runner/work/Stockfish/Stockfish/.output/sde-temp-files/sde-external-9.27.0-2023-09-13-lin/sde -future --" + "sde": "/home/runner/work/Stockfish/Stockfish/.output/sde-temp-files/sde-external-9.33.0-2024-01-07-lin/sde -future --" }, { - "name": "MacOS 13 Apple Clang", - "os": "macos-13", + "name": "macOS 15 Apple Clang", + "os": "macos-15-intel", "simple_name": "macos", "compiler": "clang++", "comp": "clang", @@ -20,8 +20,8 @@ "archive_ext": "tar" }, { - "name": "MacOS 14 Apple Clang M1", - "os": "macos-14", + "name": "macOS 15 Apple Clang M1", + "os": "macos-15", "simple_name": "macos-m1", "compiler": "clang++", "comp": "clang", @@ -38,7 +38,19 @@ "msys_env": "x86_64-gcc", "shell": "msys2 {0}", "ext": ".exe", - "sde": "/d/a/Stockfish/Stockfish/.output/sde-temp-files/sde-external-9.27.0-2023-09-13-win/sde.exe -future --", + "sde": "/d/a/Stockfish/Stockfish/.output/sde-temp-files/sde-external-9.33.0-2024-01-07-win/sde.exe -future --", + "archive_ext": "zip" + }, + { + "name": "Windows 11 Mingw-w64 Clang arm64", + "os": "windows-11-arm", + "simple_name": "windows", + "compiler": "clang++", + "comp": "clang", + "msys_sys": "clangarm64", + "msys_env": "clang-aarch64-clang", + "shell": "msys2 {0}", + "ext": ".exe", "archive_ext": "zip" } ], @@ -49,93 +61,131 @@ "x86-64-bmi2", "x86-64-avxvnni", "x86-64-avx512", - "x86-64-vnni256", "x86-64-vnni512", - "apple-silicon" + "x86-64-avx512icl", + "apple-silicon", + "armv8", + "armv8-dotprod" ], "exclude": [ { "binaries": "x86-64", "config": { - "os": "macos-14" + "os": "macos-15" } }, { "binaries": "x86-64-sse41-popcnt", "config": { - "os": "macos-14" + "os": "macos-15" } }, { "binaries": "x86-64-avx2", "config": { - "os": "macos-14" + "os": "macos-15" } }, { "binaries": "x86-64-bmi2", "config": { - "os": "macos-14" + "os": "macos-15" } }, { "binaries": "x86-64-avxvnni", "config": { - "os": "macos-14" - } - }, - { - "binaries": "x86-64-avxvnni", - "config": { - "os": "macos-14" + "os": "macos-15" } }, { "binaries": "x86-64-avx512", "config": { - "os": "macos-14" - } - }, - { - "binaries": "x86-64-vnni256", - "config": { - "os": "macos-14" + "os": "macos-15" } }, { "binaries": "x86-64-vnni512", "config": { - "os": "macos-14" + "os": "macos-15" + } + }, + { + "binaries": "x86-64-avx512icl", + "config": { + "os": "macos-15" } }, { "binaries": "x86-64-avxvnni", "config": { - "ubuntu-20.04": null - } - }, - { - "binaries": "x86-64-avxvnni", - "config": { - "os": "macos-13" + "os": "macos-15-intel" } }, { "binaries": "x86-64-avx512", "config": { - "os": "macos-13" - } - }, - { - "binaries": "x86-64-vnni256", - "config": { - "os": "macos-13" + "os": "macos-15-intel" } }, { "binaries": "x86-64-vnni512", "config": { - "os": "macos-13" + "os": "macos-15-intel" + } + }, + { + "binaries": "x86-64-avx512icl", + "config": { + "os": "macos-15-intel" + } + }, + { + "binaries": "x86-64", + "config": { + "os": "windows-11-arm" + } + }, + { + "binaries": "x86-64-sse41-popcnt", + "config": { + "os": "windows-11-arm" + } + }, + { + "binaries": "x86-64-avx2", + "config": { + "os": "windows-11-arm" + } + }, + { + "binaries": "x86-64-bmi2", + "config": { + "os": "windows-11-arm" + } + }, + { + "binaries": "x86-64-avxvnni", + "config": { + "os": "windows-11-arm" + } + }, + { + "binaries": "x86-64-avx512", + "config": { + "os": "windows-11-arm" + } + }, + { + "binaries": "x86-64-vnni512", + "config": { + "os": "windows-11-arm" + } + }, + { + "binaries": "x86-64-avx512icl", + "config": { + "os": "windows-11-arm" } }, { @@ -147,7 +197,7 @@ { "binaries": "apple-silicon", "config": { - "os": "macos-13" + "os": "windows-11-arm" } }, { @@ -155,6 +205,78 @@ "config": { "os": "ubuntu-20.04" } + }, + { + "binaries": "apple-silicon", + "config": { + "os": "ubuntu-22.04" + } + }, + { + "binaries": "apple-silicon", + "config": { + "os": "macos-15-intel" + } + }, + { + "binaries": "armv8", + "config": { + "os": "windows-2022" + } + }, + { + "binaries": "armv8", + "config": { + "os": "ubuntu-20.04" + } + }, + { + "binaries": "armv8", + "config": { + "os": "ubuntu-22.04" + } + }, + { + "binaries": "armv8", + "config": { + "os": "macos-15-intel" + } + }, + { + "binaries": "armv8", + "config": { + "os": "macos-15" + } + }, + { + "binaries": "armv8-dotprod", + "config": { + "os": "windows-2022" + } + }, + { + "binaries": "armv8-dotprod", + "config": { + "os": "ubuntu-20.04" + } + }, + { + "binaries": "armv8-dotprod", + "config": { + "os": "ubuntu-22.04" + } + }, + { + "binaries": "armv8-dotprod", + "config": { + "os": "macos-15-intel" + } + }, + { + "binaries": "armv8-dotprod", + "config": { + "os": "macos-15" + } } ] } diff --git a/.github/workflows/arm_compilation.yml b/.github/workflows/arm_compilation.yml index ef141971d..86d222182 100644 --- a/.github/workflows/arm_compilation.yml +++ b/.github/workflows/arm_compilation.yml @@ -10,7 +10,7 @@ jobs: name: ${{ matrix.config.name }} ${{ matrix.binaries }} runs-on: ${{ matrix.config.os }} env: - COMPILER: ${{ matrix.config.compiler }} + COMPCXX: ${{ matrix.config.compiler }} COMP: ${{ matrix.config.comp }} EMU: ${{ matrix.config.emu }} EXT: ${{ matrix.config.ext }} @@ -26,6 +26,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Download required linux packages if: runner.os == 'Linux' @@ -37,7 +38,7 @@ jobs: if: runner.os == 'Linux' run: | if [ $COMP == ndk ]; then - NDKV="21.4.7075529" + NDKV="27.2.12479018" ANDROID_ROOT=/usr/local/lib/android ANDROID_SDK_ROOT=$ANDROID_ROOT/sdk SDKMANAGER=$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager @@ -62,7 +63,7 @@ jobs: if [ $COMP == ndk ]; then export PATH=${{ env.ANDROID_NDK_BIN }}:$PATH fi - $COMPILER -v + $COMPCXX -v - name: Test help target run: make help @@ -79,9 +80,9 @@ jobs: export LDFLAGS="-static -Wno-unused-command-line-argument" fi make clean - make -j4 profile-build ARCH=$BINARY COMP=$COMP WINE_PATH=$EMU + make -j4 profile-build ARCH=$BINARY COMP=$COMP RUN_PREFIX=$EMU make strip ARCH=$BINARY COMP=$COMP - WINE_PATH=$EMU ../tests/signature.sh $benchref + RUN_PREFIX=$EMU ../tests/signature.sh $benchref mv ./stockfish$EXT ../stockfish-android-$BINARY$EXT - name: Remove non src files @@ -91,4 +92,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ matrix.config.simple_name }} ${{ matrix.binaries }} - path: . + path: | + . + !.git + !.output diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index e20e0d5d6..46607c1e9 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -11,21 +11,25 @@ on: paths: - "**.cpp" - "**.h" + +permissions: + pull-requests: write + jobs: Clang-Format: name: Clang-Format - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Run clang-format style check - uses: jidicula/clang-format-action@f62da5e3d3a2d88ff364771d9d938773a618ab5e # @v4.11.0 + uses: jidicula/clang-format-action@4726374d1aa3c6aecf132e5197e498979588ebc8 # @v4.15.0 id: clang-format continue-on-error: true with: - clang-format-version: "17" + clang-format-version: "20" exclude-regex: "incbin" - name: Comment on PR @@ -33,12 +37,13 @@ jobs: uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # @v2.5.0 with: message: | - clang-format 17 needs to be run on this PR. + clang-format 20 needs to be run on this PR. If you do not have clang-format installed, the maintainer will run it when merging. - For the exact version please see https://packages.ubuntu.com/mantic/clang-format-17. + For the exact version please see https://packages.ubuntu.com/plucky/clang-format-20. _(execution **${{ github.run_id }}** / attempt **${{ github.run_attempt }}**)_ comment_tag: execution + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Comment on PR if: steps.clang-format.outcome != 'failure' @@ -49,3 +54,4 @@ jobs: create_if_not_exists: false comment_tag: execution mode: delete + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d949a5a76..a338083cf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,6 +30,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL @@ -45,7 +47,7 @@ jobs: - name: Build working-directory: src - run: make -j build ARCH=x86-64-modern + run: make -j build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/compilation.yml b/.github/workflows/compilation.yml index 964b5f05e..473665ec2 100644 --- a/.github/workflows/compilation.yml +++ b/.github/workflows/compilation.yml @@ -10,7 +10,7 @@ jobs: name: ${{ matrix.config.name }} ${{ matrix.binaries }} runs-on: ${{ matrix.config.os }} env: - COMPILER: ${{ matrix.config.compiler }} + COMPCXX: ${{ matrix.config.compiler }} COMP: ${{ matrix.config.comp }} EXT: ${{ matrix.config.ext }} NAME: ${{ matrix.config.simple_name }} @@ -25,6 +25,8 @@ jobs: shell: ${{ matrix.config.shell }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install fixed GCC on Linux if: runner.os == 'Linux' @@ -41,16 +43,16 @@ jobs: - name: Download SDE package if: runner.os == 'Linux' || runner.os == 'Windows' - uses: petarpetrovt/setup-sde@91a1a03434384e064706634125a15f7446d2aafb # @v2.3 + uses: petarpetrovt/setup-sde@f0fa5971dc275704531e94264dd23250c442aa41 # @v2.4 with: environmentVariableName: SDE_DIR - sdeVersion: 9.27.0 + sdeVersion: 9.33.0 - name: Download the used network from the fishtest framework run: make net - name: Check compiler - run: $COMPILER -v + run: $COMPCXX -v - name: Test help target run: make help @@ -59,24 +61,24 @@ jobs: run: git --version - name: Check compiler - run: $COMPILER -v + run: $COMPCXX -v - - name: Show g++ cpu info - if: runner.os != 'macOS' - run: g++ -Q -march=native --help=target - - - name: Show clang++ cpu info - if: runner.os == 'macOS' - run: clang++ -E - -march=native -### + - name: Show compiler cpu info + run: | + if [[ "$COMPCXX" == clang* ]]; then + $COMPCXX -E - -march=native -### + else + $COMPCXX -Q -march=native --help=target + fi # x86-64 with newer extensions tests - name: Compile ${{ matrix.config.binaries }} build run: | make clean - make -j4 profile-build ARCH=$BINARY COMP=$COMP WINE_PATH="$SDE" + make -j4 profile-build ARCH=$BINARY COMP=$COMP RUN_PREFIX="$SDE" make strip ARCH=$BINARY COMP=$COMP - WINE_PATH="$SDE" ../tests/signature.sh $benchref + RUN_PREFIX="$SDE" ../tests/signature.sh $benchref mv ./stockfish$EXT ../stockfish-$NAME-$BINARY$EXT - name: Remove non src files @@ -86,4 +88,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ matrix.config.simple_name }} ${{ matrix.binaries }} - path: . + path: | + . + !.git + !.output diff --git a/.github/workflows/games.yml b/.github/workflows/games.yml new file mode 100644 index 000000000..a50ca594b --- /dev/null +++ b/.github/workflows/games.yml @@ -0,0 +1,43 @@ +# This workflow will play games with a debug enabled SF using the PR + +name: Games +on: + workflow_call: +jobs: + Matetrack: + name: Games + runs-on: ubuntu-22.04 + steps: + - name: Checkout SF repo + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: Stockfish + persist-credentials: false + + - name: build debug enabled version of SF + working-directory: Stockfish/src + run: make -j build debug=yes + + - name: Checkout fastchess repo + uses: actions/checkout@v4 + with: + repository: Disservin/fastchess + path: fastchess + ref: 894616028492ae6114835195f14a899f6fa237d3 + persist-credentials: false + + - name: fastchess build + working-directory: fastchess + run: make -j + + - name: Run games + working-directory: fastchess + run: | + ./fastchess -rounds 4 -games 2 -repeat -concurrency 4 -openings file=app/tests/data/openings.epd format=epd order=random -srand $RANDOM\ + -engine name=sf1 cmd=/home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish\ + -engine name=sf2 cmd=/home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish\ + -ratinginterval 1 -report penta=true -each proto=uci tc=4+0.04 -log file=fast.log | tee fast.out + cat fast.log + ! grep "Assertion" fast.log > /dev/null + ! grep "disconnect" fast.out > /dev/null diff --git a/.github/workflows/iwyu.yml b/.github/workflows/iwyu.yml index 0552a598c..f8898b1c9 100644 --- a/.github/workflows/iwyu.yml +++ b/.github/workflows/iwyu.yml @@ -14,6 +14,7 @@ jobs: uses: actions/checkout@v4 with: path: Stockfish + persist-credentials: false - name: Checkout include-what-you-use uses: actions/checkout@v4 @@ -21,6 +22,7 @@ jobs: repository: include-what-you-use/include-what-you-use ref: f25caa280dc3277c4086ec345ad279a2463fea0f path: include-what-you-use + persist-credentials: false - name: Download required linux packages run: | diff --git a/.github/workflows/matetrack.yml b/.github/workflows/matetrack.yml new file mode 100644 index 000000000..43d35ca18 --- /dev/null +++ b/.github/workflows/matetrack.yml @@ -0,0 +1,90 @@ +# This workflow will run matetrack on the PR + +name: Matetrack +on: + workflow_call: +jobs: + Matetrack: + name: Matetrack + runs-on: ubuntu-22.04 + steps: + - name: Checkout SF repo + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: Stockfish + persist-credentials: false + + - name: build SF + working-directory: Stockfish/src + run: make -j profile-build + + - name: Checkout matetrack repo + uses: actions/checkout@v4 + with: + repository: vondele/matetrack + path: matetrack + ref: 2d96fa3373f90edb032b7ea7468473fb9e6f0343 + persist-credentials: false + + - name: matetrack install deps + working-directory: matetrack + run: pip install -r requirements.txt + + - name: cache syzygy + id: cache-syzygy + uses: actions/cache@v4 + with: + path: | + matetrack/3-4-5-wdl/ + matetrack/3-4-5-dtz/ + key: key-syzygy + + - name: download syzygy 3-4-5 if needed + working-directory: matetrack + if: steps.cache-syzygy.outputs.cache-hit != 'true' + run: | + wget --no-verbose -r -nH --cut-dirs=2 --no-parent --reject="index.html*" -e robots=off https://tablebase.lichess.ovh/tables/standard/3-4-5-wdl/ + wget --no-verbose -r -nH --cut-dirs=2 --no-parent --reject="index.html*" -e robots=off https://tablebase.lichess.ovh/tables/standard/3-4-5-dtz/ + + - name: Run matetrack th1 + working-directory: matetrack + run: | + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 | tee matecheckout1.out + ! grep "issues were detected" matecheckout1.out > /dev/null + + - name: Run matetrack th4 + working-directory: matetrack + run: | + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 --threads 4 | tee matecheckout4.out + ! grep "issues were detected" matecheckout4.out > /dev/null + + - name: Run matetrack th1 with --syzygy50MoveRule false + working-directory: matetrack + run: | + grep 5men cursed.epd > cursed5.epd + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile cursed5.epd --nodes 100000 --syzygy50MoveRule false | tee matecheckcursed1.out + ! grep "issues were detected" matecheckcursed1.out > /dev/null + + - name: Run matetrack th4 with --syzygy50MoveRule false + working-directory: matetrack + run: | + grep 5men cursed.epd > cursed5.epd + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile cursed5.epd --nodes 100000 --threads 4 --syzygy50MoveRule false | tee matecheckcursed4.out + ! grep "issues were detected" matecheckcursed4.out > /dev/null + + - name: Verify mate and TB win count for matecheckcursed[14].out + working-directory: matetrack + run: | + mates=$(grep "Found mates:" matecheckcursed1.out | awk '{print $3}') + tbwins=$(grep "Found TB wins:" matecheckcursed1.out | awk '{print $4}') + if [ $(($mates + $tbwins)) -ne 32 ]; then + echo "Sum of mates and TB wins is not 32 in matecheckcursed1.out" >&2 + exit 1 + fi + mates=$(grep "Found mates:" matecheckcursed4.out | awk '{print $3}') + tbwins=$(grep "Found TB wins:" matecheckcursed4.out | awk '{print $4}') + if [ $(($mates + $tbwins)) -ne 32 ]; then + echo "Sum of mates and TB wins is not 32 in matecheckcursed4.out" >&2 + exit 1 + fi diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index 7ab1f997a..d9b783eb6 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -6,7 +6,7 @@ jobs: name: ${{ matrix.sanitizers.name }} runs-on: ${{ matrix.config.os }} env: - COMPILER: ${{ matrix.config.compiler }} + COMPCXX: ${{ matrix.config.compiler }} COMP: ${{ matrix.config.comp }} CXXFLAGS: "-Werror" strategy: @@ -21,22 +21,36 @@ jobs: sanitizers: - name: Run with thread sanitizer make_option: sanitize=thread + cxx_extra_flags: "" instrumented_option: sanitizer-thread - name: Run with UB sanitizer make_option: sanitize=undefined + cxx_extra_flags: "" instrumented_option: sanitizer-undefined - name: Run under valgrind make_option: "" + cxx_extra_flags: "" instrumented_option: valgrind - name: Run under valgrind-thread make_option: "" + cxx_extra_flags: "" instrumented_option: valgrind-thread + - name: Run non-instrumented + make_option: "" + cxx_extra_flags: "" + instrumented_option: none + - name: Run with glibcxx assertions + make_option: "" + cxx_extra_flags: -D_GLIBCXX_ASSERTIONS + instrumented_option: none defaults: run: working-directory: src shell: ${{ matrix.config.shell }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Download required linux packages run: | @@ -47,7 +61,7 @@ jobs: run: make net - name: Check compiler - run: $COMPILER -v + run: $COMPCXX -v - name: Test help target run: make help @@ -55,11 +69,19 @@ jobs: - name: Check git run: git --version + # Since Linux Kernel 6.5 we are getting false positives from the ci, + # lower the ALSR entropy to disable ALSR, which works as a temporary workaround. + # https://github.com/google/sanitizers/issues/1716 + # https://bugs.launchpad.net/ubuntu/+source/linux/+bug/2056762 + + - name: Lower ALSR entropy + run: sudo sysctl -w vm.mmap_rnd_bits=28 + # Sanitizers - name: ${{ matrix.sanitizers.name }} run: | - export CXXFLAGS="-O1 -fno-inline" + export CXXFLAGS="-O1 -fno-inline ${{ matrix.sanitizers.cxx_extra_flags }}" make clean make -j4 ARCH=x86-64-sse41-popcnt ${{ matrix.sanitizers.make_option }} debug=yes optimize=no build > /dev/null - ../tests/instrumented.sh --${{ matrix.sanitizers.instrumented_option }} + python3 ../tests/instrumented.py --${{ matrix.sanitizers.instrumented_option }} ./stockfish diff --git a/.github/workflows/stockfish.yml b/.github/workflows/stockfish.yml index 13d57f9ed..1f87e061b 100644 --- a/.github/workflows/stockfish.yml +++ b/.github/workflows/stockfish.yml @@ -15,8 +15,12 @@ jobs: Prerelease: if: github.repository == 'official-stockfish/Stockfish' && (github.ref == 'refs/heads/master' || (startsWith(github.ref_name, 'sf_') && github.ref_type == 'tag')) runs-on: ubuntu-latest + permissions: + contents: write # For deleting/creating a prerelease steps: - uses: actions/checkout@v4 + with: + persist-credentials: false # returns null if no pre-release exists - name: Get Commit SHA of Latest Pre-release @@ -66,6 +70,8 @@ jobs: arm_matrix: ${{ steps.set-arm-matrix.outputs.arm_matrix }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - id: set-matrix run: | TASKS=$(echo $(cat .github/ci/matrix.json) ) @@ -90,15 +96,27 @@ jobs: uses: ./.github/workflows/sanitizers.yml Tests: uses: ./.github/workflows/tests.yml + Matetrack: + uses: ./.github/workflows/matetrack.yml + Games: + uses: ./.github/workflows/games.yml Binaries: if: github.repository == 'official-stockfish/Stockfish' needs: [Matrix, Prerelease, Compilation] uses: ./.github/workflows/upload_binaries.yml with: matrix: ${{ needs.Matrix.outputs.matrix }} + permissions: + contents: write # For deleting/creating a (pre)release + secrets: + token: ${{ secrets.GITHUB_TOKEN }} ARM_Binaries: if: github.repository == 'official-stockfish/Stockfish' needs: [Matrix, Prerelease, ARMCompilation] uses: ./.github/workflows/upload_binaries.yml with: matrix: ${{ needs.Matrix.outputs.arm_matrix }} + permissions: + contents: write # For deleting/creating a (pre)release + secrets: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 702e86e5f..f0da51e7e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,22 +6,22 @@ jobs: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} env: - COMPILER: ${{ matrix.config.compiler }} + COMPCXX: ${{ matrix.config.compiler }} COMP: ${{ matrix.config.comp }} CXXFLAGS: "-Werror" strategy: fail-fast: false matrix: config: - - name: Ubuntu 20.04 GCC - os: ubuntu-20.04 + - name: Ubuntu 22.04 GCC + os: ubuntu-22.04 compiler: g++ comp: gcc run_32bit_tests: true run_64bit_tests: true shell: bash - - name: Ubuntu 20.04 Clang - os: ubuntu-20.04 + - name: Ubuntu 22.04 Clang + os: ubuntu-22.04 compiler: clang++ comp: clang run_32bit_tests: true @@ -29,24 +29,25 @@ jobs: shell: bash - name: Android NDK aarch64 os: ubuntu-22.04 - compiler: aarch64-linux-android21-clang++ + compiler: aarch64-linux-android29-clang++ comp: ndk run_armv8_tests: true shell: bash - name: Android NDK arm os: ubuntu-22.04 - compiler: armv7a-linux-androideabi21-clang++ + compiler: armv7a-linux-androideabi29-clang++ comp: ndk run_armv7_tests: true shell: bash - - name: Linux GCC riscv64 - os: ubuntu-22.04 - compiler: g++ - comp: gcc - run_riscv64_tests: true - base_image: "riscv64/alpine:edge" - platform: linux/riscv64 - shell: bash + # Currently segfaults in the CI unrelated to a Stockfish change. + # - name: Linux GCC riscv64 + # os: ubuntu-22.04 + # compiler: g++ + # comp: gcc + # run_riscv64_tests: true + # base_image: "riscv64/alpine:edge" + # platform: linux/riscv64 + # shell: bash - name: Linux GCC ppc64 os: ubuntu-22.04 compiler: g++ @@ -55,21 +56,21 @@ jobs: base_image: "ppc64le/alpine:latest" platform: linux/ppc64le shell: bash - - name: MacOS 13 Apple Clang - os: macos-13 + - name: macOS 15 Apple Clang + os: macos-15-intel compiler: clang++ comp: clang run_64bit_tests: true shell: bash - - name: MacOS 14 Apple Clang M1 - os: macos-14 + - name: macOS 15 Apple Clang M1 + os: macos-15 compiler: clang++ comp: clang run_64bit_tests: false run_m1_tests: true shell: bash - - name: MacOS 13 GCC 11 - os: macos-13 + - name: macOS 15 GCC 11 + os: macos-15-intel compiler: g++-11 comp: gcc run_64bit_tests: true @@ -98,6 +99,14 @@ jobs: msys_sys: clang64 msys_env: clang-x86_64-clang shell: msys2 {0} + - name: Windows 11 Mingw-w64 Clang arm64 + os: windows-11-arm + compiler: clang++ + comp: clang + run_armv8_tests: true + msys_sys: clangarm64 + msys_env: clang-aarch64-clang + shell: msys2 {0} defaults: run: working-directory: src @@ -106,6 +115,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Download required linux packages if: runner.os == 'Linux' @@ -117,7 +127,7 @@ jobs: if: runner.os == 'Linux' run: | if [ $COMP == ndk ]; then - NDKV="21.4.7075529" + NDKV="27.2.12479018" ANDROID_ROOT=/usr/local/lib/android ANDROID_SDK_ROOT=$ANDROID_ROOT/sdk SDKMANAGER=$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager @@ -138,16 +148,16 @@ jobs: - name: Build Docker container if: matrix.config.base_image run: | - docker buildx build --load -t sf_builder - << EOF + docker buildx build --platform ${{ matrix.config.platform }} --load -t sf_builder - << EOF FROM ${{ matrix.config.base_image }} WORKDIR /app RUN apk update && apk add make g++ - CMD ["sh", "script.sh"] + CMD ["sh", "src/script.sh"] EOF - name: Download required macOS packages if: runner.os == 'macOS' - run: brew install coreutils + run: brew install coreutils gcc@11 - name: Setup msys and install required packages if: runner.os == 'Windows' @@ -172,10 +182,10 @@ jobs: if [ $COMP == ndk ]; then export PATH=${{ env.ANDROID_NDK_BIN }}:$PATH fi - $COMPILER -v + $COMPCXX -v else - echo "$COMPILER -v" > script.sh - docker run --rm --platform ${{ matrix.config.platform }} -v ${{ github.workspace }}/src:/app sf_builder + echo "$COMPCXX -v" > script.sh + docker run --rm --platform ${{ matrix.config.platform }} -v ${{ github.workspace }}:/app sf_builder fi - name: Test help target @@ -301,8 +311,10 @@ jobs: - name: Test armv8 build if: matrix.config.run_armv8_tests run: | - export PATH=${{ env.ANDROID_NDK_BIN }}:$PATH - export LDFLAGS="-static -Wno-unused-command-line-argument" + if [ $COMP == ndk ]; then + export PATH=${{ env.ANDROID_NDK_BIN }}:$PATH + export LDFLAGS="-static -Wno-unused-command-line-argument" + fi make clean make -j4 ARCH=armv8 build ../tests/signature.sh $benchref @@ -310,8 +322,10 @@ jobs: - name: Test armv8-dotprod build if: matrix.config.run_armv8_tests run: | - export PATH=${{ env.ANDROID_NDK_BIN }}:$PATH - export LDFLAGS="-static -Wno-unused-command-line-argument" + if [ $COMP == ndk ]; then + export PATH=${{ env.ANDROID_NDK_BIN }}:$PATH + export LDFLAGS="-static -Wno-unused-command-line-argument" + fi make clean make -j4 ARCH=armv8-dotprod build ../tests/signature.sh $benchref @@ -341,8 +355,8 @@ jobs: - name: Test riscv64 build if: matrix.config.run_riscv64_tests run: | - echo "export LDFLAGS='-static' && make clean && make -j4 ARCH=riscv64 build" > script.sh - docker run --rm --platform ${{ matrix.config.platform }} -v ${{ github.workspace }}/src:/app sf_builder + echo "cd src && export LDFLAGS='-static' && make clean && make -j4 ARCH=riscv64 build" > script.sh + docker run --rm --platform ${{ matrix.config.platform }} -v ${{ github.workspace }}:/app sf_builder ../tests/signature.sh $benchref # ppc64 tests @@ -350,8 +364,8 @@ jobs: - name: Test ppc64 build if: matrix.config.run_ppc64_tests run: | - echo "export LDFLAGS='-static' && make clean && make -j4 ARCH=ppc-64 build" > script.sh - docker run --rm --platform ${{ matrix.config.platform }} -v ${{ github.workspace }}/src:/app sf_builder + echo "cd src && export LDFLAGS='-static' && make clean && make -j4 ARCH=ppc-64 build" > script.sh + docker run --rm --platform ${{ matrix.config.platform }} -v ${{ github.workspace }}:/app sf_builder ../tests/signature.sh $benchref # Other tests diff --git a/.github/workflows/upload_binaries.yml b/.github/workflows/upload_binaries.yml index 015b514ce..073e40a13 100644 --- a/.github/workflows/upload_binaries.yml +++ b/.github/workflows/upload_binaries.yml @@ -5,26 +5,28 @@ on: matrix: type: string required: true + secrets: + token: + required: true jobs: Artifacts: name: ${{ matrix.config.name }} ${{ matrix.binaries }} - runs-on: ${{ matrix.config.os }} + runs-on: ubuntu-latest env: - COMPILER: ${{ matrix.config.compiler }} - COMP: ${{ matrix.config.comp }} EXT: ${{ matrix.config.ext }} NAME: ${{ matrix.config.simple_name }} BINARY: ${{ matrix.binaries }} - SDE: ${{ matrix.config.sde }} strategy: fail-fast: false matrix: ${{ fromJson(inputs.matrix) }} defaults: run: - shell: ${{ matrix.config.shell }} + shell: bash steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Download artifact from compilation uses: actions/download-artifact@v4 @@ -32,13 +34,6 @@ jobs: name: ${{ matrix.config.simple_name }} ${{ matrix.binaries }} path: ${{ matrix.config.simple_name }} ${{ matrix.binaries }} - - name: Setup msys and install required packages - if: runner.os == 'Windows' - uses: msys2/setup-msys2@v2 - with: - msystem: ${{ matrix.config.msys_sys }} - install: mingw-w64-${{ matrix.config.msys_env }} make git zip - - name: Create Package run: | mkdir stockfish @@ -54,6 +49,7 @@ jobs: mv "${{ matrix.config.simple_name }} ${{ matrix.binaries }}" stockfish-workflow cd stockfish-workflow cp -r src ../stockfish/ + cp -r scripts ../stockfish/ cp stockfish-$NAME-$BINARY$EXT ../stockfish/ cp "Top CPU Contributors.txt" ../stockfish/ cp Copying.txt ../stockfish/ @@ -63,13 +59,13 @@ jobs: cp CONTRIBUTING.md ../stockfish/ - name: Create tar - if: runner.os != 'Windows' + if: ${{ !startsWith(matrix.config.os, 'windows') }} run: | chmod +x ./stockfish/stockfish-$NAME-$BINARY$EXT tar -cvf stockfish-$NAME-$BINARY.tar stockfish - name: Create zip - if: runner.os == 'Windows' + if: ${{ startsWith(matrix.config.os, 'windows') }} run: | zip -r stockfish-$NAME-$BINARY.zip stockfish @@ -78,6 +74,7 @@ jobs: uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981 with: files: stockfish-${{ matrix.config.simple_name }}-${{ matrix.binaries }}.${{ matrix.config.archive_ext }} + token: ${{ secrets.token }} - name: Get last commit sha id: last_commit @@ -104,3 +101,4 @@ jobs: tag_name: stockfish-dev-${{ env.COMMIT_DATE }}-${{ env.COMMIT_SHA }} prerelease: true files: stockfish-${{ matrix.config.simple_name }}-${{ matrix.binaries }}.${{ matrix.config.archive_ext }} + token: ${{ secrets.token }} diff --git a/.gitignore b/.gitignore index 8981efcaf..2fc80d487 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ src/-lstdc++.res # Neural network for the NNUE evaluation **/*.nnue +# Files generated by the instrumented tests +tsan.supp +__pycache__/ +tests/syzygy +tests/bench_tmp.epd \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index abae401c1..fac1e7d8d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Motohiro Isozaki (yaneurao) Hisayori Noda (nodchip) # All other authors of Stockfish code (in alphabetical order) +87flowers Aditya (absimaldata) Adrian Petrescu (apetresc) Ahmed Kerimov (wcdbmv) @@ -20,6 +21,8 @@ Alexander Kure Alexander Pagel (Lolligerhans) Alfredo Menezes (lonfom169) Ali AlZhrani (Cooffe) +AliceRoselia +Andreas Jan van der Meulen (Andyson007) Andreas Matthies (Matthies) Andrei Vetrov (proukornew) Andrew Grant (AndyGrant) @@ -28,10 +31,13 @@ Andy Duplain Antoine Champion (antoinechampion) Aram Tumanian (atumanian) Arjun Temurnikar +Aron Petkovski (fury) +Arseniy Surkov (codedeliveryservice) Artem Solopiy (EntityFX) Auguste Pop Balazs Szilagyi Balint Pfliegel +Baptiste Rech (breatn) Ben Chaney (Chaneybenjamini) Ben Koshy (BKSpurgeon) Bill Henry (VoyagerOne) @@ -44,15 +50,20 @@ Bruno de Melo Costa (BM123499) Bruno Pellanda (pellanda) Bryan Cross (crossbr) candirufish +Carlos Esparza Sánchez (ces42) Chess13234 +Chris Bao (sscg13) Chris Cain (ceebo) +Ciekce clefrks Clemens L. (rn5f107s2) Cody Ho (aesrentai) +CSTENTOR Dale Weiler (graphitemaster) Daniel Axtens (daxtens) Daniel Dugovic (ddugovic) -Daniel Monroe (Ergodice) +Daniel Monroe (daniel-monroe) +Daniel Samek (DanSamek) Dan Schmidt (dfannius) Dariusz Orzechowski (dorzechowski) David (dav1312) @@ -67,9 +78,11 @@ Douglas Matos Gomes (dsmsgms) Dubslow Eduardo Cáceres (eduherminio) Eelco de Groot (KingDefender) +Ehsan Rashid (erashid) Elvin Liu (solarlight2) erbsenzaehler Ernesto Gatti +evqsx Fabian Beuke (madnight) Fabian Fichter (ianfab) Fanael Linithien (Fanael) @@ -113,19 +126,27 @@ Jonathan McDermid (jonathanmcdermid) Joost VandeVondele (vondele) Joseph Ellis (jhellis3) Joseph R. Prostko +Jost Triller (tsoj) Jörg Oster (joergoster) Julian Willemer (NightlyKing) jundery Justin Blanchard (UncombedCoconut) +Kazuki Yamashita (KazApps) Kelly Wilson Ken Takusagawa +Kenneth Lee (kennethlee33) +kevlu8 Kian E (KJE-98) +Kieren Pearson (KierenP) kinderchocolate Kiran Panditrao (Krgp) +Kirill Zaripov (kokodio) Kojirion +Krisztián Peőcz Krystian Kuzniarek (kuzkry) Leonardo Ljubičić (ICCF World Champion) Leonid Pechenik (lp--) +Li Ying (yl25946) Liam Keegan (lkeegan) Linmiao Xu (linrock) Linus Arver (listx) @@ -136,8 +157,10 @@ Lucas Braesch (lucasart) Lyudmil Antonov (lantonov) Maciej Żenczykowski (zenczykowski) Malcolm Campbell (xoto10) +Mark Marosi (Mapika) Mark Tenzer (31m059) marotear +Mathias Parnaudeau (mparnaudeau) Matt Ginsberg (mattginsberg) Matthew Lai (matthewlai) Matthew Sullivan (Matt14916) @@ -162,19 +185,23 @@ Nathan Rugg (nmrugg) Nguyen Pham (nguyenpham) Nicklas Persson (NicklasPersson) Nick Pelling (nickpelling) +Nicolas Duhamel (nikloskoda) Niklas Fiekas (niklasf) Nikolay Kostov (NikolayIT) Norman Schmidt (FireFather) notruck +Nour Berakdar (Nonlinear) Ofek Shochat (OfekShochat, ghostway) Ondrej Mosnáček (WOnder93) Ondřej Mišina (AndrovT) Oskar Werkelin Ahlin +Ömer Faruk Tutkun (OmerFarukTutkun) Pablo Vazquez Panthee Pascal Romaret Pasquale Pigazzini (ppigazzini) Patrick Jansen (mibere) +Patrick Leonhardt (Yoshie2000) Peter Schneider (pschneider1968) Peter Zsifkovits (CoffeeOne) PikaCat @@ -193,6 +220,7 @@ Ronald de Man (syzygy1, syzygy) Ron Britvich (Britvich) rqs Rui Coelho (ruicoelhopedro) +rustam-cpp Ryan Schmitt Ryan Takker Sami Kiminki (skiminki) @@ -212,12 +240,15 @@ Stefano Di Martino (StefanoD) Steinar Gunderson (sesse) Stéphane Nicolet (snicolet) Stephen Touset (stouset) +Stockfisher69 +Styx (styxdoto) Syine Mineta (MinetaS) Taras Vuk (TarasVuk) Thanar2 thaspel theo77186 TierynnB +Timothy Herchen (anematode) Ting-Hsuan Huang (fffelix-huang) Tobias Steinmann Tomasz Sobczyk (Sopel97) @@ -231,6 +262,7 @@ Unai Corzo (unaiic) Uri Blass (uriblass) Vince Negri (cuddlestmonkey) Viren +Wencey Wang windfishballad xefoci7612 Xiang Wang (KatyushaScarlet) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf9cecda2..07fe9f542 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,7 @@ further discussion._ - Provide a clear and concise description of the changes in the pull request description. -_First time contributors should add their name to [AUTHORS](../AUTHORS)._ +_First time contributors should add their name to [AUTHORS](./AUTHORS)._ _Stockfish's development is not focused on adding new features. Thus any pull request introducing new features will potentially be closed without further @@ -59,7 +59,7 @@ discussion._ Changes to Stockfish C++ code should respect our coding style defined by [.clang-format](.clang-format). You can format your changes by running -`make format`. This requires clang-format version 17 to be installed on your system. +`make format`. This requires clang-format version 20 to be installed on your system. ## Navigate @@ -86,7 +86,6 @@ more details. Thank you for contributing to Stockfish and helping us make it even better! - [copying-link]: https://github.com/official-stockfish/Stockfish/blob/master/Copying.txt [discord-link]: https://discord.gg/GWDRS3kU6R [discussions-link]: https://github.com/official-stockfish/Stockfish/discussions/new diff --git a/README.md b/README.md index 2813549a6..165b525cf 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ descriptions. An example suitable for most Intel and AMD chips: ``` cd src -make -j profile-build ARCH=x86-64-avx2 +make -j profile-build ``` Detailed compilation instructions for all platforms can be found in our @@ -147,6 +147,11 @@ where the source code can be found) to generate the exact binary you are distributing. If you make any changes to the source code, these changes must also be made available under GPL v3. +## Acknowledgements + +Stockfish uses neural networks trained on [data provided by the Leela Chess Zero +project][lc0-data-link], which is made available under the [Open Database License][odbl-link] (ODbL). + [authors-link]: https://github.com/official-stockfish/Stockfish/blob/master/AUTHORS [build-link]: https://github.com/official-stockfish/Stockfish/actions/workflows/stockfish.yml @@ -171,6 +176,8 @@ also be made available under GPL v3. [wiki-uci-link]: https://github.com/official-stockfish/Stockfish/wiki/UCI-&-Commands [wiki-usage-link]: https://github.com/official-stockfish/Stockfish/wiki/Download-and-usage [worker-link]: https://github.com/official-stockfish/fishtest/wiki/Running-the-worker +[lc0-data-link]: https://storage.lczero.org/files/training_data +[odbl-link]: https://opendatacommons.org/licenses/odbl/odbl-10.txt [build-badge]: https://img.shields.io/github/actions/workflow/status/official-stockfish/Stockfish/stockfish.yml?branch=master&style=for-the-badge&label=stockfish&logo=github [commits-badge]: https://img.shields.io/github/commits-since/official-stockfish/Stockfish/latest?style=for-the-badge diff --git a/Top CPU Contributors.txt b/Top CPU Contributors.txt index 11636e840..f8134a199 100644 --- a/Top CPU Contributors.txt +++ b/Top CPU Contributors.txt @@ -1,146 +1,186 @@ -Contributors to Fishtest with >10,000 CPU hours, as of 2024-02-24. +Contributors to Fishtest with >10,000 CPU hours, as of 2025-12-24. Thank you! Username CPU Hours Games played ------------------------------------------------------------------ -noobpwnftw 39302472 3055513453 -technologov 20845762 994893444 -linrock 8616428 560281417 +noobpwnftw 42692720 3385202467 +vdv 39922218 1277282126 +technologov 26354561 1163905856 +linrock 12002255 785641643 +olafm 3030005 197722318 mlang 3026000 200065824 -okrout 2332151 222639518 -pemo 1800019 60274069 +okrout 3020471 268364402 +pemo 2009761 66178221 +TueRens 1956328 83294326 +sebastronomy 1806628 73868874 dew 1689162 100033738 -TueRens 1474943 75121774 -grandphish2 1463002 91616949 -JojoM 1109702 72927902 -olafm 978631 71037944 -sebastronomy 939955 44920556 +grandphish2 1479778 92306101 +JojoM 1130646 73666860 +rpngn 1081976 65292619 +oz 1029329 69522328 +gvreuls 844572 59249068 tvijlbrief 796125 51897690 -gvreuls 711320 49142318 mibere 703840 46867607 -oz 646268 46293638 -rpngn 572571 38928563 -leszek 531858 39316505 -cw 518116 34894291 +leszek 609538 45301765 +cw 519602 34988289 fastgm 503862 30260818 -CSU_Dynasty 468784 31385034 -ctoks 434591 28520597 -maximmasiutin 429983 27066286 +robal 503208 32703510 +maximmasiutin 500174 30818270 +CSU_Dynasty 481663 31916842 +ctoks 435431 28551199 crunchy 427414 27371625 bcross 415724 29061187 +mgrabiak 380202 27586936 +tolkki963 358623 26373242 velislav 342588 22140902 -mgrabiak 338763 23999170 +ncfish1 329039 20624527 Fisherman 327231 21829379 -robal 299836 20213182 +Fifis 323909 16200123 +Sylvain27 320732 11671388 +marrco 310446 19587107 +Calis007 310201 18969692 +Viren6 297938 5847458 Dantist 296386 18031762 -ncfish1 267604 17881149 +naclosagc 296040 13865010 +anematode 293146 3918134 +maposora 278093 20454200 +javran 271465 20506096 +cody 258835 13301710 nordlandia 249322 16420192 -marrco 234581 17714473 -tolkki963 233490 19773930 +Goatminola 218812 21411814 +Torom 211061 7238522 glinscott 208125 13277240 drabel 204167 13930674 +Wencey 203584 9943614 mhoram 202894 12601997 +sschnee 201756 12874780 bking_US 198894 11876016 -Calis007 188631 12795784 +Mineta 195312 10337614 Thanar 179852 12365359 -Fifis 176209 10638245 -vdv 175544 9904472 +armo9494 169747 11254404 +amicic 161636 11290899 +DesolatedDodo 160605 10392474 +markkulix 158320 13538874 spams 157128 10319326 -DesolatedDodo 156659 10210328 -armo9494 155355 10566898 sqrt2 147963 9724586 +vdbergh 141201 9308647 jcAEie 140086 10603658 -vdbergh 139746 9172061 CoffeeOne 137100 5024116 malala 136182 8002293 xoto 133759 9159372 +Dubslow 130795 8609646 +zeryl 129154 7911565 davar 129023 8376525 DMBK 122960 8980062 +cuistot 122470 8393996 +megaman7de 122254 8066174 dsmith 122059 7570238 -javran 121564 10144656 -amicic 119661 7938029 -sschnee 118107 7389266 -Wolfgang 114616 8070494 +Wolfgang 120919 8619168 +CypressChess 120902 8683904 +sterni1971 113754 6054022 +Spprtr 113356 8129809 Data 113305 8220352 BrunoBanani 112960 7436849 -Wencey 111502 5991676 -cuistot 108503 7006992 -CypressChess 108331 7759788 skiminki 107583 7218170 +MediumBerry5575 103884 7830022 MaZePallas 102823 6633619 -sterni1971 100532 5880772 +YvesKn 102213 5098076 sunu 100167 7040199 -zeryl 99331 6221261 -thirdlife 99156 2245320 +thirdlife 99182 2246960 ElbertoOne 99028 7023771 -Dubslow 98600 6903242 -markkulix 97010 7643900 -bigpen0r 94809 6529203 +TechiePirate 98957 1249064 +DeepnessFulled 97313 5083358 +TataneSan 97257 4239502 +romangol 95662 7784954 +bigpen0r 94825 6529241 +jojo2357 94358 7635486 +malfoy 92712 3392874 +voidedstarlight 92582 2342038 brabos 92118 6186135 Maxim 90818 3283364 psk 89957 5984901 -megaman7de 88822 6052132 +szupaw 89775 7800606 +jromang 87260 5988073 racerschmacer 85805 6122790 -maposora 85710 7778146 Vizvezdenec 83761 5344740 0x3C33 82614 5271253 +MarcusTullius 82359 5335665 BRAVONE 81239 5054681 +rn 78566 6000852 nssy 76497 5259388 -jromang 76106 5236025 +woutboat 76379 6031688 teddybaer 75125 5407666 Pking_cda 73776 5293873 -yurikvelo 73516 5036928 -MarcusTullius 71053 4803477 +yurikvelo 73611 5046822 +Zirie 71260 4602355 Bobo1239 70579 4794999 solarlight 70517 5028306 dv8silencer 70287 3883992 -Spprtr 69646 4806763 -Mineta 66325 4537742 +0x539 67147 2918044 manap 66273 4121774 -szupaw 65468 5669742 tinker 64333 4268790 +CounterFlow 63914 3775062 +mecevdimitar 62493 3508750 +DanielMiao1 62188 1335664 qurashee 61208 3429862 -woutboat 59496 4906352 -AGI 58195 4329580 +AGI 58316 4336328 robnjr 57262 4053117 Freja 56938 3733019 MaxKlaxxMiner 56879 3423958 ttruscott 56010 3680085 rkl 55132 4164467 -jmdana 54697 4012593 +jmdana 54988 4041917 +notchris 53936 4184018 renouve 53811 3501516 -notchris 52433 4044590 +jibarbosa 53504 5110028 +somethingintheshadows 52333 4344808 finfish 51360 3370515 eva42 51272 3599691 eastorwest 51117 3454811 -Goatminola 51004 4432492 +sylvek 50391 3765170 rap 49985 3219146 pb00067 49733 3298934 GPUex 48686 3684998 OuaisBla 48626 3445134 +lemtea 48563 1672454 ronaldjerum 47654 3240695 +abdicj 46740 2709482 biffhero 46564 3111352 -oryx 45533 3539290 +oryx 46422 3607582 VoyagerOne 45476 3452465 +rdp65536 43948 2881890 speedycpu 43842 3003273 jbwiebe 43305 2805433 +gopeto 43046 2821514 Antihistamine 41788 2761312 mhunt 41735 2691355 +WoodMan777 40858 3491196 +Epic29 40771 4067404 +drauh 40419 1634770 homyur 39893 2850481 gri 39871 2515779 +vidar808 39774 1656372 +Gaster319 38994 3477702 Garf 37741 2999686 SC 37299 2731694 -Sylvain27 36520 1467082 +ZacHFX 36533 2553282 csnodgrass 36207 2688994 -Gaster319 35655 3149442 +icewulf 34935 2421834 strelock 34716 2074055 +Jopo12321 33921 2531448 +xuhdev 33798 3295210 +csnodgra 33780 1446866 EthanOConnor 33370 2090311 slakovv 32915 2021889 -gopeto 31884 2076712 +IslandLambda 32667 1659344 +Kataiser 32477 2688862 +shawnxu 32330 2830036 +srowen 32248 1791136 +qgluca 31941 2491622 Gelma 31771 1551204 kdave 31157 2198362 manapbk 30987 1810399 -ZacHFX 30551 2238078 +votoanthuan 30691 2460856 Prcuvu 30377 2170122 anst 30301 2190091 jkiiski 30136 1904470 @@ -148,139 +188,172 @@ spcc 29925 1901692 hyperbolic.tom 29840 2017394 chuckstablers 29659 2093438 Pyafue 29650 1902349 +Flopzee 29388 1899905 +hoching 29054 2067144 belzedar94 28846 1811530 -votoanthuan 27978 2285818 -shawnxu 27438 2465810 +wizardassassin 28007 2318204 +purpletree 27892 2061966 +Kyrega 27674 963872 +joendter 27193 1781570 +Danielv123 27132 1043614 chriswk 26902 1868317 xwziegtm 26897 2124586 +spotscene 26877 2139674 achambord 26582 1767323 +shreven 26448 1703328 Patrick_G 26276 1801617 yorkman 26193 1992080 -Ulysses 25397 1701264 -Jopo12321 25227 1652482 +ols 26173 1443517 +wer 26136 793146 +Skiff84 26083 1135002 +RudyMars 25980 2211364 +Ulysses 25544 1714542 SFTUser 25182 1675689 nabildanial 25068 1531665 Sharaf_DG 24765 1786697 rodneyc 24376 1416402 jsys14 24297 1721230 +AndreasKrug 24235 1934711 agg177 23890 1395014 -srowen 23842 1342508 +Disservin 23768 1934576 Ente 23752 1678188 -jojo2357 23479 2061238 JanErik 23408 1703875 Isidor 23388 1680691 Norabor 23371 1603244 +Nullvalue 23155 2022752 +fishtester 23115 1581502 cisco2015 22920 1763301 -Zirie 22542 1472937 -Nullvalue 22490 1970374 -AndreasKrug 22485 1769491 +Hjax 22561 1566151 +gerbil 22435 1679842 +Serpensin 22396 1861156 team-oh 22272 1636708 +mkstockfishtester 22253 2029566 Roady 22220 1465606 +tsim67 22077 1353048 MazeOfGalious 21978 1629593 -sg4032 21947 1643353 +sg4032 21950 1643373 +sev 21791 1983016 ianh2105 21725 1632562 xor12 21628 1680365 dex 21612 1467203 nesoneg 21494 1463031 user213718 21454 1404128 sphinx 21211 1384728 -qoo_charly_cai 21135 1514907 +qoo_charly_cai 21136 1514927 jjoshua2 21001 1423089 Zake9298 20938 1565848 horst.prack 20878 1465656 0xB00B1ES 20590 1208666 -Serpensin 20487 1729674 -Dinde 20440 1292390 +t3hf1sht3ster 20544 673134 +Dinde 20459 1292774 j3corre 20405 941444 Adrian.Schmidt123 20316 1281436 wei 19973 1745989 -fishtester 19617 1257388 +teenychess 19819 1762006 +RickGroszkiewicz 19749 1913986 rstoesser 19569 1293588 eudhan 19274 1283717 +nalanzeyu 19211 396674 vulcan 18871 1729392 Karpovbot 18766 1053178 -WoodMan777 18556 1628264 +Farseer 18536 1078326 jundery 18445 1115855 +sebv15 18267 1262588 +whelanh 17887 347974 ville 17883 1384026 chris 17698 1487385 purplefishies 17595 1092533 dju 17414 981289 -ols 17291 1042003 iisiraider 17275 1049015 -Skiff84 17111 950248 +Karby 17177 1030688 +fogleman 17134 815562 +zhujianzhao 17111 1666972 DragonLord 17014 1162790 +pirt 16993 1274363 redstone59 16842 1461780 -Karby 16839 1010124 Alb11747 16787 1213990 -pirt 16493 1237199 Naven94 16414 951718 -wizardassassin 16392 1148672 +scuzzi 16155 995347 IgorLeMasson 16064 1147232 -scuzzi 15757 968735 +micpilar 15866 1399266 ako027ako 15671 1173203 +infinigon 15285 965966 +fishtrawler 15205 1436165 Nikolay.IT 15154 1068349 Andrew Grant 15114 895539 OssumOpossum 14857 1007129 LunaticBFF57 14525 1190310 +YELNAMRON 14480 1141420 enedene 14476 905279 -IslandLambda 14393 958196 +MooTheCow 14459 1023868 +BestBoyBerlin 14353 1365584 bpfliegel 14233 882523 -YELNAMRON 14230 1128094 mpx86 14019 759568 jpulman 13982 870599 getraideBFF 13871 1172846 +crocogoat 13817 1119086 Nesa92 13806 1116101 -crocogoat 13803 1117422 -joster 13710 946160 +joster 13717 946960 mbeier 13650 1044928 -Hjax 13535 915487 +Pablohn26 13552 1088532 +wxt9861 13550 1312306 +biniek 13469 930029 Dark_wizzie 13422 1007152 +Jackfish 13422 914984 +Hongildong 13297 699288 Rudolphous 13244 883140 +Phoenix17 13032 1124066 Machariel 13010 863104 -infinigon 12991 943216 mabichito 12903 749391 +FormazChar 12899 980413 thijsk 12886 722107 AdrianSA 12860 804972 -Flopzee 12698 894821 +szczur90 12720 979324 mschmidt 12644 863193 korposzczur 12606 838168 -tsim67 12570 890180 -Jackfish 12553 836958 fatmurphy 12547 853210 -Oakwen 12503 853105 +Oakwen 12537 856257 SapphireBrand 12416 969604 +Snuuka 12392 509082 deflectooor 12386 579392 modolief 12386 896470 -TataneSan 12358 609332 -Farseer 12249 694108 +ckaz 12273 754644 pgontarz 12151 848794 dbernier 12103 860824 -FormazChar 11989 907809 +rensonthemove 11999 971993 stocky 11954 699440 -somethingintheshadows 11940 989472 -MooTheCow 11892 776126 +ali-al-zhrani 11887 836126 3cho 11842 1036786 -whelanh 11557 245188 +Craftyawesome 11736 832254 +dragon123118 11578 1044142 +ImperiumAeternum 11482 979142 +lvdv 11475 594400 infinity 11470 727027 +kusihe 11468 468450 +vaskoul 11446 976902 aga 11412 695127 +Def9Infinity 11408 700682 torbjo 11395 729145 Thomas A. Anderson 11372 732094 savage84 11358 670860 d64 11263 789184 -ali-al-zhrani 11245 779246 -ckaz 11170 680866 +Poly 11172 455568 +enizor 11140 630194 snicolet 11106 869170 dapper 11032 771402 Ethnikoi 10993 945906 -Snuuka 10938 435504 -Karmatron 10859 678058 +Karmatron 10871 678306 +zarthus 10773 1034536 +OliverClarke 10696 942654 +Omed 10680 669816 +cyberthink 10647 936538 basepi 10637 744851 -jibarbosa 10628 857100 +michaelrpg 10624 748179 Cubox 10621 826448 -mecevdimitar 10609 787318 -michaelrpg 10509 739239 -Def9Infinity 10427 686978 +GBx3TV 10499 343266 +Styx 10450 867836 OIVAS7572 10420 995586 -wxt9861 10412 1013864 Garruk 10365 706465 dzjp 10343 732529 +Lorenz 10311 886308 +borinot 10026 902130 diff --git a/scripts/.gitattributes b/scripts/.gitattributes new file mode 100644 index 000000000..dfdb8b771 --- /dev/null +++ b/scripts/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/scripts/get_native_properties.sh b/scripts/get_native_properties.sh index fb124021a..67dd60ca5 100755 --- a/scripts/get_native_properties.sh +++ b/scripts/get_native_properties.sh @@ -26,12 +26,27 @@ check_znver_1_2() { [ "$vendor_id" = "AuthenticAMD" ] && [ "$cpu_family" = "23" ] && znver_1_2=true } +# Set the file CPU loongarch64 architecture +set_arch_loongarch64() { + if check_flags 'lasx'; then + true_arch='loongarch64-lasx' + elif check_flags 'lsx'; then + true_arch='loongarch64-lsx' + else + true_arch='loongarch64' + fi +} + # Set the file CPU x86_64 architecture set_arch_x86_64() { - if check_flags 'avx512vnni' 'avx512dq' 'avx512f' 'avx512bw' 'avx512vl'; then - true_arch='x86-64-vnni256' + if check_flags 'avx512f' 'avx512cd' 'avx512vl' 'avx512dq' 'avx512bw' 'avx512ifma' 'avx512vbmi' 'avx512vbmi2' 'avx512vpopcntdq' 'avx512bitalg' 'avx512vnni' 'vpclmulqdq' 'gfni' 'vaes'; then + true_arch='x86-64-avx512icl' + elif check_flags 'avx512vnni' 'avx512dq' 'avx512f' 'avx512bw' 'avx512vl'; then + true_arch='x86-64-vnni512' elif check_flags 'avx512f' 'avx512bw'; then true_arch='x86-64-avx512' + elif check_flags 'avxvnni'; then + true_arch='x86-64-avxvnni' elif [ -z "${znver_1_2+1}" ] && check_flags 'bmi2'; then true_arch='x86-64-bmi2' elif check_flags 'avx2'; then @@ -43,6 +58,20 @@ set_arch_x86_64() { fi } +set_arch_ppc_64() { + if grep -q -w "altivec" /proc/cpuinfo; then + power=$(grep -oP -m 1 'cpu\t+: POWER\K\d+' /proc/cpuinfo) + if [ "0$power" -gt 7 ]; then + # VSX started with POWER8 + true_arch='ppc-64-vsx' + else + true_arch='ppc-64-altivec' + fi + else + true_arch='ppc-64' + fi +} + # Check the system type uname_s=$(uname -s) uname_m=$(uname -m) @@ -51,12 +80,12 @@ case $uname_s in case $uname_m in 'arm64') true_arch='apple-silicon' - file_arch='x86-64-sse41-popcnt' # Supported by Rosetta 2 + file_arch='m1-apple-silicon' ;; 'x86_64') flags=$(sysctl -n machdep.cpu.features machdep.cpu.leaf7_features | tr '\n' ' ' | tr '[:upper:]' '[:lower:]' | tr -d '_.') set_arch_x86_64 - if [ "$true_arch" = 'x86-64-vnni256' ] || [ "$true_arch" = 'x86-64-avx512' ]; then + if [ "$true_arch" = 'x86-64-avx512' ]; then file_arch='x86-64-bmi2' fi ;; @@ -76,6 +105,10 @@ case $uname_s in file_os='ubuntu' true_arch='x86-32' ;; + 'ppc64'*) + file_os='ubuntu' + set_arch_ppc_64 + ;; 'aarch64') file_os='android' true_arch='armv8' @@ -90,6 +123,10 @@ case $uname_s in true_arch="$true_arch-neon" fi ;; + 'loongarch64'*) + file_os='linux' + set_arch_loongarch64 + ;; *) # Unsupported machine type, exit with error printf 'Unsupported machine type: %s\n' "$uname_m" exit 1 @@ -97,7 +134,13 @@ case $uname_s in esac file_ext='tar' ;; - 'CYGWIN'*|'MINGW'*|'MSYS'*) # Windows system with POSIX compatibility layer + 'MINGW'*'ARM64'*) # Windows ARM64 system with POSIX compatibility layer + # TODO: older chips might be armv8, but we have no good way to detect, /proc/cpuinfo shows x86 info + file_os='windows' + true_arch='armv8-dotprod' + file_ext='zip' + ;; + 'CYGWIN'*|'MINGW'*|'MSYS'*) # Windows x86_64system with POSIX compatibility layer get_flags check_znver_1_2 set_arch_x86_64 diff --git a/scripts/net.sh b/scripts/net.sh new file mode 100755 index 000000000..1aa1fbfb1 --- /dev/null +++ b/scripts/net.sh @@ -0,0 +1,76 @@ +#!/bin/sh + +wget_or_curl=$( (command -v wget > /dev/null 2>&1 && echo "wget -qO-") || \ + (command -v curl > /dev/null 2>&1 && echo "curl -skL")) + + +sha256sum=$( (command -v shasum > /dev/null 2>&1 && echo "shasum -a 256") || \ + (command -v sha256sum > /dev/null 2>&1 && echo "sha256sum")) + +if [ -z "$sha256sum" ]; then + >&2 echo "sha256sum not found, NNUE files will be assumed valid." +fi + +get_nnue_filename() { + grep "$1" evaluate.h | grep "#define" | sed "s/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/" +} + +validate_network() { + # If no sha256sum command is available, assume the file is always valid. + if [ -n "$sha256sum" ] && [ -f "$1" ]; then + if [ "$1" != "nn-$($sha256sum "$1" | cut -c 1-12).nnue" ]; then + rm -f "$1" + return 1 + fi + fi +} + +fetch_network() { + _filename="$(get_nnue_filename "$1")" + + if [ -z "$_filename" ]; then + >&2 echo "NNUE file name not found for: $1" + return 1 + fi + + if [ -f "$_filename" ]; then + if validate_network "$_filename"; then + echo "Existing $_filename validated, skipping download" + return + else + echo "Removing invalid NNUE file: $_filename" + fi + fi + + if [ -z "$wget_or_curl" ]; then + >&2 printf "%s\n" "Neither wget or curl is installed." \ + "Install one of these tools to download NNUE files automatically." + exit 1 + fi + + for url in \ + "https://tests.stockfishchess.org/api/nn/$_filename" \ + "https://github.com/official-stockfish/networks/raw/master/$_filename"; do + echo "Downloading from $url ..." + if $wget_or_curl "$url" > "$_filename"; then + if validate_network "$_filename"; then + echo "Successfully validated $_filename" + else + echo "Downloaded $_filename is invalid" + continue + fi + else + echo "Failed to download from $url" + fi + if [ -f "$_filename" ]; then + return + fi + done + + # Download was not successful in the loop, return false. + >&2 echo "Failed to download $_filename" + return 1 +} + +fetch_network EvalFileDefaultNameBig && \ +fetch_network EvalFileDefaultNameSmall diff --git a/src/Makefile b/src/Makefile index fe8bfc8ef..a40612888 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,5 +1,5 @@ # Stockfish, a UCI chess playing engine derived from Glaurung 2.1 -# Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) +# 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 @@ -25,6 +25,21 @@ ifeq ($(KERNEL),Linux) OS := $(shell uname -o) endif +### Command prefix to run the built executable (e.g. wine, sde, qemu) +### Backward compatible alias: WINE_PATH (deprecated) +ifneq ($(strip $(WINE_PATH)),) +ifeq ($(strip $(RUN_PREFIX)),) +RUN_PREFIX := $(WINE_PATH) +endif +ifeq ($(MAKELEVEL),0) +ifneq ($(strip $(RUN_PREFIX)),$(strip $(WINE_PATH))) +$(warning *** Both RUN_PREFIX and WINE_PATH are set; ignoring WINE_PATH. ***) +else +$(warning *** WINE_PATH is deprecated; use RUN_PREFIX instead. ***) +endif +endif +endif + ### Target Windows OS ifeq ($(OS),Windows_NT) ifneq ($(COMP),ndk) @@ -32,8 +47,8 @@ ifeq ($(OS),Windows_NT) endif else ifeq ($(COMP),mingw) target_windows = yes - ifeq ($(WINE_PATH),) - WINE_PATH := $(shell which wine) + ifeq ($(RUN_PREFIX),) + RUN_PREFIX := $(shell which wine) endif endif @@ -49,21 +64,24 @@ PREFIX = /usr/local BINDIR = $(PREFIX)/bin ### Built-in benchmark for pgo-builds -PGOBENCH = $(WINE_PATH) ./$(EXE) bench +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 \ search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp \ - nnue/nnue_misc.cpp nnue/features/half_ka_v2_hm.cpp nnue/network.cpp + nnue/nnue_accumulator.cpp nnue/nnue_misc.cpp nnue/network.cpp \ + nnue/features/half_ka_v2_hm.cpp nnue/features/full_threats.cpp \ + engine.cpp score.cpp memory.cpp -HEADERS = benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h \ - nnue/nnue_misc.h nnue/features/half_ka_v2_hm.h nnue/layers/affine_transform.h \ - nnue/layers/affine_transform_sparse_input.h nnue/layers/clipped_relu.h nnue/layers/simd.h \ - nnue/layers/sqr_clipped_relu.h nnue/nnue_accumulator.h nnue/nnue_architecture.h \ - nnue/nnue_common.h nnue/nnue_feature_transformer.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 cluster.h +HEADERS = benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h history.h \ + nnue/nnue_misc.h nnue/features/half_ka_v2_hm.h nnue/features/full_threats.h \ + nnue/layers/affine_transform.h nnue/layers/affine_transform_sparse_input.h \ + 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 OBJS = $(notdir $(SRCS:.cpp=.o)) @@ -96,10 +114,14 @@ VPATH = syzygy:nnue:nnue/features # avx2 = yes/no --- -mavx2 --- Use Intel Advanced Vector Extensions 2 # avxvnni = yes/no --- -mavxvnni --- Use Intel Vector Neural Network Instructions AVX # avx512 = yes/no --- -mavx512bw --- Use Intel Advanced Vector Extensions 512 -# vnni256 = yes/no --- -mavx256vnni --- Use Intel Vector Neural Network Instructions 512 with 256bit operands # vnni512 = yes/no --- -mavx512vnni --- Use Intel Vector Neural Network Instructions 512 +# avx512icl = yes/no --- ... multiple ... --- Use All AVX-512 features available on both Intel Ice Lake and AMD Zen 4 +# altivec = yes/no --- -maltivec --- Use PowerPC Altivec SIMD extension +# vsx = yes/no --- -mvsx --- Use POWER VSX SIMD extension # neon = yes/no --- -DUSE_NEON --- Use ARM SIMD architecture # 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 @@ -121,12 +143,13 @@ ifeq ($(ARCH), native) endif # explicitly check for the list of supported architectures (as listed with make help), -# the user can override with `make ARCH=x86-32-vnni256 SUPPORTED_ARCH=true` +# the user can override with `make ARCH=x86-64-avx512icl SUPPORTED_ARCH=true` ifeq ($(ARCH), $(filter $(ARCH), \ - x86-64-vnni512 x86-64-vnni256 x86-64-avx512 x86-64-avxvnni x86-64-bmi2 \ - x86-64-avx2 x86-64-sse41-popcnt x86-64-modern x86-64-ssse3 x86-64-sse3-popcnt \ - x86-64 x86-32-sse41-popcnt x86-32-sse2 x86-32 ppc-64 ppc-32 e2k \ - armv7 armv7-neon armv8 armv8-dotprod apple-silicon general-64 general-32 riscv64 loongarch64)) + x86-64-avx512icl x86-64-vnni512 x86-64-avx512 x86-64-avxvnni \ + x86-64-bmi2 x86-64-avx2 x86-64-sse41-popcnt x86-64-modern x86-64-ssse3 x86-64-sse3-popcnt \ + x86-64 x86-32-sse41-popcnt x86-32-sse2 x86-32 ppc-64 ppc-64-altivec ppc-64-vsx ppc-32 e2k \ + armv7 armv7-neon armv8 armv8-dotprod apple-silicon general-64 general-32 riscv64 \ + loongarch64 loongarch64-lsx loongarch64-lasx)) SUPPORTED_ARCH=true else SUPPORTED_ARCH=false @@ -147,16 +170,20 @@ sse41 = no avx2 = no avxvnni = no avx512 = no -vnni256 = no vnni512 = no +avx512icl = no +altivec = no +vsx = no neon = no mpi = no dotprod = no arm_version = 0 +lsx = no +lasx = no STRIP = strip -ifneq ($(shell which clang-format-17 2> /dev/null),) - CLANG-FORMAT = clang-format-17 +ifneq ($(shell which clang-format-20 2> /dev/null),) + CLANG-FORMAT = clang-format-20 else CLANG-FORMAT = clang-format endif @@ -259,17 +286,6 @@ ifeq ($(findstring -avx512,$(ARCH)),-avx512) avx512 = yes endif -ifeq ($(findstring -vnni256,$(ARCH)),-vnni256) - popcnt = yes - sse = yes - sse2 = yes - ssse3 = yes - sse41 = yes - avx2 = yes - pext = yes - vnni256 = yes -endif - ifeq ($(findstring -vnni512,$(ARCH)),-vnni512) popcnt = yes sse = yes @@ -282,6 +298,19 @@ ifeq ($(findstring -vnni512,$(ARCH)),-vnni512) vnni512 = yes endif +ifeq ($(findstring -avx512icl,$(ARCH)),-avx512icl) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + pext = yes + avx512 = yes + vnni512 = yes + avx512icl = yes +endif + ifeq ($(sse),yes) prefetch = yes endif @@ -357,6 +386,20 @@ ifeq ($(ARCH),ppc-64) prefetch = yes endif +ifeq ($(ARCH),ppc-64-altivec) + arch = ppc64 + popcnt = yes + prefetch = yes + altivec = yes +endif + +ifeq ($(ARCH),ppc-64-vsx) + arch = ppc64 + popcnt = yes + prefetch = yes + vsx = yes +endif + ifeq ($(findstring e2k,$(ARCH)),e2k) arch = e2k mmx = yes @@ -372,8 +415,19 @@ ifeq ($(ARCH),riscv64) arch = riscv64 endif -ifeq ($(ARCH),loongarch64) +ifeq ($(findstring loongarch64,$(ARCH)),loongarch64) arch = loongarch64 + prefetch = yes + +ifeq ($(findstring -lasx,$(ARCH)),-lasx) + lsx = yes + lasx = yes +endif + +ifeq ($(findstring -lsx,$(ARCH)),-lsx) + lsx = yes +endif + endif endif @@ -410,7 +464,7 @@ ifeq ($(COMP),gcc) ifeq ($(ARCH),riscv64) CXXFLAGS += -latomic endif - else ifeq ($(ARCH),loongarch64) + else ifeq ($(arch),loongarch64) CXXFLAGS += -latomic else CXXFLAGS += -m$(bits) @@ -482,7 +536,7 @@ ifeq ($(COMP),clang) ifeq ($(ARCH),riscv64) CXXFLAGS += -latomic endif - else ifeq ($(ARCH),loongarch64) + else ifeq ($(arch),loongarch64) CXXFLAGS += -latomic else CXXFLAGS += -m$(bits) @@ -491,8 +545,8 @@ ifeq ($(COMP),clang) endif ifeq ($(KERNEL),Darwin) - CXXFLAGS += -mmacosx-version-min=10.14 - LDFLAGS += -mmacosx-version-min=10.14 + CXXFLAGS += -mmacosx-version-min=10.15 + LDFLAGS += -mmacosx-version-min=10.15 ifneq ($(arch),any) CXXFLAGS += -arch $(arch) LDFLAGS += -arch $(arch) @@ -500,14 +554,12 @@ ifeq ($(KERNEL),Darwin) XCRUN = xcrun endif -# To cross-compile for Android, NDK version r21 or later is recommended. -# In earlier NDK versions, you'll need to pass -fno-addrsig if using GNU binutils. -# Currently we don't know how to make PGO builds with the NDK yet. +# To cross-compile for Android, use NDK version r27c or later. ifeq ($(COMP),ndk) - CXXFLAGS += -stdlib=libc++ -fPIE + CXXFLAGS += -stdlib=libc++ comp=clang ifeq ($(arch),armv7) - CXX=armv7a-linux-androideabi16-clang++ + CXX=armv7a-linux-androideabi29-clang++ CXXFLAGS += -mthumb -march=armv7-a -mfloat-abi=softfp -mfpu=neon ifneq ($(shell which arm-linux-androideabi-strip 2>/dev/null),) STRIP=arm-linux-androideabi-strip @@ -516,7 +568,7 @@ ifeq ($(COMP),ndk) endif endif ifeq ($(arch),armv8) - CXX=aarch64-linux-android21-clang++ + CXX=aarch64-linux-android29-clang++ ifneq ($(shell which aarch64-linux-android-strip 2>/dev/null),) STRIP=aarch64-linux-android-strip else @@ -524,14 +576,32 @@ ifeq ($(COMP),ndk) endif endif ifeq ($(arch),x86_64) - CXX=x86_64-linux-android21-clang++ + CXX=x86_64-linux-android29-clang++ ifneq ($(shell which x86_64-linux-android-strip 2>/dev/null),) STRIP=x86_64-linux-android-strip else STRIP=llvm-strip endif endif - LDFLAGS += -static-libstdc++ -pie -lm -latomic + LDFLAGS += -static-libstdc++ +endif + +### Allow overwriting CXX from command line +ifdef COMPCXX + CXX=$(COMPCXX) +endif + +# llvm-profdata must be version compatible with the specified CXX (be it clang, or the gcc alias) +# make -j profile-build CXX=clang++-20 COMP=clang +# Locate the version in the same directory as the compiler used, +# with fallback to a generic one if it can't be located + LLVM_PROFDATA := $(dir $(realpath $(shell which $(CXX) 2> /dev/null)))llvm-profdata +# for icx +ifeq ($(wildcard $(LLVM_PROFDATA)),) + LLVM_PROFDATA := $(dir $(realpath $(shell which $(CXX) 2> /dev/null)))/compiler/llvm-profdata +endif +ifeq ($(wildcard $(LLVM_PROFDATA)),) + LLVM_PROFDATA := llvm-profdata endif ifeq ($(comp),icx) @@ -548,16 +618,6 @@ else endif endif -### Travis CI script uses COMPILER to overwrite CXX -ifdef COMPILER - COMPCXX=$(COMPILER) -endif - -### Allow overwriting CXX from command line -ifdef COMPCXX - CXX=$(COMPCXX) -endif - ### Sometimes gcc is really clang ifeq ($(COMP),gcc) gccversion := $(shell $(CXX) --version 2>/dev/null) @@ -565,6 +625,8 @@ ifeq ($(COMP),gcc) ifneq ($(gccisclang),) profile_make = clang-profile-make profile_use = clang-profile-use + else + CXXFLAGS += -Wstack-usage=128000 endif endif @@ -577,6 +639,19 @@ ifneq ($(comp),mingw) ifneq ($(KERNEL),Haiku) ifneq ($(COMP),ndk) LDFLAGS += -lpthread + + add_lrt = yes + ifeq ($(target_windows),yes) + add_lrt = no + endif + + ifeq ($(KERNEL),Darwin) + add_lrt = no + endif + + ifeq ($(add_lrt),yes) + LDFLAGS += -lrt + endif endif endif endif @@ -587,6 +662,7 @@ ifeq ($(debug),no) CXXFLAGS += -DNDEBUG else CXXFLAGS += -g + CXXFLAGS += -D_GLIBCXX_ASSERTIONS -D_GLIBCXX_DEBUG endif ### 3.2.2 Debugging with undefined behavior sanitizers @@ -641,7 +717,7 @@ else endif ifeq ($(popcnt),yes) - ifeq ($(arch),$(filter $(arch),ppc64 armv7 armv8 arm64)) + ifeq ($(arch),$(filter $(arch),ppc64 ppc64-altivec ppc64-vsx armv7 armv8 arm64)) CXXFLAGS += -DUSE_POPCNT else CXXFLAGS += -msse3 -mpopcnt -DUSE_POPCNT @@ -666,21 +742,21 @@ endif ifeq ($(avx512),yes) CXXFLAGS += -DUSE_AVX512 ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) - CXXFLAGS += -mavx512f -mavx512bw - endif -endif - -ifeq ($(vnni256),yes) - CXXFLAGS += -DUSE_VNNI - ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) - CXXFLAGS += -mavx512f -mavx512bw -mavx512vnni -mavx512dq -mavx512vl -mprefer-vector-width=256 + CXXFLAGS += -mavx512f -mavx512bw -mavx512dq -mavx512vl endif endif ifeq ($(vnni512),yes) CXXFLAGS += -DUSE_VNNI ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) - CXXFLAGS += -mavx512f -mavx512bw -mavx512vnni -mavx512dq -mavx512vl -mprefer-vector-width=512 + CXXFLAGS += -mavx512f -mavx512bw -mavx512vnni -mavx512dq -mavx512vl + endif +endif + +ifeq ($(avx512icl),yes) + CXXFLAGS += -DUSE_AVX512 -DUSE_VNNI -DUSE_AVX512ICL + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mavx512f -mavx512cd -mavx512vl -mavx512dq -mavx512bw -mavx512ifma -mavx512vbmi -mavx512vbmi2 -mavx512vpopcntdq -mavx512bitalg -mavx512vnni -mvpclmulqdq -mgfni -mvaes endif endif @@ -711,6 +787,20 @@ ifeq ($(mmx),yes) endif endif +ifeq ($(altivec),yes) + CXXFLAGS += -maltivec + ifeq ($(COMP),gcc) + CXXFLAGS += -mabi=altivec + endif +endif + +ifeq ($(vsx),yes) + CXXFLAGS += -mvsx + ifeq ($(COMP),gcc) + CXXFLAGS += -DNO_WARN_X86_INTRINSICS -DUSE_SSE2 + endif +endif + ifeq ($(neon),yes) CXXFLAGS += -DUSE_NEON=$(arm_version) ifeq ($(KERNEL),Linux) @@ -726,6 +816,18 @@ ifeq ($(dotprod),yes) CXXFLAGS += -march=armv8.2-a+dotprod -DUSE_NEON_DOTPROD endif +ifeq ($(lasx),yes) + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mlasx + endif +endif + +ifeq ($(lsx),yes) + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mlsx + endif +endif + ### 3.7 pext ifeq ($(pext),yes) CXXFLAGS += -DUSE_PEXT @@ -807,71 +909,75 @@ endif ### ========================================================================== help: - @echo "" - @echo "To compile stockfish, type: " - @echo "" - @echo "make -j target [ARCH=arch] [COMP=compiler] [COMPCXX=cxx]" - @echo "" - @echo "Supported targets:" - @echo "" - @echo "help > Display architecture details" - @echo "profile-build > standard build with profile-guided optimization" - @echo "build > skip profile-guided optimization" - @echo "net > Download the default nnue nets" - @echo "strip > Strip executable" - @echo "install > Install executable" - @echo "clean > Clean up" - @echo "" - @echo "Supported archs:" - @echo "" - @echo "native > select the best architecture for the host processor (default)" - @echo "x86-64-vnni512 > x86 64-bit with vnni 512bit support" - @echo "x86-64-vnni256 > x86 64-bit with vnni 512bit support, limit operands to 256bit wide" - @echo "x86-64-avx512 > x86 64-bit with avx512 support" - @echo "x86-64-avxvnni > x86 64-bit with vnni 256bit support" - @echo "x86-64-bmi2 > x86 64-bit with bmi2 support" - @echo "x86-64-avx2 > x86 64-bit with avx2 support" - @echo "x86-64-sse41-popcnt > x86 64-bit with sse41 and popcnt support" - @echo "x86-64-modern > deprecated, currently x86-64-sse41-popcnt" - @echo "x86-64-ssse3 > x86 64-bit with ssse3 support" - @echo "x86-64-sse3-popcnt > x86 64-bit with sse3 compile and popcnt support" - @echo "x86-64 > x86 64-bit generic (with sse2 support)" - @echo "x86-32-sse41-popcnt > x86 32-bit with sse41 and popcnt support" - @echo "x86-32-sse2 > x86 32-bit with sse2 support" - @echo "x86-32 > x86 32-bit generic (with mmx compile support)" - @echo "ppc-64 > PPC 64-bit" - @echo "ppc-32 > PPC 32-bit" - @echo "armv7 > ARMv7 32-bit" - @echo "armv7-neon > ARMv7 32-bit with popcnt and neon" - @echo "armv8 > ARMv8 64-bit with popcnt and neon" - @echo "armv8-dotprod > ARMv8 64-bit with popcnt, neon and dot product support" - @echo "e2k > Elbrus 2000" - @echo "apple-silicon > Apple silicon ARM64" - @echo "general-64 > unspecified 64-bit" - @echo "general-32 > unspecified 32-bit" - @echo "riscv64 > RISC-V 64-bit" - @echo "loongarch64 > LoongArch 64-bit" - @echo "" - @echo "Supported compilers:" - @echo "" - @echo "gcc > GNU compiler (default)" - @echo "mingw > GNU compiler with MinGW under Windows" - @echo "clang > LLVM Clang compiler" - @echo "icx > Intel oneAPI DPC++/C++ Compiler" - @echo "ndk > Google NDK to cross-compile for Android" - @echo "" - @echo "Simple examples. If you don't know what to do, you likely want to run one of: " - @echo "" - @echo "make -j profile-build ARCH=x86-64-avx2 # typically a fast compile for common systems " - @echo "make -j profile-build ARCH=x86-64-sse41-popcnt # A more portable compile for 64-bit systems " - @echo "make -j profile-build ARCH=x86-64 # A portable compile for 64-bit systems " - @echo "" - @echo "Advanced examples, for experienced users: " - @echo "" - @echo "make -j profile-build ARCH=x86-64-avxvnni" - @echo "make -j profile-build ARCH=x86-64-avxvnni COMP=gcc COMPCXX=g++-12.0" - @echo "make -j build ARCH=x86-64-ssse3 COMP=clang" - @echo "" + @echo "" && \ + echo "To compile stockfish, type: " && \ + echo "" && \ + echo "make -j target [ARCH=arch] [COMP=compiler] [COMPCXX=cxx]" && \ + echo "" && \ + echo "Supported targets:" && \ + echo "" && \ + echo "help > Display architecture details" && \ + echo "profile-build > standard build with profile-guided optimization" && \ + echo "build > skip profile-guided optimization" && \ + echo "net > Download the default nnue nets" && \ + echo "strip > Strip executable" && \ + echo "install > Install executable" && \ + echo "clean > Clean up" && \ + echo "" && \ + echo "Supported archs:" && \ + echo "" && \ + echo "native > select the best architecture for the host processor (default)" && \ + echo "x86-64-avx512icl > x86 64-bit with minimum avx512 support of Intel Ice Lake or AMD Zen 4" && \ + echo "x86-64-vnni512 > x86 64-bit with vnni 512bit support" && \ + echo "x86-64-avx512 > x86 64-bit with avx512 support" && \ + echo "x86-64-avxvnni > x86 64-bit with vnni 256bit support" && \ + echo "x86-64-bmi2 > x86 64-bit with bmi2 support" && \ + echo "x86-64-avx2 > x86 64-bit with avx2 support" && \ + echo "x86-64-sse41-popcnt > x86 64-bit with sse41 and popcnt support" && \ + echo "x86-64-modern > deprecated, currently x86-64-sse41-popcnt" && \ + echo "x86-64-ssse3 > x86 64-bit with ssse3 support" && \ + echo "x86-64-sse3-popcnt > x86 64-bit with sse3 compile and popcnt support" && \ + echo "x86-64 > x86 64-bit generic (with sse2 support)" && \ + echo "x86-32-sse41-popcnt > x86 32-bit with sse41 and popcnt support" && \ + echo "x86-32-sse2 > x86 32-bit with sse2 support" && \ + echo "x86-32 > x86 32-bit generic (with mmx compile support)" && \ + echo "ppc-64 > PPC 64-bit" && \ + echo "ppc-64-altivec > PPC 64-bit with altivec support" && \ + echo "ppc-64-vsx > PPC 64-bit with vsx support" && \ + echo "ppc-32 > PPC 32-bit" && \ + echo "armv7 > ARMv7 32-bit" && \ + echo "armv7-neon > ARMv7 32-bit with popcnt and neon" && \ + echo "armv8 > ARMv8 64-bit with popcnt and neon" && \ + echo "armv8-dotprod > ARMv8 64-bit with popcnt, neon and dot product support" && \ + echo "e2k > Elbrus 2000" && \ + echo "apple-silicon > Apple silicon ARM64" && \ + echo "general-64 > unspecified 64-bit" && \ + echo "general-32 > unspecified 32-bit" && \ + echo "riscv64 > RISC-V 64-bit" && \ + echo "loongarch64 > LoongArch 64-bit" && \ + echo "loongarch64-lsx > LoongArch 64-bit with SIMD eXtension" && \ + echo "loongarch64-lasx > LoongArch 64-bit with Advanced SIMD eXtension" && \ + echo "" && \ + echo "Supported compilers:" && \ + echo "" && \ + echo "gcc > GNU compiler (default)" && \ + echo "mingw > GNU compiler with MinGW under Windows" && \ + echo "clang > LLVM Clang compiler" && \ + echo "icx > Intel oneAPI DPC++/C++ Compiler" && \ + echo "ndk > Google NDK to cross-compile for Android" && \ + echo "" && \ + echo "Simple examples. If you don't know what to do, you likely want to run one of: " && \ + echo "" && \ + echo "make -j profile-build ARCH=x86-64-avx2 # typically a fast compile for common systems " && \ + echo "make -j profile-build ARCH=x86-64-sse41-popcnt # A more portable compile for 64-bit systems " && \ + echo "make -j profile-build ARCH=x86-64 # A portable compile for 64-bit systems " && \ + echo "" && \ + echo "Advanced examples, for experienced users: " && \ + echo "" && \ + echo "make -j profile-build ARCH=x86-64-avxvnni" && \ + echo "make -j profile-build ARCH=x86-64-avxvnni COMP=gcc COMPCXX=g++-12.0" && \ + echo "make -j build ARCH=x86-64-ssse3 COMP=clang" && \ + echo "" ifneq ($(SUPPORTED_ARCH), true) @echo "Specify a supported architecture with the ARCH option for more details" @echo "" @@ -933,67 +1039,13 @@ profileclean: @rm -f stockfish.res @rm -f ./-lstdc++.res -define fetch_network - @echo "Default net: $(nnuenet)" - @if [ "x$(curl_or_wget)" = "x" ]; then \ - echo "Neither curl nor wget is installed. Install one of these tools unless the net has been downloaded manually"; \ - fi - @if [ "x$(shasum_command)" = "x" ]; then \ - echo "shasum / sha256sum not found, skipping net validation"; \ - elif test -f "$(nnuenet)"; then \ - if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ - echo "Removing invalid network"; rm -f $(nnuenet); \ - fi; \ - fi; - @for nnuedownloadurl in "$(nnuedownloadurl1)" "$(nnuedownloadurl2)"; do \ - if test -f "$(nnuenet)"; then \ - echo "$(nnuenet) available : OK"; break; \ - else \ - if [ "x$(curl_or_wget)" != "x" ]; then \ - echo "Downloading $${nnuedownloadurl}"; $(curl_or_wget) $${nnuedownloadurl} > $(nnuenet);\ - else \ - echo "No net found and download not possible"; exit 1;\ - fi; \ - fi; \ - if [ "x$(shasum_command)" != "x" ]; then \ - if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ - echo "Removing failed download"; rm -f $(nnuenet); \ - fi; \ - fi; \ - done - @if ! test -f "$(nnuenet)"; then \ - echo "Failed to download $(nnuenet)."; \ - fi; - @if [ "x$(shasum_command)" != "x" ]; then \ - if [ "$(nnuenet)" = "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ - echo "Network validated"; break; \ - fi; \ - fi; -endef - -# set up shell variables for the net stuff -define netvariables -$(eval nnuenet := $(shell grep $(1) evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/')) -$(eval nnuedownloadurl1 := https://tests.stockfishchess.org/api/nn/$(nnuenet)) -$(eval nnuedownloadurl2 := https://github.com/official-stockfish/networks/raw/master/$(nnuenet)) -$(eval curl_or_wget := $(shell if hash curl 2>/dev/null; then echo "curl -skL"; elif hash wget 2>/dev/null; then echo "wget -qO-"; fi)) -$(eval shasum_command := $(shell if hash shasum 2>/dev/null; then echo "shasum -a 256 "; elif hash sha256sum 2>/dev/null; then echo "sha256sum "; fi)) -endef - # evaluation network (nnue) net: - $(call netvariables, EvalFileDefaultNameBig) - $(call fetch_network) - $(call netvariables, EvalFileDefaultNameSmall) - $(call fetch_network) + @$(SHELL) ../scripts/net.sh format: $(CLANG-FORMAT) -i $(SRCS) $(HEADERS) -style=file -# default target -default: - help - ### ========================================================================== ### Section 5. Private Targets ### ========================================================================== @@ -1002,62 +1054,72 @@ all: $(EXE) .depend config-sanity: net @echo "" - @echo "Config:" - @echo "debug: '$(debug)'" - @echo "sanitize: '$(sanitize)'" - @echo "optimize: '$(optimize)'" - @echo "arch: '$(arch)'" - @echo "bits: '$(bits)'" - @echo "kernel: '$(KERNEL)'" - @echo "os: '$(OS)'" - @echo "prefetch: '$(prefetch)'" - @echo "popcnt: '$(popcnt)'" - @echo "pext: '$(pext)'" - @echo "sse: '$(sse)'" - @echo "mmx: '$(mmx)'" - @echo "sse2: '$(sse2)'" - @echo "ssse3: '$(ssse3)'" - @echo "sse41: '$(sse41)'" - @echo "avx2: '$(avx2)'" - @echo "avxvnni: '$(avxvnni)'" - @echo "avx512: '$(avx512)'" - @echo "vnni256: '$(vnni256)'" - @echo "vnni512: '$(vnni512)'" - @echo "neon: '$(neon)'" - @echo "mpi: '$(mpi)'" - @echo "dotprod: '$(dotprod)'" - @echo "arm_version: '$(arm_version)'" - @echo "target_windows: '$(target_windows)'" - @echo "" - @echo "Flags:" - @echo "CXX: $(CXX)" - @echo "CXXFLAGS: $(CXXFLAGS)" - @echo "LDFLAGS: $(LDFLAGS)" - @echo "" - @echo "Testing config sanity. If this fails, try 'make help' ..." - @echo "" - @test "$(debug)" = "yes" || test "$(debug)" = "no" - @test "$(optimize)" = "yes" || test "$(optimize)" = "no" - @test "$(SUPPORTED_ARCH)" = "true" - @test "$(arch)" = "any" || test "$(arch)" = "x86_64" || test "$(arch)" = "i386" || \ + @echo "Config:" && \ + echo "debug: '$(debug)'" && \ + echo "sanitize: '$(sanitize)'" && \ + echo "optimize: '$(optimize)'" && \ + echo "arch: '$(arch)'" && \ + echo "bits: '$(bits)'" && \ + echo "kernel: '$(KERNEL)'" && \ + echo "os: '$(OS)'" && \ + echo "prefetch: '$(prefetch)'" && \ + echo "popcnt: '$(popcnt)'" && \ + echo "pext: '$(pext)'" && \ + echo "sse: '$(sse)'" && \ + echo "mmx: '$(mmx)'" && \ + echo "sse2: '$(sse2)'" && \ + echo "ssse3: '$(ssse3)'" && \ + echo "sse41: '$(sse41)'" && \ + echo "avx2: '$(avx2)'" && \ + echo "avxvnni: '$(avxvnni)'" && \ + echo "avx512: '$(avx512)'" && \ + echo "vnni512: '$(vnni512)'" && \ + echo "avx512icl: '$(avx512icl)'" && \ + echo "altivec: '$(altivec)'" && \ + echo "vsx: '$(vsx)'" && \ + echo "neon: '$(neon)'" && \ + echo "mpi: '$(mpi)'" && \ + echo "dotprod: '$(dotprod)'" && \ + echo "arm_version: '$(arm_version)'" && \ + echo "lsx: '$(lsx)'" && \ + echo "lasx: '$(lasx)'" && \ + echo "target_windows: '$(target_windows)'" && \ + echo "" && \ + echo "Flags:" && \ + echo "CXX: $(CXX)" && \ + echo "CXXFLAGS: $(CXXFLAGS)" && \ + echo "LDFLAGS: $(LDFLAGS)" && \ + echo "" && \ + echo "Testing config sanity. If this fails, try 'make help' ..." && \ + echo "" && \ + (test "$(debug)" = "yes" || test "$(debug)" = "no") && \ + (test "$(optimize)" = "yes" || test "$(optimize)" = "no") && \ + (test "$(SUPPORTED_ARCH)" = "true") && \ + (test "$(arch)" = "any" || test "$(arch)" = "x86_64" || test "$(arch)" = "i386" || \ test "$(arch)" = "ppc64" || test "$(arch)" = "ppc" || test "$(arch)" = "e2k" || \ - test "$(arch)" = "armv7" || test "$(arch)" = "armv8" || test "$(arch)" = "arm64" || test "$(arch)" = "riscv64" || test "$(arch)" = "loongarch64" - @test "$(bits)" = "32" || test "$(bits)" = "64" - @test "$(prefetch)" = "yes" || test "$(prefetch)" = "no" - @test "$(popcnt)" = "yes" || test "$(popcnt)" = "no" - @test "$(pext)" = "yes" || test "$(pext)" = "no" - @test "$(sse)" = "yes" || test "$(sse)" = "no" - @test "$(mmx)" = "yes" || test "$(mmx)" = "no" - @test "$(sse2)" = "yes" || test "$(sse2)" = "no" - @test "$(ssse3)" = "yes" || test "$(ssse3)" = "no" - @test "$(sse41)" = "yes" || test "$(sse41)" = "no" - @test "$(avx2)" = "yes" || test "$(avx2)" = "no" - @test "$(avx512)" = "yes" || test "$(avx512)" = "no" - @test "$(vnni256)" = "yes" || test "$(vnni256)" = "no" - @test "$(vnni512)" = "yes" || test "$(vnni512)" = "no" - @test "$(neon)" = "yes" || test "$(neon)" = "no" - @test "$(comp)" = "gcc" || test "$(comp)" = "icx" || test "$(comp)" = "mingw" || test "$(comp)" = "clang" \ - || test "$(comp)" = "armv7a-linux-androideabi16-clang" || test "$(comp)" = "aarch64-linux-android21-clang" + test "$(arch)" = "armv7" || test "$(arch)" = "armv8" || test "$(arch)" = "arm64" || \ + test "$(arch)" = "riscv64" || test "$(arch)" = "loongarch64") && \ + (test "$(bits)" = "32" || test "$(bits)" = "64") && \ + (test "$(prefetch)" = "yes" || test "$(prefetch)" = "no") && \ + (test "$(popcnt)" = "yes" || test "$(popcnt)" = "no") && \ + (test "$(pext)" = "yes" || test "$(pext)" = "no") && \ + (test "$(sse)" = "yes" || test "$(sse)" = "no") && \ + (test "$(mmx)" = "yes" || test "$(mmx)" = "no") && \ + (test "$(sse2)" = "yes" || test "$(sse2)" = "no") && \ + (test "$(ssse3)" = "yes" || test "$(ssse3)" = "no") && \ + (test "$(sse41)" = "yes" || test "$(sse41)" = "no") && \ + (test "$(avx2)" = "yes" || test "$(avx2)" = "no") && \ + (test "$(avx512)" = "yes" || test "$(avx512)" = "no") && \ + (test "$(vnni512)" = "yes" || test "$(vnni512)" = "no") && \ + (test "$(avx512icl)" = "yes" || test "$(avx512icl)" = "no") && \ + (test "$(altivec)" = "yes" || test "$(altivec)" = "no") && \ + (test "$(vsx)" = "yes" || test "$(vsx)" = "no") && \ + (test "$(neon)" = "yes" || test "$(neon)" = "no") && \ + (test "$(lsx)" = "yes" || test "$(lsx)" = "no") && \ + (test "$(lasx)" = "yes" || test "$(lasx)" = "no") && \ + (test "$(comp)" = "gcc" || test "$(comp)" = "icx" || test "$(comp)" = "mingw" || \ + test "$(comp)" = "clang" || test "$(comp)" = "armv7a-linux-androideabi16-clang" || \ + test "$(comp)" = "aarch64-linux-android21-clang") $(EXE): $(OBJS) +$(CXX) -o $@ $(OBJS) $(LDFLAGS) @@ -1068,14 +1130,14 @@ FORCE: clang-profile-make: $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ - EXTRACXXFLAGS='-fprofile-instr-generate ' \ - EXTRALDFLAGS=' -fprofile-instr-generate' \ + EXTRACXXFLAGS='-fprofile-generate ' \ + EXTRALDFLAGS=' -fprofile-generate' \ all clang-profile-use: - $(XCRUN) llvm-profdata merge -output=stockfish.profdata *.profraw + $(XCRUN) $(LLVM_PROFDATA) merge -output=stockfish.profdata *.profraw $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ - EXTRACXXFLAGS='-fprofile-instr-use=stockfish.profdata' \ + EXTRACXXFLAGS='-fprofile-use=stockfish.profdata' \ EXTRALDFLAGS='-fprofile-use ' \ all @@ -1101,7 +1163,7 @@ icx-profile-make: all icx-profile-use: - $(XCRUN) llvm-profdata merge -output=stockfish.profdata *.profraw + $(XCRUN) $(LLVM_PROFDATA) merge -output=stockfish.profdata *.profraw $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ EXTRACXXFLAGS='-fprofile-instr-use=stockfish.profdata' \ EXTRALDFLAGS='-fprofile-use ' \ @@ -1110,6 +1172,6 @@ icx-profile-use: .depend: $(SRCS) -@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@ 2> /dev/null -ifeq (, $(filter $(MAKECMDGOALS), help strip install clean net objclean profileclean config-sanity)) +ifeq (, $(filter $(MAKECMDGOALS), help strip install clean net objclean profileclean format config-sanity)) -include .depend endif diff --git a/src/benchmark.cpp b/src/benchmark.cpp index 50f8612d9..4e266db89 100644 --- a/src/benchmark.cpp +++ b/src/benchmark.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -17,14 +17,13 @@ */ #include "benchmark.h" +#include "numa.h" #include #include #include #include -#include "position.h" - namespace { // clang-format off @@ -66,6 +65,10 @@ const std::vector Defaults = { "3Qb1k1/1r2ppb1/pN1n2q1/Pp1Pp1Pr/4P2p/4BP2/4B1R1/1R5K b - - 11 40", "4k3/3q1r2/1N2r1b1/3ppN2/2nPP3/1B1R2n1/2R1Q3/3K4 w - - 5 1", + // Positions with high numbers of changed threats + "k7/2n1n3/1nbNbn2/2NbRBn1/1nbRQR2/2NBRBN1/3N1N2/7K w - - 0 1", + "K7/8/8/BNQNQNB1/N5N1/R1Q1q2r/n5n1/bnqnqnbk w - - 0 1", + // 5-man positions "8/8/8/8/5kp1/P7/8/1K1N4 w - - 0 1", // Kc2 - mate "8/8/8/5N2/8/p7/8/2NK3k w - - 0 1", // Na2 - mate @@ -93,9 +96,285 @@ const std::vector Defaults = { }; // clang-format on +// clang-format off +// human-randomly picked 5 games with <60 moves from +// https://tests.stockfishchess.org/tests/view/665c71f9fd45fb0f907c21e0 +// only moves for one side +const std::vector> BenchmarkPositions = { + { + "rnbq1k1r/ppp1bppp/4pn2/8/2B5/2NP1N2/PPP2PPP/R1BQR1K1 b - - 2 8", + "rnbq1k1r/pp2bppp/4pn2/2p5/2B2B2/2NP1N2/PPP2PPP/R2QR1K1 b - - 1 9", + "r1bq1k1r/pp2bppp/2n1pn2/2p5/2B1NB2/3P1N2/PPP2PPP/R2QR1K1 b - - 3 10", + "r1bq1k1r/pp2bppp/2n1p3/2p5/2B1PB2/5N2/PPP2PPP/R2QR1K1 b - - 0 11", + "r1b2k1r/pp2bppp/2n1p3/2p5/2B1PB2/5N2/PPP2PPP/3RR1K1 b - - 0 12", + "r1b1k2r/pp2bppp/2n1p3/2p5/2B1PB2/2P2N2/PP3PPP/3RR1K1 b - - 0 13", + "r1b1k2r/1p2bppp/p1n1p3/2p5/4PB2/2P2N2/PP2BPPP/3RR1K1 b - - 1 14", + "r1b1k2r/4bppp/p1n1p3/1pp5/P3PB2/2P2N2/1P2BPPP/3RR1K1 b - - 0 15", + "r1b1k2r/4bppp/p1n1p3/1P6/2p1PB2/2P2N2/1P2BPPP/3RR1K1 b - - 0 16", + "r1b1k2r/4bppp/2n1p3/1p6/2p1PB2/1PP2N2/4BPPP/3RR1K1 b - - 0 17", + "r3k2r/3bbppp/2n1p3/1p6/2P1PB2/2P2N2/4BPPP/3RR1K1 b - - 0 18", + "r3k2r/3bbppp/2n1p3/8/1pP1P3/2P2N2/3BBPPP/3RR1K1 b - - 1 19", + "1r2k2r/3bbppp/2n1p3/8/1pPNP3/2P5/3BBPPP/3RR1K1 b - - 3 20", + "1r2k2r/3bbppp/2n1p3/8/2PNP3/2B5/4BPPP/3RR1K1 b - - 0 21", + "1r2k2r/3bb1pp/2n1pp2/1N6/2P1P3/2B5/4BPPP/3RR1K1 b - - 1 22", + "1r2k2r/3b2pp/2n1pp2/1N6/1BP1P3/8/4BPPP/3RR1K1 b - - 0 23", + "1r2k2r/3b2pp/4pp2/1N6/1nP1P3/8/3RBPPP/4R1K1 b - - 1 24", + "1r5r/3bk1pp/4pp2/1N6/1nP1PP2/8/3RB1PP/4R1K1 b - - 0 25", + "1r5r/3bk1pp/2n1pp2/1N6/2P1PP2/8/3RBKPP/4R3 b - - 2 26", + "1r5r/3bk1pp/2n2p2/1N2p3/2P1PP2/6P1/3RBK1P/4R3 b - - 0 27", + "1r1r4/3bk1pp/2n2p2/1N2p3/2P1PP2/6P1/3RBK1P/R7 b - - 2 28", + "1r1r4/N3k1pp/2n1bp2/4p3/2P1PP2/6P1/3RBK1P/R7 b - - 4 29", + "1r1r4/3bk1pp/2N2p2/4p3/2P1PP2/6P1/3RBK1P/R7 b - - 0 30", + "1r1R4/4k1pp/2b2p2/4p3/2P1PP2/6P1/4BK1P/R7 b - - 0 31", + "3r4/4k1pp/2b2p2/4P3/2P1P3/6P1/4BK1P/R7 b - - 0 32", + "3r4/R3k1pp/2b5/4p3/2P1P3/6P1/4BK1P/8 b - - 1 33", + "8/3rk1pp/2b5/R3p3/2P1P3/6P1/4BK1P/8 b - - 3 34", + "8/3r2pp/2bk4/R1P1p3/4P3/6P1/4BK1P/8 b - - 0 35", + "8/2kr2pp/2b5/R1P1p3/4P3/4K1P1/4B2P/8 b - - 2 36", + "1k6/3r2pp/2b5/RBP1p3/4P3/4K1P1/7P/8 b - - 4 37", + "8/1k1r2pp/2b5/R1P1p3/4P3/3BK1P1/7P/8 b - - 6 38", + "1k6/3r2pp/2b5/2P1p3/4P3/3BK1P1/7P/R7 b - - 8 39", + "1k6/r5pp/2b5/2P1p3/4P3/3BK1P1/7P/5R2 b - - 10 40", + "1k3R2/6pp/2b5/2P1p3/4P3/r2BK1P1/7P/8 b - - 12 41", + "5R2/2k3pp/2b5/2P1p3/4P3/r2B2P1/3K3P/8 b - - 14 42", + "5R2/2k3pp/2b5/2P1p3/4P3/3BK1P1/r6P/8 b - - 16 43", + "5R2/2k3pp/2b5/2P1p3/4P3/r2B2P1/4K2P/8 b - - 18 44", + "5R2/2k3pp/2b5/2P1p3/4P3/3B1KP1/r6P/8 b - - 20 45", + "8/2k2Rpp/2b5/2P1p3/4P3/r2B1KP1/7P/8 b - - 22 46", + "3k4/5Rpp/2b5/2P1p3/4P3/r2B2P1/4K2P/8 b - - 24 47", + "3k4/5Rpp/2b5/2P1p3/4P3/3B1KP1/r6P/8 b - - 26 48", + "3k4/5Rpp/2b5/2P1p3/4P3/r2B2P1/4K2P/8 b - - 28 49", + "3k4/5Rpp/2b5/2P1p3/4P3/3BK1P1/r6P/8 b - - 30 50", + "3k4/5Rpp/2b5/2P1p3/4P3/r2B2P1/3K3P/8 b - - 32 51", + "3k4/5Rpp/2b5/2P1p3/4P3/2KB2P1/r6P/8 b - - 34 52", + "3k4/5Rpp/2b5/2P1p3/4P3/r2B2P1/2K4P/8 b - - 36 53", + "3k4/5Rpp/2b5/2P1p3/4P3/1K1B2P1/r6P/8 b - - 38 54", + "3k4/6Rp/2b5/2P1p3/4P3/1K1B2P1/7r/8 b - - 0 55", + "3k4/8/2b3Rp/2P1p3/4P3/1K1B2P1/7r/8 b - - 1 56", + "8/2k3R1/2b4p/2P1p3/4P3/1K1B2P1/7r/8 b - - 3 57", + "3k4/8/2b3Rp/2P1p3/4P3/1K1B2P1/7r/8 b - - 5 58", + "8/2k5/2b3Rp/2P1p3/1K2P3/3B2P1/7r/8 b - - 7 59", + "8/2k5/2b3Rp/2P1p3/4P3/2KB2P1/3r4/8 b - - 9 60", + "8/2k5/2b3Rp/2P1p3/1K2P3/3B2P1/6r1/8 b - - 11 61", + "8/2k5/2b3Rp/2P1p3/4P3/2KB2P1/3r4/8 b - - 13 62", + "8/2k5/2b3Rp/2P1p3/2K1P3/3B2P1/6r1/8 b - - 15 63", + "4b3/2k3R1/7p/2P1p3/2K1P3/3B2P1/6r1/8 b - - 17 64", + }, + { + "r1bqkbnr/npp1pppp/p7/3P4/4pB2/2N5/PPP2PPP/R2QKBNR w KQkq - 1 6", + "r1bqkb1r/npp1pppp/p4n2/3P4/4pB2/2N5/PPP1QPPP/R3KBNR w KQkq - 3 7", + "r2qkb1r/npp1pppp/p4n2/3P1b2/4pB2/2N5/PPP1QPPP/2KR1BNR w kq - 5 8", + "r2qkb1r/1pp1pppp/p4n2/1n1P1b2/4pB2/2N4P/PPP1QPP1/2KR1BNR w kq - 1 9", + "r2qkb1r/1pp1pppp/5n2/1p1P1b2/4pB2/7P/PPP1QPP1/2KR1BNR w kq - 0 10", + "r2qkb1r/1ppbpppp/5n2/1Q1P4/4pB2/7P/PPP2PP1/2KR1BNR w kq - 1 11", + "3qkb1r/1Qpbpppp/5n2/3P4/4pB2/7P/rPP2PP1/2KR1BNR w k - 0 12", + "q3kb1r/1Qpbpppp/5n2/3P4/4pB2/7P/rPP2PP1/1K1R1BNR w k - 2 13", + "r3kb1r/2pbpppp/5n2/3P4/4pB2/7P/1PP2PP1/1K1R1BNR w k - 0 14", + "r3kb1r/2Bb1ppp/4pn2/3P4/4p3/7P/1PP2PP1/1K1R1BNR w k - 0 15", + "r3kb1r/2Bb2pp/4pn2/8/4p3/7P/1PP2PP1/1K1R1BNR w k - 0 16", + "r3k2r/2Bb2pp/4pn2/2b5/4p3/7P/1PP1NPP1/1K1R1B1R w k - 2 17", + "r6r/2Bbk1pp/4pn2/2b5/3Np3/7P/1PP2PP1/1K1R1B1R w - - 4 18", + "r6r/b2bk1pp/4pn2/4B3/3Np3/7P/1PP2PP1/1K1R1B1R w - - 6 19", + "r1r5/b2bk1pp/4pn2/4B3/2BNp3/7P/1PP2PP1/1K1R3R w - - 8 20", + "r7/b2bk1pp/4pn2/2r1B3/2BNp3/1P5P/2P2PP1/1K1R3R w - - 1 21", + "rb6/3bk1pp/4pn2/2r1B3/2BNpP2/1P5P/2P3P1/1K1R3R w - - 1 22", + "1r6/3bk1pp/4pn2/2r5/2BNpP2/1P5P/2P3P1/1K1R3R w - - 0 23", + "1r6/3bk1p1/4pn1p/2r5/2BNpP2/1P5P/2P3P1/2KR3R w - - 0 24", + "8/3bk1p1/1r2pn1p/2r5/2BNpP1P/1P6/2P3P1/2KR3R w - - 1 25", + "8/3bk3/1r2pnpp/2r5/2BNpP1P/1P6/2P3P1/2K1R2R w - - 0 26", + "2b5/4k3/1r2pnpp/2r5/2BNpP1P/1P4P1/2P5/2K1R2R w - - 1 27", + "8/1b2k3/1r2pnpp/2r5/2BNpP1P/1P4P1/2P5/2K1R1R1 w - - 3 28", + "8/1b1nk3/1r2p1pp/2r5/2BNpPPP/1P6/2P5/2K1R1R1 w - - 1 29", + "8/1b2k3/1r2p1pp/2r1nP2/2BNp1PP/1P6/2P5/2K1R1R1 w - - 1 30", + "8/1b2k3/1r2p1p1/2r1nPp1/2BNp2P/1P6/2P5/2K1R1R1 w - - 0 31", + "8/1b2k3/1r2p1n1/2r3p1/2BNp2P/1P6/2P5/2K1R1R1 w - - 0 32", + "8/1b2k3/1r2p1n1/6r1/2BNp2P/1P6/2P5/2K1R3 w - - 0 33", + "8/1b2k3/1r2p3/4n1P1/2BNp3/1P6/2P5/2K1R3 w - - 1 34", + "8/1b2k3/1r2p3/4n1P1/2BN4/1P2p3/2P5/2K4R w - - 0 35", + "8/1b2k3/1r2p2R/6P1/2nN4/1P2p3/2P5/2K5 w - - 0 36", + "8/1b2k3/3rp2R/6P1/2PN4/4p3/2P5/2K5 w - - 1 37", + "8/4k3/3rp2R/6P1/2PN4/2P1p3/6b1/2K5 w - - 1 38", + "8/4k3/r3p2R/2P3P1/3N4/2P1p3/6b1/2K5 w - - 1 39", + "8/3k4/r3p2R/2P2NP1/8/2P1p3/6b1/2K5 w - - 3 40", + "8/3k4/4p2R/2P3P1/8/2P1N3/6b1/r1K5 w - - 1 41", + "8/3k4/4p2R/2P3P1/8/2P1N3/3K2b1/6r1 w - - 3 42", + "8/3k4/4p2R/2P3P1/8/2PKNb2/8/6r1 w - - 5 43", + "8/4k3/4p1R1/2P3P1/8/2PKNb2/8/6r1 w - - 7 44", + "8/4k3/4p1R1/2P3P1/3K4/2P1N3/8/6rb w - - 9 45", + "8/3k4/4p1R1/2P1K1P1/8/2P1N3/8/6rb w - - 11 46", + "8/3k4/4p1R1/2P3P1/5K2/2P1N3/8/4r2b w - - 13 47", + "8/3k4/2b1p2R/2P3P1/5K2/2P1N3/8/4r3 w - - 15 48", + "8/3k4/2b1p3/2P3P1/5K2/2P1N2R/8/6r1 w - - 17 49", + "2k5/7R/2b1p3/2P3P1/5K2/2P1N3/8/6r1 w - - 19 50", + "2k5/7R/4p3/2P3P1/b1P2K2/4N3/8/6r1 w - - 1 51", + "2k5/3bR3/4p3/2P3P1/2P2K2/4N3/8/6r1 w - - 3 52", + "3k4/3b2R1/4p3/2P3P1/2P2K2/4N3/8/6r1 w - - 5 53", + "3kb3/6R1/4p1P1/2P5/2P2K2/4N3/8/6r1 w - - 1 54", + "3kb3/6R1/4p1P1/2P5/2P2KN1/8/8/2r5 w - - 3 55", + "3kb3/6R1/4p1P1/2P1N3/2P2K2/8/8/5r2 w - - 5 56", + "3kb3/6R1/4p1P1/2P1N3/2P5/4K3/8/4r3 w - - 7 57", + }, + { + "rnbq1rk1/ppp1npb1/4p1p1/3P3p/3PP3/2N2N2/PP2BPPP/R1BQ1RK1 b - - 0 8", + "rnbq1rk1/ppp1npb1/6p1/3pP2p/3P4/2N2N2/PP2BPPP/R1BQ1RK1 b - - 0 9", + "rn1q1rk1/ppp1npb1/6p1/3pP2p/3P2b1/2N2N2/PP2BPPP/R1BQR1K1 b - - 2 10", + "r2q1rk1/ppp1npb1/2n3p1/3pP2p/3P2bN/2N5/PP2BPPP/R1BQR1K1 b - - 4 11", + "r4rk1/pppqnpb1/2n3p1/3pP2p/3P2bN/2N4P/PP2BPP1/R1BQR1K1 b - - 0 12", + "r4rk1/pppqnpb1/2n3p1/3pP2p/3P3N/7P/PP2NPP1/R1BQR1K1 b - - 0 13", + "r4rk1/pppq1pb1/2n3p1/3pPN1p/3P4/7P/PP2NPP1/R1BQR1K1 b - - 0 14", + "r4rk1/ppp2pb1/2n3p1/3pPq1p/3P1N2/7P/PP3PP1/R1BQR1K1 b - - 1 15", + "r4rk1/pppq1pb1/2n3p1/3pP2p/P2P1N2/7P/1P3PP1/R1BQR1K1 b - - 0 16", + "r2n1rk1/pppq1pb1/6p1/3pP2p/P2P1N2/R6P/1P3PP1/2BQR1K1 b - - 2 17", + "r4rk1/pppq1pb1/4N1p1/3pP2p/P2P4/R6P/1P3PP1/2BQR1K1 b - - 0 18", + "r4rk1/ppp2pb1/4q1p1/3pP1Bp/P2P4/R6P/1P3PP1/3QR1K1 b - - 1 19", + "r3r1k1/ppp2pb1/4q1p1/3pP1Bp/P2P1P2/R6P/1P4P1/3QR1K1 b - - 0 20", + "r3r1k1/ppp3b1/4qpp1/3pP2p/P2P1P1B/R6P/1P4P1/3QR1K1 b - - 1 21", + "r3r1k1/ppp3b1/4q1p1/3pP2p/P4P1B/R6P/1P4P1/3QR1K1 b - - 0 22", + "r4rk1/ppp3b1/4q1p1/3pP1Bp/P4P2/R6P/1P4P1/3QR1K1 b - - 2 23", + "r4rk1/pp4b1/4q1p1/2ppP1Bp/P4P2/3R3P/1P4P1/3QR1K1 b - - 1 24", + "r4rk1/pp4b1/4q1p1/2p1P1Bp/P2p1PP1/3R3P/1P6/3QR1K1 b - - 0 25", + "r4rk1/pp4b1/4q1p1/2p1P1B1/P2p1PP1/3R4/1P6/3QR1K1 b - - 0 26", + "r5k1/pp3rb1/4q1p1/2p1P1B1/P2p1PP1/6R1/1P6/3QR1K1 b - - 2 27", + "5rk1/pp3rb1/4q1p1/2p1P1B1/P2pRPP1/6R1/1P6/3Q2K1 b - - 4 28", + "5rk1/1p3rb1/p3q1p1/P1p1P1B1/3pRPP1/6R1/1P6/3Q2K1 b - - 0 29", + "4r1k1/1p3rb1/p3q1p1/P1p1P1B1/3pRPP1/1P4R1/8/3Q2K1 b - - 0 30", + "4r1k1/5rb1/pP2q1p1/2p1P1B1/3pRPP1/1P4R1/8/3Q2K1 b - - 0 31", + "4r1k1/5rb1/pq4p1/2p1P1B1/3pRPP1/1P4R1/4Q3/6K1 b - - 1 32", + "4r1k1/1r4b1/pq4p1/2p1P1B1/3pRPP1/1P4R1/2Q5/6K1 b - - 3 33", + "4r1k1/1r4b1/1q4p1/p1p1P1B1/3p1PP1/1P4R1/2Q5/4R1K1 b - - 1 34", + "4r1k1/3r2b1/1q4p1/p1p1P1B1/2Qp1PP1/1P4R1/8/4R1K1 b - - 3 35", + "4r1k1/3r2b1/4q1p1/p1p1P1B1/2Qp1PP1/1P4R1/5K2/4R3 b - - 5 36", + "4r1k1/3r2b1/6p1/p1p1P1B1/2Pp1PP1/6R1/5K2/4R3 b - - 0 37", + "4r1k1/3r2b1/6p1/p1p1P1B1/2P2PP1/3p2R1/5K2/3R4 b - - 1 38", + "5rk1/3r2b1/6p1/p1p1P1B1/2P2PP1/3p2R1/8/3RK3 b - - 3 39", + "5rk1/6b1/6p1/p1p1P1B1/2Pr1PP1/3R4/8/3RK3 b - - 0 40", + "5rk1/3R2b1/6p1/p1p1P1B1/2r2PP1/8/8/3RK3 b - - 1 41", + "5rk1/3R2b1/6p1/p1p1P1B1/4rPP1/8/3K4/3R4 b - - 3 42", + "1r4k1/3R2b1/6p1/p1p1P1B1/4rPP1/2K5/8/3R4 b - - 5 43", + "1r4k1/3R2b1/6p1/p1p1P1B1/2K2PP1/4r3/8/3R4 b - - 7 44", + "1r3bk1/8/3R2p1/p1p1P1B1/2K2PP1/4r3/8/3R4 b - - 9 45", + "1r3bk1/8/6R1/2p1P1B1/p1K2PP1/4r3/8/3R4 b - - 0 46", + "1r3b2/5k2/R7/2p1P1B1/p1K2PP1/4r3/8/3R4 b - - 2 47", + "5b2/1r3k2/R7/2p1P1B1/p1K2PP1/4r3/8/7R b - - 4 48", + "5b2/5k2/R7/2pKP1B1/pr3PP1/4r3/8/7R b - - 6 49", + "5b2/5k2/R1K5/2p1P1B1/p2r1PP1/4r3/8/7R b - - 8 50", + "8/R4kb1/2K5/2p1P1B1/p2r1PP1/4r3/8/7R b - - 10 51", + "8/R5b1/2K3k1/2p1PPB1/p2r2P1/4r3/8/7R b - - 0 52", + "8/6R1/2K5/2p1PPk1/p2r2P1/4r3/8/7R b - - 0 53", + "8/6R1/2K5/2p1PP2/p2r1kP1/4r3/8/5R2 b - - 2 54", + "8/6R1/2K2P2/2p1P3/p2r2P1/4r1k1/8/5R2 b - - 0 55", + "8/5PR1/2K5/2p1P3/p2r2P1/4r3/6k1/5R2 b - - 0 56", + }, + { + "rn1qkb1r/p1pbpppp/5n2/8/2pP4/2N5/1PQ1PPPP/R1B1KBNR w KQkq - 0 7", + "r2qkb1r/p1pbpppp/2n2n2/8/2pP4/2N2N2/1PQ1PPPP/R1B1KB1R w KQkq - 2 8", + "r2qkb1r/p1pbpppp/5n2/8/1npPP3/2N2N2/1PQ2PPP/R1B1KB1R w KQkq - 1 9", + "r2qkb1r/p1pb1ppp/4pn2/8/1npPP3/2N2N2/1P3PPP/R1BQKB1R w KQkq - 0 10", + "r2qk2r/p1pbbppp/4pn2/8/1nBPP3/2N2N2/1P3PPP/R1BQK2R w KQkq - 1 11", + "r2q1rk1/p1pbbppp/4pn2/8/1nBPP3/2N2N2/1P3PPP/R1BQ1RK1 w - - 3 12", + "r2q1rk1/2pbbppp/p3pn2/8/1nBPPB2/2N2N2/1P3PPP/R2Q1RK1 w - - 0 13", + "r2q1rk1/2p1bppp/p3pn2/1b6/1nBPPB2/2N2N2/1P3PPP/R2QR1K1 w - - 2 14", + "r2q1rk1/4bppp/p1p1pn2/1b6/1nBPPB2/1PN2N2/5PPP/R2QR1K1 w - - 0 15", + "r4rk1/3qbppp/p1p1pn2/1b6/1nBPPB2/1PN2N2/3Q1PPP/R3R1K1 w - - 2 16", + "r4rk1/1q2bppp/p1p1pn2/1b6/1nBPPB2/1PN2N1P/3Q1PP1/R3R1K1 w - - 1 17", + "r3r1k1/1q2bppp/p1p1pn2/1b6/1nBPPB2/1PN2N1P/4QPP1/R3R1K1 w - - 3 18", + "r3r1k1/1q1nbppp/p1p1p3/1b6/1nBPPB2/1PN2N1P/4QPP1/3RR1K1 w - - 5 19", + "r3rbk1/1q1n1ppp/p1p1p3/1b6/1nBPPB2/1PN2N1P/3RQPP1/4R1K1 w - - 7 20", + "r3rbk1/1q3ppp/pnp1p3/1b6/1nBPPB2/1PN2N1P/3RQPP1/4R2K w - - 9 21", + "2r1rbk1/1q3ppp/pnp1p3/1b6/1nBPPB2/1PN2N1P/3RQPP1/1R5K w - - 11 22", + "2r1rbk1/1q4pp/pnp1pp2/1b6/1nBPPB2/1PN2N1P/4QPP1/1R1R3K w - - 0 23", + "2r1rbk1/5qpp/pnp1pp2/1b6/1nBPP3/1PN1BN1P/4QPP1/1R1R3K w - - 2 24", + "2r1rbk1/5qp1/pnp1pp1p/1b6/1nBPP3/1PN1BN1P/4QPP1/1R1R2K1 w - - 0 25", + "2r1rbk1/5qp1/pnp1pp1p/1b6/2BPP3/1P2BN1P/n3QPP1/1R1R2K1 w - - 0 26", + "r3rbk1/5qp1/pnp1pp1p/1b6/2BPP3/1P2BN1P/Q4PP1/1R1R2K1 w - - 1 27", + "rr3bk1/5qp1/pnp1pp1p/1b6/2BPP3/1P2BN1P/Q4PP1/R2R2K1 w - - 3 28", + "rr2qbk1/6p1/pnp1pp1p/1b6/2BPP3/1P2BN1P/4QPP1/R2R2K1 w - - 5 29", + "rr2qbk1/6p1/1np1pp1p/pb6/2BPP3/1P1QBN1P/5PP1/R2R2K1 w - - 0 30", + "rr2qbk1/6p1/1n2pp1p/pp6/3PP3/1P1QBN1P/5PP1/R2R2K1 w - - 0 31", + "rr2qbk1/6p1/1n2pp1p/1p1P4/p3P3/1P1QBN1P/5PP1/R2R2K1 w - - 0 32", + "rr2qbk1/3n2p1/3Ppp1p/1p6/p3P3/1P1QBN1P/5PP1/R2R2K1 w - - 1 33", + "rr3bk1/3n2p1/3Ppp1p/1p5q/pP2P3/3QBN1P/5PP1/R2R2K1 w - - 1 34", + "rr3bk1/3n2p1/3Ppp1p/1p5q/1P2P3/p2QBN1P/5PP1/2RR2K1 w - - 0 35", + "1r3bk1/3n2p1/r2Ppp1p/1p5q/1P2P3/pQ2BN1P/5PP1/2RR2K1 w - - 2 36", + "1r2qbk1/2Rn2p1/r2Ppp1p/1p6/1P2P3/pQ2BN1P/5PP1/3R2K1 w - - 4 37", + "1r2qbk1/2Rn2p1/r2Ppp1p/1pB5/1P2P3/1Q3N1P/p4PP1/3R2K1 w - - 0 38", + "1r2q1k1/2Rn2p1/r2bpp1p/1pB5/1P2P3/1Q3N1P/p4PP1/R5K1 w - - 0 39", + "1r2q1k1/2Rn2p1/3rpp1p/1p6/1P2P3/1Q3N1P/p4PP1/R5K1 w - - 0 40", + "2r1q1k1/2Rn2p1/3rpp1p/1p6/1P2P3/5N1P/Q4PP1/R5K1 w - - 1 41", + "1r2q1k1/1R1n2p1/3rpp1p/1p6/1P2P3/5N1P/Q4PP1/R5K1 w - - 3 42", + "2r1q1k1/2Rn2p1/3rpp1p/1p6/1P2P3/5N1P/Q4PP1/R5K1 w - - 5 43", + "1r2q1k1/1R1n2p1/3rpp1p/1p6/1P2P3/5N1P/Q4PP1/R5K1 w - - 7 44", + "1rq3k1/R2n2p1/3rpp1p/1p6/1P2P3/5N1P/Q4PP1/R5K1 w - - 9 45", + "2q3k1/Rr1n2p1/3rpp1p/1p6/1P2P3/5N1P/4QPP1/R5K1 w - - 11 46", + "Rrq3k1/3n2p1/3rpp1p/1p6/1P2P3/5N1P/4QPP1/R5K1 w - - 13 47", + }, + { + "rn1qkb1r/1pp2ppp/p4p2/3p1b2/5P2/1P2PN2/P1PP2PP/RN1QKB1R b KQkq - 1 6", + "r2qkb1r/1pp2ppp/p1n2p2/3p1b2/3P1P2/1P2PN2/P1P3PP/RN1QKB1R b KQkq - 0 7", + "r2qkb1r/1pp2ppp/p4p2/3p1b2/1n1P1P2/1P1BPN2/P1P3PP/RN1QK2R b KQkq - 2 8", + "r2qkb1r/1pp2ppp/p4p2/3p1b2/3P1P2/1P1PPN2/P5PP/RN1QK2R b KQkq - 0 9", + "r2qk2r/1pp2ppp/p2b1p2/3p1b2/3P1P2/1PNPPN2/P5PP/R2QK2R b KQkq - 2 10", + "r2qk2r/1p3ppp/p1pb1p2/3p1b2/3P1P2/1PNPPN2/P5PP/R2Q1RK1 b kq - 1 11", + "r2q1rk1/1p3ppp/p1pb1p2/3p1b2/3P1P2/1PNPPN2/P2Q2PP/R4RK1 b - - 3 12", + "r2qr1k1/1p3ppp/p1pb1p2/3p1b2/3P1P2/1P1PPN2/P2QN1PP/R4RK1 b - - 5 13", + "r3r1k1/1p3ppp/pqpb1p2/3p1b2/3P1P2/1P1PPNN1/P2Q2PP/R4RK1 b - - 7 14", + "r3r1k1/1p3ppp/pqp2p2/3p1b2/1b1P1P2/1P1PPNN1/P1Q3PP/R4RK1 b - - 9 15", + "r3r1k1/1p1b1ppp/pqp2p2/3p4/1b1P1P2/1P1PPNN1/P4QPP/R4RK1 b - - 11 16", + "2r1r1k1/1p1b1ppp/pqp2p2/3p4/1b1PPP2/1P1P1NN1/P4QPP/R4RK1 b - - 0 17", + "2r1r1k1/1p1b1ppp/pq3p2/2pp4/1b1PPP2/PP1P1NN1/5QPP/R4RK1 b - - 0 18", + "2r1r1k1/1p1b1ppp/pq3p2/2Pp4/4PP2/PPbP1NN1/5QPP/R4RK1 b - - 0 19", + "2r1r1k1/1p1b1ppp/p4p2/2Pp4/4PP2/PqbP1NN1/5QPP/RR4K1 b - - 1 20", + "2r1r1k1/1p1b1ppp/p4p2/2Pp4/q3PP2/P1bP1NN1/R4QPP/1R4K1 b - - 3 21", + "2r1r1k1/1p3ppp/p4p2/1bPP4/q4P2/P1bP1NN1/R4QPP/1R4K1 b - - 0 22", + "2r1r1k1/1p3ppp/p4p2/2PP4/q4P2/P1bb1NN1/R4QPP/2R3K1 b - - 1 23", + "2r1r1k1/1p3ppp/p2P1p2/2P5/2q2P2/P1bb1NN1/R4QPP/2R3K1 b - - 0 24", + "2rr2k1/1p3ppp/p2P1p2/2P5/2q2P2/P1bb1NN1/R4QPP/2R4K b - - 2 25", + "2rr2k1/1p3ppp/p2P1p2/2Q5/5P2/P1bb1NN1/R5PP/2R4K b - - 0 26", + "3r2k1/1p3ppp/p2P1p2/2r5/5P2/P1bb1N2/R3N1PP/2R4K b - - 1 27", + "3r2k1/1p3ppp/p2P1p2/2r5/5P2/P1b2N2/4R1PP/2R4K b - - 0 28", + "3r2k1/1p3ppp/p2P1p2/2r5/1b3P2/P4N2/4R1PP/3R3K b - - 2 29", + "3r2k1/1p2Rppp/p2P1p2/b1r5/5P2/P4N2/6PP/3R3K b - - 4 30", + "3r2k1/1R3ppp/p1rP1p2/b7/5P2/P4N2/6PP/3R3K b - - 0 31", + "3r2k1/1R3ppp/p2R1p2/b7/5P2/P4N2/6PP/7K b - - 0 32", + "6k1/1R3ppp/p2r1p2/b7/5P2/P4NP1/7P/7K b - - 0 33", + "6k1/1R3p1p/p2r1pp1/b7/5P1P/P4NP1/8/7K b - - 0 34", + "6k1/3R1p1p/pr3pp1/b7/5P1P/P4NP1/8/7K b - - 2 35", + "6k1/5p2/pr3pp1/b2R3p/5P1P/P4NP1/8/7K b - - 1 36", + "6k1/5p2/pr3pp1/7p/5P1P/P1bR1NP1/8/7K b - - 3 37", + "6k1/5p2/p1r2pp1/7p/5P1P/P1bR1NP1/6K1/8 b - - 5 38", + "6k1/5p2/p1r2pp1/b2R3p/5P1P/P4NP1/6K1/8 b - - 7 39", + "6k1/5p2/p4pp1/b2R3p/5P1P/P4NPK/2r5/8 b - - 9 40", + "6k1/2b2p2/p4pp1/7p/5P1P/P2R1NPK/2r5/8 b - - 11 41", + "6k1/2b2p2/5pp1/p6p/3N1P1P/P2R2PK/2r5/8 b - - 1 42", + "6k1/2b2p2/5pp1/p6p/3N1P1P/P1R3PK/r7/8 b - - 3 43", + "6k1/5p2/1b3pp1/p6p/5P1P/P1R3PK/r1N5/8 b - - 5 44", + "8/5pk1/1bR2pp1/p6p/5P1P/P5PK/r1N5/8 b - - 7 45", + "3b4/5pk1/2R2pp1/p4P1p/7P/P5PK/r1N5/8 b - - 0 46", + "8/4bpk1/2R2pp1/p4P1p/6PP/P6K/r1N5/8 b - - 0 47", + "8/5pk1/2R2pP1/p6p/6PP/b6K/r1N5/8 b - - 0 48", + "8/6k1/2R2pp1/p6P/7P/b6K/r1N5/8 b - - 0 49", + "8/6k1/2R2p2/p6p/7P/b5K1/r1N5/8 b - - 1 50", + "8/8/2R2pk1/p6p/7P/b4K2/r1N5/8 b - - 3 51", + "8/8/2R2pk1/p6p/7P/4NK2/rb6/8 b - - 5 52", + "2R5/8/5pk1/7p/p6P/4NK2/rb6/8 b - - 1 53", + "6R1/8/5pk1/7p/p6P/4NK2/1b6/r7 b - - 3 54", + "R7/5k2/5p2/7p/p6P/4NK2/1b6/r7 b - - 5 55", + "R7/5k2/5p2/7p/7P/p3N3/1b2K3/r7 b - - 1 56", + "8/R4k2/5p2/7p/7P/p3N3/1b2K3/7r b - - 3 57", + "8/8/5pk1/7p/R6P/p3N3/1b2K3/7r b - - 5 58", + "8/8/5pk1/7p/R6P/p7/4K3/2bN3r b - - 7 59", + "8/8/5pk1/7p/R6P/p7/4KN1r/2b5 b - - 9 60", + "8/8/5pk1/7p/R6P/p3K3/1b3N1r/8 b - - 11 61", + "8/8/R4pk1/7p/7P/p1b1K3/5N1r/8 b - - 13 62", + "8/8/5pk1/7p/7P/2b1K3/R4N1r/8 b - - 0 63", + "8/8/5pk1/7p/3K3P/8/R4N1r/4b3 b - - 2 64", + } +}; +// clang-format on + } // namespace -namespace Stockfish { +namespace Stockfish::Benchmark { // Builds a list of UCI commands to be run by bench. There // are five parameters: TT size in MB, number of search threads that @@ -108,7 +387,7 @@ namespace Stockfish { // bench 64 1 100000 default nodes : search default positions for 100K nodes each // bench 64 4 5000 current movetime : search current position with 4 threads for 5 sec // bench 16 1 5 blah perft : run a perft 5 on positions in file "blah" -std::vector setup_bench(const Position& current, std::istream& is) { +std::vector setup_bench(const std::string& currentFen, std::istream& is) { std::vector fens, list; std::string go, token; @@ -126,7 +405,7 @@ std::vector setup_bench(const Position& current, std::istream& is) fens = Defaults; else if (fenFile == "current") - fens.push_back(current.fen()); + fens.push_back(currentFen); else { @@ -162,4 +441,75 @@ std::vector setup_bench(const Position& current, std::istream& is) return list; } -} // namespace Stockfish \ No newline at end of file +BenchmarkSetup setup_benchmark(std::istream& is) { + // TT_SIZE_PER_THREAD is chosen such that roughly half of the hash is used all positions + // for the current sequence have been searched. + static constexpr int TT_SIZE_PER_THREAD = 128; + + static constexpr int DEFAULT_DURATION_S = 150; + + BenchmarkSetup setup{}; + + // Assign default values to missing arguments + int desiredTimeS; + + if (!(is >> setup.threads)) + setup.threads = int(get_hardware_concurrency()); + else + setup.originalInvocation += std::to_string(setup.threads); + + if (!(is >> setup.ttSize)) + setup.ttSize = TT_SIZE_PER_THREAD * setup.threads; + else + setup.originalInvocation += " " + std::to_string(setup.ttSize); + + if (!(is >> desiredTimeS)) + desiredTimeS = DEFAULT_DURATION_S; + else + setup.originalInvocation += " " + std::to_string(desiredTimeS); + + setup.filledInvocation += std::to_string(setup.threads) + " " + std::to_string(setup.ttSize) + + " " + std::to_string(desiredTimeS); + + auto getCorrectedTime = [&](int ply) { + // time per move is fit roughly based on LTC games + // seconds = 50/{ply+15} + // ms = 50000/{ply+15} + // with this fit 10th move gets 2000ms + // adjust for desired 10th move time + return 50000.0 / (static_cast(ply) + 15.0); + }; + + float totalTime = 0; + for (const auto& game : BenchmarkPositions) + { + int ply = 1; + for (int i = 0; i < static_cast(game.size()); ++i) + { + const float correctedTime = float(getCorrectedTime(ply)); + totalTime += correctedTime; + ply += 1; + } + } + + float timeScaleFactor = static_cast(desiredTimeS * 1000) / totalTime; + + for (const auto& game : BenchmarkPositions) + { + setup.commands.emplace_back("ucinewgame"); + int ply = 1; + for (const std::string& fen : game) + { + setup.commands.emplace_back("position fen " + fen); + + const int correctedTime = static_cast(getCorrectedTime(ply) * timeScaleFactor); + setup.commands.emplace_back("go movetime " + std::to_string(correctedTime)); + + ply += 1; + } + } + + return setup; +} + +} // namespace Stockfish diff --git a/src/benchmark.h b/src/benchmark.h index 86f8a0ad5..a6606e78c 100644 --- a/src/benchmark.h +++ b/src/benchmark.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -23,11 +23,19 @@ #include #include -namespace Stockfish { +namespace Stockfish::Benchmark { -class Position; +std::vector setup_bench(const std::string&, std::istream&); -std::vector setup_bench(const Position&, std::istream&); +struct BenchmarkSetup { + int ttSize; + int threads; + std::vector commands; + std::string originalInvocation; + std::string filledInvocation; +}; + +BenchmarkSetup setup_benchmark(std::istream&); } // namespace Stockfish diff --git a/src/bitboard.cpp b/src/bitboard.cpp index 32c626d47..4decb8d66 100644 --- a/src/bitboard.cpp +++ b/src/bitboard.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -31,25 +31,16 @@ uint8_t SquareDistance[SQUARE_NB][SQUARE_NB]; Bitboard LineBB[SQUARE_NB][SQUARE_NB]; Bitboard BetweenBB[SQUARE_NB][SQUARE_NB]; -Bitboard PseudoAttacks[PIECE_TYPE_NB][SQUARE_NB]; -Bitboard PawnAttacks[COLOR_NB][SQUARE_NB]; +Bitboard RayPassBB[SQUARE_NB][SQUARE_NB]; -Magic RookMagics[SQUARE_NB]; -Magic BishopMagics[SQUARE_NB]; +alignas(64) Magic Magics[SQUARE_NB][2]; namespace { Bitboard RookTable[0x19000]; // To store rook attacks Bitboard BishopTable[0x1480]; // To store bishop attacks -void init_magics(PieceType pt, Bitboard table[], Magic magics[]); - -// Returns the bitboard of target square for the given step -// from the given square. If the step is off the board, returns empty bitboard. -Bitboard safe_destination(Square s, int step) { - Square to = Square(s + step); - return is_ok(to) && distance(s, to) <= 2 ? square_bb(to) : Bitboard(0); -} +void init_magics(PieceType pt, Bitboard table[], Magic magics[][2]); } // Returns an ASCII representation of a bitboard suitable @@ -82,23 +73,11 @@ void Bitboards::init() { for (Square s2 = SQ_A1; s2 <= SQ_H8; ++s2) SquareDistance[s1][s2] = std::max(distance(s1, s2), distance(s1, s2)); - init_magics(ROOK, RookTable, RookMagics); - init_magics(BISHOP, BishopTable, BishopMagics); + init_magics(ROOK, RookTable, Magics); + init_magics(BISHOP, BishopTable, Magics); for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1) { - PawnAttacks[WHITE][s1] = pawn_attacks_bb(square_bb(s1)); - PawnAttacks[BLACK][s1] = pawn_attacks_bb(square_bb(s1)); - - for (int step : {-9, -8, -7, -1, 1, 7, 8, 9}) - PseudoAttacks[KING][s1] |= safe_destination(s1, step); - - for (int step : {-17, -15, -10, -6, 6, 10, 15, 17}) - PseudoAttacks[KNIGHT][s1] |= safe_destination(s1, step); - - PseudoAttacks[QUEEN][s1] = PseudoAttacks[BISHOP][s1] = attacks_bb(s1, 0); - PseudoAttacks[QUEEN][s1] |= PseudoAttacks[ROOK][s1] = attacks_bb(s1, 0); - for (PieceType pt : {BISHOP, ROOK}) for (Square s2 = SQ_A1; s2 <= SQ_H8; ++s2) { @@ -107,6 +86,8 @@ void Bitboards::init() { LineBB[s1][s2] = (attacks_bb(pt, s1, 0) & attacks_bb(pt, s2, 0)) | s1 | s2; BetweenBB[s1][s2] = (attacks_bb(pt, s1, square_bb(s2)) & attacks_bb(pt, s2, square_bb(s1))); + RayPassBB[s1][s2] = + attacks_bb(pt, s1, 0) & (attacks_bb(pt, s2, square_bb(s1)) | s2); } BetweenBB[s1][s2] |= s2; } @@ -114,62 +95,52 @@ void Bitboards::init() { } namespace { - -Bitboard sliding_attack(PieceType pt, Square sq, Bitboard occupied) { - - Bitboard attacks = 0; - Direction RookDirections[4] = {NORTH, SOUTH, EAST, WEST}; - Direction BishopDirections[4] = {NORTH_EAST, SOUTH_EAST, SOUTH_WEST, NORTH_WEST}; - - for (Direction d : (pt == ROOK ? RookDirections : BishopDirections)) - { - Square s = sq; - while (safe_destination(s, d) && !(occupied & s)) - attacks |= (s += d); - } - - return attacks; -} - - // Computes all rook and bishop attacks at startup. Magic // bitboards are used to look up attacks of sliding pieces. As a reference see -// www.chessprogramming.org/Magic_Bitboards. In particular, here we use the so -// called "fancy" approach. -void init_magics(PieceType pt, Bitboard table[], Magic magics[]) { +// https://www.chessprogramming.org/Magic_Bitboards. In particular, here we use +// the so called "fancy" approach. +void init_magics(PieceType pt, Bitboard table[], Magic magics[][2]) { +#ifndef USE_PEXT // Optimal PRNG seeds to pick the correct magics in the shortest time int seeds[][RANK_NB] = {{8977, 44560, 54343, 38998, 5731, 95205, 104912, 17020}, {728, 10316, 55013, 32803, 12281, 15100, 16645, 255}}; - Bitboard occupancy[4096], reference[4096], edges, b; - int epoch[4096] = {}, cnt = 0, size = 0; + Bitboard occupancy[4096]; + int epoch[4096] = {}, cnt = 0; +#endif + Bitboard reference[4096]; + int size = 0; for (Square s = SQ_A1; s <= SQ_H8; ++s) { // Board edges are not considered in the relevant occupancies - edges = ((Rank1BB | Rank8BB) & ~rank_bb(s)) | ((FileABB | FileHBB) & ~file_bb(s)); + Bitboard edges = ((Rank1BB | Rank8BB) & ~rank_bb(s)) | ((FileABB | FileHBB) & ~file_bb(s)); // Given a square 's', the mask is the bitboard of sliding attacks from // 's' computed on an empty board. The index must be big enough to contain // all the attacks for each possible subset of the mask and so is 2 power // the number of 1s of the mask. Hence we deduce the size of the shift to // apply to the 64 or 32 bits word to get the index. - Magic& m = magics[s]; - m.mask = sliding_attack(pt, s, 0) & ~edges; - m.shift = (Is64Bit ? 64 : 32) - popcount(m.mask); - + Magic& m = magics[s][pt - BISHOP]; + m.mask = Bitboards::sliding_attack(pt, s, 0) & ~edges; +#ifndef USE_PEXT + m.shift = (Is64Bit ? 64 : 32) - popcount(m.mask); +#endif // Set the offset for the attacks table of the square. We have individual // table sizes for each square with "Fancy Magic Bitboards". - m.attacks = s == SQ_A1 ? table : magics[s - 1].attacks + size; + m.attacks = s == SQ_A1 ? table : magics[s - 1][pt - BISHOP].attacks + size; + size = 0; // Use Carry-Rippler trick to enumerate all subsets of masks[s] and // store the corresponding sliding attack bitboard in reference[]. - b = size = 0; + Bitboard b = 0; do { +#ifndef USE_PEXT occupancy[size] = b; - reference[size] = sliding_attack(pt, s, b); +#endif + reference[size] = Bitboards::sliding_attack(pt, s, b); if (HasPext) m.attacks[pext(b, m.mask)] = reference[size]; @@ -178,9 +149,7 @@ void init_magics(PieceType pt, Bitboard table[], Magic magics[]) { b = (b - m.mask) & m.mask; } while (b); - if (HasPext) - continue; - +#ifndef USE_PEXT PRNG rng(seeds[Is64Bit][rank_of(s)]); // Find a magic for square 's' picking up an (almost) random number @@ -209,6 +178,7 @@ void init_magics(PieceType pt, Bitboard table[], Magic magics[]) { break; } } +#endif } } } diff --git a/src/bitboard.h b/src/bitboard.h index cdff4c759..f97ff3219 100644 --- a/src/bitboard.h +++ b/src/bitboard.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -22,9 +22,12 @@ #include #include #include +#include #include #include #include +#include +#include #include "types.h" @@ -60,34 +63,36 @@ extern uint8_t SquareDistance[SQUARE_NB][SQUARE_NB]; extern Bitboard BetweenBB[SQUARE_NB][SQUARE_NB]; extern Bitboard LineBB[SQUARE_NB][SQUARE_NB]; -extern Bitboard PseudoAttacks[PIECE_TYPE_NB][SQUARE_NB]; -extern Bitboard PawnAttacks[COLOR_NB][SQUARE_NB]; - +extern Bitboard RayPassBB[SQUARE_NB][SQUARE_NB]; // Magic holds all magic bitboards relevant data for a single square struct Magic { Bitboard mask; - Bitboard magic; Bitboard* attacks; - unsigned shift; +#ifndef USE_PEXT + Bitboard magic; + unsigned shift; +#endif // Compute the attack's index using the 'magic bitboards' approach unsigned index(Bitboard occupied) const { - if (HasPext) - return unsigned(pext(occupied, mask)); - +#ifdef USE_PEXT + return unsigned(pext(occupied, mask)); +#else if (Is64Bit) return unsigned(((occupied & mask) * magic) >> shift); unsigned lo = unsigned(occupied) & unsigned(mask); unsigned hi = unsigned(occupied >> 32) & unsigned(mask >> 32); return (lo * unsigned(magic) ^ hi * unsigned(magic >> 32)) >> shift; +#endif } + + Bitboard attacks_bb(Bitboard occupied) const { return attacks[index(occupied)]; } }; -extern Magic RookMagics[SQUARE_NB]; -extern Magic BishopMagics[SQUARE_NB]; +extern Magic Magics[SQUARE_NB][2]; constexpr Bitboard square_bb(Square s) { assert(is_ok(s)); @@ -98,17 +103,17 @@ constexpr Bitboard square_bb(Square s) { // Overloads of bitwise operators between a Bitboard and a Square for testing // whether a given bit is set in a bitboard, and for setting and clearing bits. -inline Bitboard operator&(Bitboard b, Square s) { return b & square_bb(s); } -inline Bitboard operator|(Bitboard b, Square s) { return b | square_bb(s); } -inline Bitboard operator^(Bitboard b, Square s) { return b ^ square_bb(s); } -inline Bitboard& operator|=(Bitboard& b, Square s) { return b |= square_bb(s); } -inline Bitboard& operator^=(Bitboard& b, Square s) { return b ^= square_bb(s); } +constexpr Bitboard operator&(Bitboard b, Square s) { return b & square_bb(s); } +constexpr Bitboard operator|(Bitboard b, Square s) { return b | square_bb(s); } +constexpr Bitboard operator^(Bitboard b, Square s) { return b ^ square_bb(s); } +constexpr Bitboard& operator|=(Bitboard& b, Square s) { return b |= square_bb(s); } +constexpr Bitboard& operator^=(Bitboard& b, Square s) { return b ^= square_bb(s); } -inline Bitboard operator&(Square s, Bitboard b) { return b & s; } -inline Bitboard operator|(Square s, Bitboard b) { return b | s; } -inline Bitboard operator^(Square s, Bitboard b) { return b ^ s; } +constexpr Bitboard operator&(Square s, Bitboard b) { return b & s; } +constexpr Bitboard operator|(Square s, Bitboard b) { return b | s; } +constexpr Bitboard operator^(Square s, Bitboard b) { return b ^ s; } -inline Bitboard operator|(Square s1, Square s2) { return square_bb(s1) | s2; } +constexpr Bitboard operator|(Square s1, Square s2) { return square_bb(s1) | s2; } constexpr bool more_than_one(Bitboard b) { return b & (b - 1); } @@ -150,11 +155,6 @@ constexpr Bitboard pawn_attacks_bb(Bitboard b) { : shift(b) | shift(b); } -inline Bitboard pawn_attacks_bb(Color c, Square s) { - - assert(is_ok(s)); - return PawnAttacks[c][s]; -} // Returns a bitboard representing an entire line (from board edge // to board edge) that intersects the two given squares. If the given squares @@ -180,11 +180,6 @@ inline Bitboard between_bb(Square s1, Square s2) { return BetweenBB[s1][s2]; } -// Returns true if the squares s1, s2 and s3 are aligned either on a -// straight or on a diagonal line. -inline bool aligned(Square s1, Square s2, Square s3) { return line_bb(s1, s2) & s3; } - - // distance() functions return the distance between x and y, defined as the // number of steps for a king in x to reach y. @@ -208,68 +203,23 @@ inline int distance(Square x, Square y) { inline int edge_distance(File f) { return std::min(f, File(FILE_H - f)); } -// Returns the pseudo attacks of the given piece type -// assuming an empty board. -template -inline Bitboard attacks_bb(Square s) { - assert((Pt != PAWN) && (is_ok(s))); - return PseudoAttacks[Pt][s]; +constexpr int constexpr_popcount(Bitboard b) { + b = b - ((b >> 1) & 0x5555555555555555ULL); + b = (b & 0x3333333333333333ULL) + ((b >> 2) & 0x3333333333333333ULL); + b = (b + (b >> 4)) & 0x0F0F0F0F0F0F0F0FULL; + return static_cast((b * 0x0101010101010101ULL) >> 56); } - -// Returns the attacks by the given piece -// assuming the board is occupied according to the passed Bitboard. -// Sliding piece attacks do not continue passed an occupied square. -template -inline Bitboard attacks_bb(Square s, Bitboard occupied) { - - assert((Pt != PAWN) && (is_ok(s))); - - switch (Pt) - { - case BISHOP : - return BishopMagics[s].attacks[BishopMagics[s].index(occupied)]; - case ROOK : - return RookMagics[s].attacks[RookMagics[s].index(occupied)]; - case QUEEN : - return attacks_bb(s, occupied) | attacks_bb(s, occupied); - default : - return PseudoAttacks[Pt][s]; - } -} - -// Returns the attacks by the given piece -// assuming the board is occupied according to the passed Bitboard. -// Sliding piece attacks do not continue passed an occupied square. -inline Bitboard attacks_bb(PieceType pt, Square s, Bitboard occupied) { - - assert((pt != PAWN) && (is_ok(s))); - - switch (pt) - { - case BISHOP : - return attacks_bb(s, occupied); - case ROOK : - return attacks_bb(s, occupied); - case QUEEN : - return attacks_bb(s, occupied) | attacks_bb(s, occupied); - default : - return PseudoAttacks[pt][s]; - } -} - - // Counts the number of non-zero bits in a bitboard. inline int popcount(Bitboard b) { #ifndef USE_POPCNT - union { - Bitboard bb; - uint16_t u[4]; - } v = {b}; - return PopCnt16[v.u[0]] + PopCnt16[v.u[1]] + PopCnt16[v.u[2]] + PopCnt16[v.u[3]]; + std::uint16_t indices[4]; + std::memcpy(indices, &b, sizeof(b)); + return PopCnt16[indices[0]] + PopCnt16[indices[1]] + PopCnt16[indices[2]] + + PopCnt16[indices[3]]; #elif defined(_MSC_VER) @@ -366,6 +316,153 @@ inline Square pop_lsb(Bitboard& b) { return s; } +namespace Bitboards { +// Returns the bitboard of target square for the given step +// from the given square. If the step is off the board, returns empty bitboard. +constexpr Bitboard safe_destination(Square s, int step) { + constexpr auto abs = [](int v) { return v < 0 ? -v : v; }; + Square to = Square(s + step); + return is_ok(to) && abs(file_of(s) - file_of(to)) <= 2 ? square_bb(to) : Bitboard(0); +} + +constexpr Bitboard sliding_attack(PieceType pt, Square sq, Bitboard occupied) { + Bitboard attacks = 0; + Direction RookDirections[4] = {NORTH, SOUTH, EAST, WEST}; + Direction BishopDirections[4] = {NORTH_EAST, SOUTH_EAST, SOUTH_WEST, NORTH_WEST}; + + for (Direction d : (pt == ROOK ? RookDirections : BishopDirections)) + { + Square s = sq; + while (safe_destination(s, d)) + { + attacks |= (s += d); + if (occupied & s) + { + break; + } + } + } + + return attacks; +} + +constexpr Bitboard knight_attack(Square sq) { + Bitboard b = {}; + for (int step : {-17, -15, -10, -6, 6, 10, 15, 17}) + b |= safe_destination(sq, step); + return b; +} + +constexpr Bitboard king_attack(Square sq) { + Bitboard b = {}; + for (int step : {-9, -8, -7, -1, 1, 7, 8, 9}) + b |= safe_destination(sq, step); + return b; +} + +constexpr Bitboard pseudo_attacks(PieceType pt, Square sq) { + switch (pt) + { + case PieceType::ROOK : + case PieceType::BISHOP : + return sliding_attack(pt, sq, 0); + case PieceType::QUEEN : + return sliding_attack(PieceType::ROOK, sq, 0) | sliding_attack(PieceType::BISHOP, sq, 0); + case PieceType::KNIGHT : + return knight_attack(sq); + case PieceType::KING : + return king_attack(sq); + default : + assert(false); + return 0; + } +} + +} + +inline constexpr auto PseudoAttacks = []() constexpr { + std::array, PIECE_TYPE_NB> attacks{}; + + for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1) + { + attacks[WHITE][s1] = pawn_attacks_bb(square_bb(s1)); + attacks[BLACK][s1] = pawn_attacks_bb(square_bb(s1)); + + attacks[KING][s1] = Bitboards::pseudo_attacks(KING, s1); + attacks[KNIGHT][s1] = Bitboards::pseudo_attacks(KNIGHT, s1); + attacks[QUEEN][s1] = attacks[BISHOP][s1] = Bitboards::pseudo_attacks(BISHOP, s1); + attacks[QUEEN][s1] |= attacks[ROOK][s1] = Bitboards::pseudo_attacks(ROOK, s1); + } + + return attacks; +}(); + + +// Returns the pseudo attacks of the given piece type +// assuming an empty board. +template +inline Bitboard attacks_bb(Square s, Color c = COLOR_NB) { + + assert((Pt != PAWN || c < COLOR_NB) && (is_ok(s))); + return Pt == PAWN ? PseudoAttacks[c][s] : PseudoAttacks[Pt][s]; +} + + +// Returns the attacks by the given piece +// assuming the board is occupied according to the passed Bitboard. +// Sliding piece attacks do not continue passed an occupied square. +template +inline Bitboard attacks_bb(Square s, Bitboard occupied) { + + assert((Pt != PAWN) && (is_ok(s))); + + switch (Pt) + { + case BISHOP : + case ROOK : + return Magics[s][Pt - BISHOP].attacks_bb(occupied); + case QUEEN : + return attacks_bb(s, occupied) | attacks_bb(s, occupied); + default : + return PseudoAttacks[Pt][s]; + } +} + +// Returns the attacks by the given piece +// assuming the board is occupied according to the passed Bitboard. +// Sliding piece attacks do not continue passed an occupied square. +inline Bitboard attacks_bb(PieceType pt, Square s, Bitboard occupied) { + + assert((pt != PAWN) && (is_ok(s))); + + switch (pt) + { + case BISHOP : + return attacks_bb(s, occupied); + case ROOK : + return attacks_bb(s, occupied); + case QUEEN : + return attacks_bb(s, occupied) | attacks_bb(s, occupied); + default : + return PseudoAttacks[pt][s]; + } +} + +inline Bitboard attacks_bb(Piece pc, Square s) { + if (type_of(pc) == PAWN) + return PseudoAttacks[color_of(pc)][s]; + + return PseudoAttacks[type_of(pc)][s]; +} + + +inline Bitboard attacks_bb(Piece pc, Square s, Bitboard occupied) { + if (type_of(pc) == PAWN) + return PseudoAttacks[color_of(pc)][s]; + + return attacks_bb(type_of(pc), s, occupied); +} + } // namespace Stockfish #endif // #ifndef BITBOARD_H_INCLUDED diff --git a/src/cluster.cpp b/src/cluster.cpp index 939345ac6..8657617d1 100644 --- a/src/cluster.cpp +++ b/src/cluster.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2020 The Stockfish developers (see AUTHORS file) + 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 @@ -35,7 +35,7 @@ #include "search.h" namespace Stockfish { -namespace Cluster { +namespace Distributed { // Total number of ranks and rank within the communicator static int world_rank = MPI_PROC_NULL; @@ -297,7 +297,7 @@ void cluster_info(const ThreadPool& threads, Depth depth, TimePoint elapsed) { void save(TranspositionTable& TT, ThreadPool& threads, Search::Worker* thread, - TTEntry* tte, + TTWriter ttWriter, Key k, Value v, bool PvHit, @@ -308,7 +308,7 @@ void save(TranspositionTable& TT, uint8_t generation8) { // Standard save to the TT - tte->save(k, v, PvHit, b, d, m, ev, generation8); + 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) @@ -321,7 +321,7 @@ void save(TranspositionTable& TT, // prepares the send buffer. { std::lock_guard lk(thread->ttCache.mutex); - thread->ttCache.buffer.replace(KeyedTTEntry(k, *tte)); + thread->ttCache.buffer.replace(KeyedTTEntry(k, TTData(m, v, ev, d, b, PvHit))); ++TTCacheCounter; } @@ -364,13 +364,11 @@ void save(TranspositionTable& TT, for (size_t i = irank * recvBuffPerRankSize; i < (irank + 1) * recvBuffPerRankSize; ++i) { - auto&& e = TTSendRecvBuffs[sendRecvPosted % 2][i]; - bool found; - TTEntry* replace_tte; - replace_tte = TT.probe(e.first, found); - replace_tte->save(e.first, e.second.value(), e.second.is_pv(), - e.second.bound(), e.second.depth(), e.second.move(), - e.second.eval(), TT.generation()); + 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()); } } @@ -387,7 +385,7 @@ void save(TranspositionTable& TT, /// 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::string& PVLine) { +void pick_moves(MoveInfo& mi, std::vector>& serializedInfo) { MoveInfo* pMoveInfo = NULL; if (is_root()) @@ -427,21 +425,33 @@ void pick_moves(MoveInfo& mi, std::string& PVLine) { // Send PV line to root as needed if (mi.rank != 0 && mi.rank == rank()) { - int size; - std::vector vec; - vec.assign(PVLine.begin(), PVLine.end()); - size = vec.size(); - MPI_Send(&size, 1, MPI_INT, 0, 42, MoveComm); - MPI_Send(vec.data(), size, MPI_CHAR, 0, 42, MoveComm); + 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()) { - int size; - std::vector 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); - PVLine.assign(vec.begin(), vec.end()); + 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 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)); + } } } @@ -466,7 +476,7 @@ uint64_t TT_saves(const ThreadPool& threads) { return TTsavesOthers + threads.TT #include "thread.h" namespace Stockfish { -namespace Cluster { +namespace Distributed { uint64_t nodes_searched(const ThreadPool& threads) { return threads.nodes_searched(); } diff --git a/src/cluster.h b/src/cluster.h index 093049c6c..27fdcf0b3 100644 --- a/src/cluster.h +++ b/src/cluster.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2020 The Stockfish developers (see AUTHORS file) + 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 @@ -24,6 +24,7 @@ #include #include +#include "misc.h" #include "tt.h" namespace Stockfish { @@ -34,7 +35,7 @@ namespace Search { class Worker; } -/// The Cluster namespace contains functionality required to run on distributed +/// 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, @@ -44,7 +45,7 @@ class Worker; /// 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 Cluster { +namespace Distributed { /// Basic info to find the cluster-wide bestMove struct MoveInfo { @@ -57,8 +58,8 @@ struct MoveInfo { #ifdef USE_MPI -// store the TTEntry with its full key, so it can be saved on the receiver side -using KeyedTTEntry = std::pair; +// store the TTData with its (full) key, so it can be saved on the receiver side +using KeyedTTEntry = std::pair; constexpr std::size_t TTCacheSize = 16; // Threads locally cache their high-depth TT entries till a batch can be send by MPI @@ -67,7 +68,7 @@ class TTCache: public std::array { struct Compare { inline bool operator()(const KeyedTTEntry& lhs, const KeyedTTEntry& rhs) { - return lhs.second.depth() > rhs.second.depth(); + return lhs.second.depth > rhs.second.depth; } }; Compare compare; @@ -96,7 +97,7 @@ inline bool is_root() { return rank() == 0; } void save(TranspositionTable&, ThreadPool&, Search::Worker* thread, - TTEntry* tte, + TTWriter ttWriter, Key k, Value v, bool PvHit, @@ -105,7 +106,7 @@ void save(TranspositionTable&, Move m, Value ev, uint8_t generation8); -void pick_moves(MoveInfo& mi, std::string& PVLine); +void pick_moves(MoveInfo& mi, std::vector>& PVLine); void ttSendRecvBuff_resize(size_t nThreads); uint64_t nodes_searched(const ThreadPool&); uint64_t tb_hits(const ThreadPool&); @@ -128,7 +129,7 @@ constexpr bool is_root() { return true; } inline void save(TranspositionTable&, ThreadPool&, Search::Worker*, - TTEntry* tte, + TTWriter ttWriter, Key k, Value v, bool PvHit, @@ -137,17 +138,17 @@ inline void save(TranspositionTable&, Move m, Value ev, uint8_t generation8) { - tte->save(k, v, PvHit, b, d, m, ev, generation8); + ttWriter.write(k, v, PvHit, b, d, m, ev, generation8); } -inline void pick_moves(MoveInfo&, std::string&) {} +inline void pick_moves(MoveInfo&, std::vector>&) {} 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& threads) {} -inline void signals_sync(ThreadPool& threads) {} +inline void signals_poll(ThreadPool&) {} +inline void signals_sync(ThreadPool&) {} #endif /* USE_MPI */ diff --git a/src/engine.cpp b/src/engine.cpp new file mode 100644 index 000000000..a315578b8 --- /dev/null +++ b/src/engine.cpp @@ -0,0 +1,411 @@ +/* + 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 . +*/ + +#include "engine.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "evaluate.h" +#include "misc.h" +#include "nnue/network.h" +#include "nnue/nnue_common.h" +#include "nnue/nnue_misc.h" +#include "numa.h" +#include "perft.h" +#include "position.h" +#include "search.h" +#include "shm.h" +#include "syzygy/tbprobe.h" +#include "types.h" +#include "uci.h" +#include "ucioption.h" + +namespace Stockfish { + +namespace NN = Eval::NNUE; + +constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; +constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048; +int MaxThreads = std::max(1024, 4 * int(get_hardware_concurrency())); + +Engine::Engine(std::optional path) : + binaryDirectory(path ? CommandLine::get_binary_directory(*path) : ""), + numaContext(NumaConfig::from_system()), + states(new std::deque(1)), + threads(), + networks( + numaContext, + // Heap-allocate because sizeof(NN::Networks) is large + std::make_unique( + std::make_unique(NN::EvalFile{EvalFileDefaultNameBig, "None", ""}, + NN::EmbeddedNNUEType::BIG), + std::make_unique(NN::EvalFile{EvalFileDefaultNameSmall, "None", ""}, + NN::EmbeddedNNUEType::SMALL))) { + + pos.set(StartFEN, false, &states->back()); + + options.add( // + "Debug Log File", Option("", [](const Option& o) { + start_logger(o); + return std::nullopt; + })); + + options.add( // + "NumaPolicy", Option("auto", [this](const Option& o) { + set_numa_config_from_option(o); + return numa_config_information_as_string() + "\n" + + thread_allocation_information_as_string(); + })); + + options.add( // + "Threads", Option(1, 1, MaxThreads, [this](const Option&) { + resize_threads(); + return thread_allocation_information_as_string(); + })); + + options.add( // + "Hash", Option(16, 1, MaxHashMB, [this](const Option& o) { + set_tt_size(o); + return std::nullopt; + })); + + options.add( // + "Clear Hash", Option([this](const Option&) { + search_clear(); + return std::nullopt; + })); + + options.add( // + "Ponder", Option(false)); + + options.add( // + "MultiPV", Option(1, 1, MAX_MOVES)); + + options.add("Skill Level", Option(20, 0, 20)); + + options.add("Move Overhead", Option(10, 0, 5000)); + + options.add("nodestime", Option(0, 0, 10000)); + + options.add("UCI_Chess960", Option(false)); + + options.add("UCI_LimitStrength", Option(false)); + + options.add("UCI_Elo", + Option(Stockfish::Search::Skill::LowestElo, Stockfish::Search::Skill::LowestElo, + Stockfish::Search::Skill::HighestElo)); + + options.add("UCI_ShowWDL", Option(false)); + + options.add( // + "SyzygyPath", Option("", [](const Option& o) { + Tablebases::init(o); + return std::nullopt; + })); + + options.add("SyzygyProbeDepth", Option(1, 1, 100)); + + options.add("Syzygy50MoveRule", Option(true)); + + options.add("SyzygyProbeLimit", Option(7, 0, 7)); + + options.add( // + "EvalFile", Option(EvalFileDefaultNameBig, [this](const Option& o) { + load_big_network(o); + return std::nullopt; + })); + + options.add( // + "EvalFileSmall", Option(EvalFileDefaultNameSmall, [this](const Option& o) { + load_small_network(o); + return std::nullopt; + })); + + load_networks(); + resize_threads(); +} + +std::uint64_t Engine::perft(const std::string& fen, Depth depth, bool isChess960) { + verify_networks(); + + return Benchmark::perft(fen, depth, isChess960); +} + +void Engine::go(Search::LimitsType& limits) { + assert(limits.perft == 0); + verify_networks(); + + threads.start_thinking(options, pos, states, limits); +} +void Engine::stop() { threads.stop = true; } + +void Engine::search_clear() { + wait_for_search_finished(); + + tt.clear(threads); + threads.clear(); + + // @TODO wont work with multiple instances + Tablebases::init(options["SyzygyPath"]); // Free mapped files +} + +void Engine::set_on_update_no_moves(std::function&& f) { + updateContext.onUpdateNoMoves = std::move(f); +} + +void Engine::set_on_update_full(std::function&& f) { + updateContext.onUpdateFull = std::move(f); +} + +void Engine::set_on_iter(std::function&& f) { + updateContext.onIter = std::move(f); +} + +void Engine::set_on_bestmove(std::function&& f) { + updateContext.onBestmove = std::move(f); +} + +void Engine::set_on_verify_networks(std::function&& f) { + onVerifyNetworks = std::move(f); +} + +void Engine::wait_for_search_finished() { threads.main_thread()->wait_for_search_finished(); } + +void Engine::set_position(const std::string& fen, const std::vector& moves) { + // Drop the old state and create a new one + states = StateListPtr(new std::deque(1)); + pos.set(fen, options["UCI_Chess960"], &states->back()); + + for (const auto& move : moves) + { + auto m = UCIEngine::to_move(pos, move); + + if (m == Move::none()) + break; + + states->emplace_back(); + pos.do_move(m, states->back()); + } +} + +// modifiers + +void Engine::set_numa_config_from_option(const std::string& o) { + if (o == "auto" || o == "system") + { + numaContext.set_numa_config(NumaConfig::from_system()); + } + else if (o == "hardware") + { + // Don't respect affinity set in the system. + numaContext.set_numa_config(NumaConfig::from_system(false)); + } + else if (o == "none") + { + numaContext.set_numa_config(NumaConfig{}); + } + else + { + numaContext.set_numa_config(NumaConfig::from_string(o)); + } + + // Force reallocation of threads in case affinities need to change. + resize_threads(); + threads.ensure_network_replicated(); +} + +void Engine::resize_threads() { + threads.wait_for_search_finished(); + threads.set(numaContext.get_numa_config(), {options, threads, tt, sharedHists, networks}, + updateContext); + + // Reallocate the hash with the new threadpool size + set_tt_size(options["Hash"]); + threads.ensure_network_replicated(); +} + +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; } + +// network related + +void Engine::verify_networks() const { + networks->big.verify(options["EvalFile"], onVerifyNetworks); + networks->small.verify(options["EvalFileSmall"], onVerifyNetworks); + + auto statuses = networks.get_status_and_errors(); + for (size_t i = 0; i < statuses.size(); ++i) + { + const auto [status, error] = statuses[i]; + std::string message = "Network replica " + std::to_string(i + 1) + ": "; + if (status == SystemWideSharedConstantAllocationStatus::NoAllocation) + { + message += "No allocation."; + } + else if (status == SystemWideSharedConstantAllocationStatus::LocalMemory) + { + message += "Local memory."; + } + else if (status == SystemWideSharedConstantAllocationStatus::SharedMemory) + { + message += "Shared memory."; + } + else + { + message += "Unknown status."; + } + + if (error.has_value()) + { + message += " " + *error; + } + + onVerifyNetworks(message); + } +} + +void Engine::load_networks() { + networks.modify_and_replicate([this](NN::Networks& networks_) { + networks_.big.load(binaryDirectory, options["EvalFile"]); + networks_.small.load(binaryDirectory, options["EvalFileSmall"]); + }); + threads.clear(); + threads.ensure_network_replicated(); +} + +void Engine::load_big_network(const std::string& file) { + networks.modify_and_replicate( + [this, &file](NN::Networks& networks_) { networks_.big.load(binaryDirectory, file); }); + threads.clear(); + threads.ensure_network_replicated(); +} + +void Engine::load_small_network(const std::string& file) { + networks.modify_and_replicate( + [this, &file](NN::Networks& networks_) { networks_.small.load(binaryDirectory, file); }); + threads.clear(); + threads.ensure_network_replicated(); +} + +void Engine::save_network(const std::pair, std::string> files[2]) { + networks.modify_and_replicate([&files](NN::Networks& networks_) { + networks_.big.save(files[0].first); + networks_.small.save(files[1].first); + }); +} + +// utility functions + +void Engine::trace_eval() const { + StateListPtr trace_states(new std::deque(1)); + Position p; + p.set(pos.fen(), options["UCI_Chess960"], &trace_states->back()); + + verify_networks(); + + sync_cout << "\n" << Eval::trace(p, *networks) << sync_endl; +} + +const OptionsMap& Engine::get_options() const { return options; } +OptionsMap& Engine::get_options() { return options; } + +std::string Engine::fen() const { return pos.fen(); } + +void Engine::flip() { pos.flip(); } + +std::string Engine::visualize() const { + std::stringstream ss; + ss << pos; + return ss.str(); +} + +int Engine::get_hashfull(int maxAge) const { return tt.hashfull(maxAge); } + +std::vector> Engine::get_bound_thread_count_by_numa_node() const { + auto counts = threads.get_bound_thread_count_by_numa_node(); + const NumaConfig& cfg = numaContext.get_numa_config(); + std::vector> ratios; + NumaIndex n = 0; + for (; n < counts.size(); ++n) + ratios.emplace_back(counts[n], cfg.num_cpus_in_numa_node(n)); + if (!counts.empty()) + for (; n < cfg.num_numa_nodes(); ++n) + ratios.emplace_back(0, cfg.num_cpus_in_numa_node(n)); + return ratios; +} + +std::string Engine::get_numa_config_as_string() const { + return numaContext.get_numa_config().to_string(); +} + +std::string Engine::numa_config_information_as_string() const { + auto cfgStr = get_numa_config_as_string(); + return "Available processors: " + cfgStr; +} + +std::string Engine::thread_binding_information_as_string() const { + auto boundThreadsByNode = get_bound_thread_count_by_numa_node(); + std::stringstream ss; + if (boundThreadsByNode.empty()) + return ss.str(); + + bool isFirst = true; + + for (auto&& [current, total] : boundThreadsByNode) + { + if (!isFirst) + ss << ":"; + ss << current << "/" << total; + isFirst = false; + } + + return ss.str(); +} + +std::string Engine::thread_allocation_information_as_string() const { + std::stringstream ss; + + size_t threadsSize = threads.size(); + ss << "Using " << threadsSize << (threadsSize > 1 ? " threads" : " thread"); + + auto boundThreadsByNodeStr = thread_binding_information_as_string(); + if (boundThreadsByNodeStr.empty()) + return ss.str(); + + ss << " with NUMA node thread binding: "; + ss << boundThreadsByNodeStr; + + return ss.str(); +} +} diff --git a/src/engine.h b/src/engine.h new file mode 100644 index 000000000..10c92d759 --- /dev/null +++ b/src/engine.h @@ -0,0 +1,133 @@ +/* + 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 . +*/ + +#ifndef ENGINE_H_INCLUDED +#define ENGINE_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "history.h" +#include "nnue/network.h" +#include "numa.h" +#include "position.h" +#include "search.h" +#include "syzygy/tbprobe.h" // for Stockfish::Depth +#include "thread.h" +#include "tt.h" +#include "ucioption.h" + +namespace Stockfish { + +class Engine { + public: + using InfoShort = Search::InfoShort; + using InfoFull = Search::InfoFull; + using InfoIter = Search::InfoIteration; + + Engine(std::optional path = std::nullopt); + + // Cannot be movable due to components holding backreferences to fields + Engine(const Engine&) = delete; + Engine(Engine&&) = delete; + Engine& operator=(const Engine&) = delete; + Engine& operator=(Engine&&) = delete; + + ~Engine() { wait_for_search_finished(); } + + std::uint64_t perft(const std::string& fen, Depth depth, bool isChess960); + + // non blocking call to start searching + void go(Search::LimitsType&); + // non blocking call to stop searching + void stop(); + + // blocking call to wait for search to finish + void wait_for_search_finished(); + // set a new position, moves are in UCI format + void set_position(const std::string& fen, const std::vector& moves); + + // modifiers + + void set_numa_config_from_option(const std::string& o); + void resize_threads(); + void set_tt_size(size_t mb); + void set_ponderhit(bool); + void search_clear(); + + void set_on_update_no_moves(std::function&&); + void set_on_update_full(std::function&&); + void set_on_iter(std::function&&); + void set_on_bestmove(std::function&&); + void set_on_verify_networks(std::function&&); + + // network related + + void verify_networks() const; + void load_networks(); + void load_big_network(const std::string& file); + void load_small_network(const std::string& file); + void save_network(const std::pair, std::string> files[2]); + + // utility functions + + void trace_eval() const; + + const OptionsMap& get_options() const; + OptionsMap& get_options(); + + int get_hashfull(int maxAge = 0) const; + + std::string fen() const; + void flip(); + std::string visualize() const; + std::vector> get_bound_thread_count_by_numa_node() const; + std::string get_numa_config_as_string() const; + std::string numa_config_information_as_string() const; + std::string thread_allocation_information_as_string() const; + std::string thread_binding_information_as_string() const; + + private: + const std::string binaryDirectory; + + NumaReplicationContext numaContext; + + Position pos; + StateListPtr states; + + OptionsMap options; + ThreadPool threads; + TranspositionTable tt; + LazyNumaReplicatedSystemWide networks; + + Search::SearchManager::UpdateContext updateContext; + std::function onVerifyNetworks; + std::map sharedHists; +}; + +} // namespace Stockfish + + +#endif // #ifndef ENGINE_H_INCLUDED diff --git a/src/evaluate.cpp b/src/evaluate.cpp index bc705b857..745bd3e4d 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -24,63 +24,64 @@ #include #include #include +#include #include +#include #include "nnue/network.h" #include "nnue/nnue_misc.h" #include "position.h" #include "types.h" #include "uci.h" +#include "nnue/nnue_accumulator.h" namespace Stockfish { // Returns a static, purely materialistic evaluation of the position from -// the point of view of the given color. It can be divided by PawnValue to get +// the point of view of the side to move. It can be divided by PawnValue to get // an approximation of the material advantage on the board in terms of pawns. -int Eval::simple_eval(const Position& pos, Color c) { - return PawnValue * (pos.count(c) - pos.count(~c)) - + (pos.non_pawn_material(c) - pos.non_pawn_material(~c)); +int Eval::simple_eval(const Position& pos) { + Color c = pos.side_to_move(); + return PawnValue * (pos.count(c) - pos.count(~c)) + pos.non_pawn_material(c) + - pos.non_pawn_material(~c); } +bool Eval::use_smallnet(const Position& pos) { return std::abs(simple_eval(pos)) > 962; } // Evaluate is the evaluator for the outer world. It returns a static evaluation // of the position from the point of view of the side to move. -Value Eval::evaluate(const Eval::NNUE::Networks& networks, const Position& pos, int optimism) { +Value Eval::evaluate(const Eval::NNUE::Networks& networks, + const Position& pos, + Eval::NNUE::AccumulatorStack& accumulators, + Eval::NNUE::AccumulatorCaches& caches, + int optimism) { assert(!pos.checkers()); - int simpleEval = simple_eval(pos, pos.side_to_move()); - bool smallNet = std::abs(simpleEval) > SmallNetThreshold; - bool psqtOnly = std::abs(simpleEval) > PsqtOnlyThreshold; - int nnueComplexity; - int v; + bool smallNet = use_smallnet(pos); + auto [psqt, positional] = smallNet ? networks.small.evaluate(pos, accumulators, caches.small) + : networks.big.evaluate(pos, accumulators, caches.big); - Value nnue = smallNet ? networks.small.evaluate(pos, true, &nnueComplexity, psqtOnly) - : networks.big.evaluate(pos, true, &nnueComplexity, false); + Value nnue = (125 * psqt + 131 * positional) / 128; - const auto adjustEval = [&](int optDiv, int nnueDiv, int pawnCountConstant, int pawnCountMul, - int npmConstant, int evalDiv, int shufflingConstant, - int shufflingDiv) { - // Blend optimism and eval with nnue complexity and material imbalance - optimism += optimism * (nnueComplexity + std::abs(simpleEval - nnue)) / optDiv; - nnue -= nnue * (nnueComplexity * 5 / 3) / nnueDiv; + // Re-evaluate the position when higher eval accuracy is worth the time spent + if (smallNet && (std::abs(nnue) < 277)) + { + std::tie(psqt, positional) = networks.big.evaluate(pos, accumulators, caches.big); + nnue = (125 * psqt + 131 * positional) / 128; + smallNet = false; + } - int npm = pos.non_pawn_material() / 64; - v = (nnue * (npm + pawnCountConstant + pawnCountMul * pos.count()) - + optimism * (npmConstant + npm)) - / evalDiv; + // Blend optimism and eval with nnue complexity + int nnueComplexity = std::abs(psqt - positional); + optimism += optimism * nnueComplexity / 476; + nnue -= nnue * nnueComplexity / 18236; - // Damp down the evaluation linearly when shuffling - int shuffling = pos.rule50_count(); - v = v * (shufflingConstant - shuffling) / shufflingDiv; - }; + int material = 534 * pos.count() + pos.non_pawn_material(); + int v = (nnue * (77871 + material) + optimism * (7191 + material)) / 77871; - if (!smallNet) - adjustEval(513, 32395, 919, 11, 145, 1036, 178, 204); - else if (psqtOnly) - adjustEval(517, 32857, 908, 7, 155, 1019, 224, 238); - else - adjustEval(499, 32793, 903, 9, 147, 1067, 208, 211); + // Damp down the evaluation linearly when shuffling + v -= v * pos.rule50_count() / 199; // Guarantee evaluation does not hit the tablebase range v = std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); @@ -97,19 +98,23 @@ std::string Eval::trace(Position& pos, const Eval::NNUE::Networks& networks) { if (pos.checkers()) return "Final evaluation: none (in check)"; + auto accumulators = std::make_unique(); + auto caches = std::make_unique(networks); + std::stringstream ss; ss << std::showpoint << std::noshowpos << std::fixed << std::setprecision(2); - ss << '\n' << NNUE::trace(pos, networks) << '\n'; + ss << '\n' << NNUE::trace(pos, networks, *caches) << '\n'; ss << std::showpoint << std::showpos << std::fixed << std::setprecision(2) << std::setw(15); - Value v = networks.big.evaluate(pos, false); - v = pos.side_to_move() == WHITE ? v : -v; - ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v, pos) << " (white side)\n"; + auto [psqt, positional] = networks.big.evaluate(pos, *accumulators, caches->big); + Value v = psqt + positional; + v = pos.side_to_move() == WHITE ? v : -v; + ss << "NNUE evaluation " << 0.01 * UCIEngine::to_cp(v, pos) << " (white side)\n"; - v = evaluate(networks, pos, VALUE_ZERO); + v = evaluate(networks, pos, *accumulators, *caches, VALUE_ZERO); v = pos.side_to_move() == WHITE ? v : -v; - ss << "Final evaluation " << 0.01 * UCI::to_cp(v, pos) << " (white side)"; + ss << "Final evaluation " << 0.01 * UCIEngine::to_cp(v, pos) << " (white side)"; ss << " [with scaled NNUE, ...]"; ss << "\n"; diff --git a/src/evaluate.h b/src/evaluate.h index 97ca4c4b6..b4f54a381 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -29,25 +29,28 @@ class Position; namespace Eval { -constexpr inline int SmallNetThreshold = 1165, PsqtOnlyThreshold = 2500; - // The default net name MUST follow the format nn-[SHA256 first 12 digits].nnue // for the build process (profile-build and fishtest) to work. Do not change the // name of the macro or the location where this macro is defined, as it is used // in the Makefile/Fishtest. -#define EvalFileDefaultNameBig "nn-ae6a388e4a1a.nnue" -#define EvalFileDefaultNameSmall "nn-baff1ede1f90.nnue" +#define EvalFileDefaultNameBig "nn-c288c895ea92.nnue" +#define EvalFileDefaultNameSmall "nn-37f18f62d772.nnue" namespace NNUE { struct Networks; +struct AccumulatorCaches; +class AccumulatorStack; } std::string trace(Position& pos, const Eval::NNUE::Networks& networks); -int simple_eval(const Position& pos, Color c); -Value evaluate(const NNUE::Networks& networks, const Position& pos, int optimism); - - +int simple_eval(const Position& pos); +bool use_smallnet(const Position& pos); +Value evaluate(const NNUE::Networks& networks, + const Position& pos, + Eval::NNUE::AccumulatorStack& accumulators, + Eval::NNUE::AccumulatorCaches& caches, + int optimism); } // namespace Eval } // namespace Stockfish diff --git a/src/history.h b/src/history.h new file mode 100644 index 000000000..c98a7ee22 --- /dev/null +++ b/src/history.h @@ -0,0 +1,273 @@ +/* + 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 . +*/ + +#ifndef HISTORY_H_INCLUDED +#define HISTORY_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include // IWYU pragma: keep + +#include "memory.h" +#include "misc.h" +#include "position.h" + +namespace Stockfish { + +constexpr int PAWN_HISTORY_BASE_SIZE = 8192; // has to be a power of 2 +constexpr int UINT_16_HISTORY_SIZE = std::numeric_limits::max() + 1; +constexpr int CORRHIST_BASE_SIZE = UINT_16_HISTORY_SIZE; +constexpr int CORRECTION_HISTORY_LIMIT = 1024; +constexpr int LOW_PLY_HISTORY_SIZE = 5; + +static_assert((PAWN_HISTORY_BASE_SIZE & (PAWN_HISTORY_BASE_SIZE - 1)) == 0, + "PAWN_HISTORY_BASE_SIZE has to be a power of 2"); + +static_assert((CORRHIST_BASE_SIZE & (CORRHIST_BASE_SIZE - 1)) == 0, + "CORRHIST_BASE_SIZE has to be a power of 2"); + +// StatsEntry is the container of various numerical statistics. We use a class +// instead of a naked value to directly call history update operator<<() on +// the entry. The first template parameter T is the base type of the array, +// and the second template parameter D limits the range of updates in [-D, D] +// when we update values with the << operator +template +struct StatsEntry { + static_assert(std::is_arithmetic_v, "Not an arithmetic type"); + + private: + std::conditional_t, T> entry; + + public: + void operator=(const T& v) { + if constexpr (Atomic) + entry.store(v, std::memory_order_relaxed); + else + entry = v; + } + + operator T() const { + if constexpr (Atomic) + return entry.load(std::memory_order_relaxed); + else + return entry; + } + + void operator<<(int bonus) { + // Make sure that bonus is in range [-D, D] + int clampedBonus = std::clamp(bonus, -D, D); + T val = *this; + *this = val + clampedBonus - val * std::abs(clampedBonus) / D; + + assert(std::abs(T(*this)) <= D); + } +}; + +enum StatsType { + NoCaptures, + Captures +}; + +template +using Stats = MultiArray, Sizes...>; + +template +using AtomicStats = MultiArray, Sizes...>; + +// DynStats is a dynamically sized array of Stats, used for thread-shared histories +// which should scale with the total number of threads. The SizeMultiplier gives +// the per-thread allocation count of T. +template +struct DynStats { + explicit DynStats(size_t s) { + size = s * SizeMultiplier; + data = make_unique_large_page(size); + } + // Sets all values in the range to 0 + void clear_range(int value, size_t threadIdx, size_t numaTotal) { + size_t start = uint64_t(threadIdx) * size / numaTotal; + assert(start < size); + size_t end = threadIdx + 1 == numaTotal ? size : uint64_t(threadIdx + 1) * size / numaTotal; + + while (start < end) + data[start++].fill(value); + } + size_t get_size() const { return size; } + T& operator[](size_t index) { + assert(index < size); + return data.get()[index]; + } + const T& operator[](size_t index) const { + assert(index < size); + return data.get()[index]; + } + + private: + size_t size; + LargePagePtr data; +}; + +// ButterflyHistory records how often quiet moves have been successful or unsuccessful +// during the current search, and is used for reduction and move ordering decisions. +// It uses 2 tables (one for each color) indexed by the move's from and to squares, +// see https://www.chessprogramming.org/Butterfly_Boards +using ButterflyHistory = Stats; + +// LowPlyHistory is addressed by ply and move's from and to squares, used +// to improve move ordering near the root +using LowPlyHistory = Stats; + +// CapturePieceToHistory is addressed by a move's [piece][to][captured piece type] +using CapturePieceToHistory = Stats; + +// PieceToHistory is like ButterflyHistory but is addressed by a move's [piece][to] +using PieceToHistory = Stats; + +// ContinuationHistory is the combined history of a given pair of moves, usually +// the current one given a previous one. The nested history table is based on +// PieceToHistory instead of ButterflyBoards. +using ContinuationHistory = MultiArray; + +// PawnHistory is addressed by the pawn structure and a move's [piece][to] +using PawnHistory = + DynStats, PAWN_HISTORY_BASE_SIZE>; + +// Correction histories record differences between the static evaluation of +// positions and their search score. It is used to improve the static evaluation +// used by some search heuristics. +// see https://www.chessprogramming.org/Static_Evaluation_Correction_History +enum CorrHistType { + Pawn, // By color and pawn structure + Minor, // By color and positions of minor pieces (Knight, Bishop) + NonPawn, // By non-pawn material positions and color + PieceTo, // By [piece][to] move + Continuation, // Combined history of move pairs +}; + +template +struct CorrectionBundle { + StatsEntry pawn; + StatsEntry minor; + StatsEntry nonPawnWhite; + StatsEntry nonPawnBlack; + + void operator=(T val) { + pawn = val; + minor = val; + nonPawnWhite = val; + nonPawnBlack = val; + } +}; + +namespace Detail { + +template +struct CorrHistTypedef { + using type = + DynStats, CORRHIST_BASE_SIZE>; +}; + +template<> +struct CorrHistTypedef { + using type = Stats; +}; + +template<> +struct CorrHistTypedef { + using type = MultiArray::type, PIECE_NB, SQUARE_NB>; +}; + +template<> +struct CorrHistTypedef { + using type = DynStats, + CORRHIST_BASE_SIZE>; +}; + +} + +using UnifiedCorrectionHistory = + DynStats, COLOR_NB>, + CORRHIST_BASE_SIZE>; + +template +using CorrectionHistory = typename Detail::CorrHistTypedef::type; + +using TTMoveHistory = StatsEntry; + +// Set of histories shared between groups of threads. To avoid excessive +// cross-node data transfer, histories are shared only between threads +// on a given NUMA node. The passed size must be a power of two to make +// the indexing more efficient. +struct SharedHistories { + SharedHistories(size_t threadCount) : + correctionHistory(threadCount), + pawnHistory(threadCount) { + assert((threadCount & (threadCount - 1)) == 0 && threadCount != 0); + sizeMinus1 = correctionHistory.get_size() - 1; + pawnHistSizeMinus1 = pawnHistory.get_size() - 1; + } + + size_t get_size() const { return sizeMinus1 + 1; } + + auto& pawn_entry(const Position& pos) { + return pawnHistory[pos.pawn_key() & pawnHistSizeMinus1]; + } + const auto& pawn_entry(const Position& pos) const { + return pawnHistory[pos.pawn_key() & pawnHistSizeMinus1]; + } + + auto& pawn_correction_entry(const Position& pos) { + return correctionHistory[pos.pawn_key() & sizeMinus1]; + } + const auto& pawn_correction_entry(const Position& pos) const { + return correctionHistory[pos.pawn_key() & sizeMinus1]; + } + + auto& minor_piece_correction_entry(const Position& pos) { + return correctionHistory[pos.minor_piece_key() & sizeMinus1]; + } + const auto& minor_piece_correction_entry(const Position& pos) const { + return correctionHistory[pos.minor_piece_key() & sizeMinus1]; + } + + template + auto& nonpawn_correction_entry(const Position& pos) { + return correctionHistory[pos.non_pawn_key(c) & sizeMinus1]; + } + template + const auto& nonpawn_correction_entry(const Position& pos) const { + return correctionHistory[pos.non_pawn_key(c) & sizeMinus1]; + } + + UnifiedCorrectionHistory correctionHistory; + PawnHistory pawnHistory; + + + private: + size_t sizeMinus1, pawnHistSizeMinus1; +}; + +} // namespace Stockfish + +#endif // #ifndef HISTORY_H_INCLUDED diff --git a/src/incbin/incbin.h b/src/incbin/incbin.h index 18718b95f..3f662e15d 100644 --- a/src/incbin/incbin.h +++ b/src/incbin/incbin.h @@ -3,8 +3,8 @@ * @author Dale Weiler * @brief Utility for including binary files * - * Facilities for including binary files into the current translation unit - * and making use of them externally in other translation units. + * Facilities for including binary files into the current translation unit and + * making use from them externally in other translation units. */ #ifndef INCBIN_HDR #define INCBIN_HDR @@ -26,7 +26,9 @@ defined(__SSSE3__) || \ defined(__SSE4_1__) || \ defined(__SSE4_2__) || \ - defined(__neon__) + defined(__neon__) || \ + defined(__ARM_NEON) || \ + defined(__ALTIVEC__) # define INCBIN_ALIGNMENT_INDEX 4 #elif ULONG_MAX != 0xffffffffu # define INCBIN_ALIGNMENT_INDEX 3 @@ -64,6 +66,9 @@ X #define INCBIN_INVOKE(N, ...) \ INCBIN_EVAL(N(__VA_ARGS__)) +/* Variable argument count for overloading by arity */ +#define INCBIN_VA_ARG_COUNTER(_1, _2, _3, N, ...) N +#define INCBIN_VA_ARGC(...) INCBIN_VA_ARG_COUNTER(__VA_ARGS__, 3, 2, 1, 0) /* Green Hills uses a different directive for including binary data */ #if defined(__ghs__) @@ -117,29 +122,50 @@ #endif /** - * @brief Optionally override the linker section into which data is emitted. - * - * @warning If you use this facility, you'll have to deal with platform-specific linker output - * section naming on your own - * - * Overriding the default linker output section, e.g for esp8266/Arduino: - * @code - * #define INCBIN_OUTPUT_SECTION ".irom.text" - * #include "incbin.h" - * INCBIN(Foo, "foo.txt"); - * // Data is emitted into program memory that never gets copied to RAM - * @endcode + * @brief Optionally override the linker section into which size and data is + * emitted. + * + * @warning If you use this facility, you might have to deal with + * platform-specific linker output section naming on your own. */ #if !defined(INCBIN_OUTPUT_SECTION) # if defined(__APPLE__) -# define INCBIN_OUTPUT_SECTION ".const_data" +# define INCBIN_OUTPUT_SECTION ".const_data" # else -# define INCBIN_OUTPUT_SECTION ".rodata" +# define INCBIN_OUTPUT_SECTION ".rodata" # endif #endif +/** + * @brief Optionally override the linker section into which data is emitted. + * + * @warning If you use this facility, you might have to deal with + * platform-specific linker output section naming on your own. + */ +#if !defined(INCBIN_OUTPUT_DATA_SECTION) +# define INCBIN_OUTPUT_DATA_SECTION INCBIN_OUTPUT_SECTION +#endif + +/** + * @brief Optionally override the linker section into which size is emitted. + * + * @warning If you use this facility, you might have to deal with + * platform-specific linker output section naming on your own. + * + * @note This is useful for Harvard architectures where program memory cannot + * be directly read from the program without special instructions. With this you + * can chose to put the size variable in RAM rather than ROM. + */ +#if !defined(INCBIN_OUTPUT_SIZE_SECTION) +# define INCBIN_OUTPUT_SIZE_SECTION INCBIN_OUTPUT_SECTION +#endif + #if defined(__APPLE__) -/* The directives are different for Apple-branded compilers */ +# include "TargetConditionals.h" +# if defined(TARGET_OS_IPHONE) && !defined(INCBIN_SILENCE_BITCODE_WARNING) +# warning "incbin is incompatible with bitcode. Using the library will break upload to App Store if you have bitcode enabled. Add `#define INCBIN_SILENCE_BITCODE_WARNING` before including this header to silence this warning." +# endif +/* The directives are different for Apple branded compilers */ # define INCBIN_SECTION INCBIN_OUTPUT_SECTION "\n" # define INCBIN_GLOBAL(NAME) ".globl " INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME "\n" # define INCBIN_INT ".long " @@ -179,27 +205,17 @@ /** * @brief Specify the prefix to use for symbol names. * - * By default this is `g', producing symbols of the form: - * @code - * #include "incbin.h" - * INCBIN(Foo, "foo.txt"); + * @note By default this is "g". * - * // Now you have the following symbols: - * // const unsigned char gFooData[]; - * // const unsigned char *const gFooEnd; - * // const unsigned int gFooSize; - * @endcode - * - * If however you specify a prefix before including: e.g: * @code * #define INCBIN_PREFIX incbin * #include "incbin.h" * INCBIN(Foo, "foo.txt"); * * // Now you have the following symbols instead: - * // const unsigned char incbinFooData[]; - * // const unsigned char *const incbinFooEnd; - * // const unsigned int incbinFooSize; + * // const unsigned char incbinFoo[]; + * // const unsigned char *const incbinFoo; + * // const unsigned int incbinFoo; * @endcode */ #if !defined(INCBIN_PREFIX) @@ -213,18 +229,8 @@ * - INCBIN_STYLE_CAMEL "CamelCase" * - INCBIN_STYLE_SNAKE "snake_case" * - * Default option is *INCBIN_STYLE_CAMEL* producing symbols of the form: - * @code - * #include "incbin.h" - * INCBIN(Foo, "foo.txt"); + * @note By default this is INCBIN_STYLE_CAMEL * - * // Now you have the following symbols: - * // const unsigned char FooData[]; - * // const unsigned char *const FooEnd; - * // const unsigned int FooSize; - * @endcode - * - * If however you specify a style before including: e.g: * @code * #define INCBIN_STYLE INCBIN_STYLE_SNAKE * #include "incbin.h" @@ -261,8 +267,8 @@ INCBIN_STRINGIZE( \ INCBIN_STYLE_IDENT(TYPE)) \ -/* Generate the global labels by indirectly invoking the macro - * with our style type and concatenate the name against them. */ +/* Generate the global labels by indirectly invoking the macro with our style + * type and concatenating the name against them. */ #define INCBIN_GLOBAL_LABELS(NAME, TYPE) \ INCBIN_INVOKE( \ INCBIN_GLOBAL, \ @@ -288,23 +294,38 @@ * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with * "Data", as well as "End" and "Size" after. An example is provided below. * + * @param TYPE Optional array type. Omitting this picks a default of `unsigned char`. * @param NAME The name given for the binary data * * @code * INCBIN_EXTERN(Foo); * * // Now you have the following symbols: - * // extern const unsigned char FooData[]; - * // extern const unsigned char *const FooEnd; - * // extern const unsigned int FooSize; + * // extern const unsigned char Foo[]; + * // extern const unsigned char *const Foo; + * // extern const unsigned int Foo; + * @endcode + * + * You may specify a custom optional data type as well as the first argument. + * @code + * INCBIN_EXTERN(custom_type, Foo); + * + * // Now you have the following symbols: + * // extern const custom_type Foo[]; + * // extern const custom_type *const Foo; + * // extern const unsigned int Foo; * @endcode */ -#define INCBIN_EXTERN(NAME) \ - INCBIN_EXTERNAL const INCBIN_ALIGN unsigned char \ +#define INCBIN_EXTERN(...) \ + INCBIN_CONCATENATE(INCBIN_EXTERN_, INCBIN_VA_ARGC(__VA_ARGS__))(__VA_ARGS__) +#define INCBIN_EXTERN_1(NAME, ...) \ + INCBIN_EXTERN_2(unsigned char, NAME) +#define INCBIN_EXTERN_2(TYPE, NAME) \ + INCBIN_EXTERNAL const INCBIN_ALIGN TYPE \ INCBIN_CONCATENATE( \ INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ INCBIN_STYLE_IDENT(DATA))[]; \ - INCBIN_EXTERNAL const INCBIN_ALIGN unsigned char *const \ + INCBIN_EXTERNAL const INCBIN_ALIGN TYPE *const \ INCBIN_CONCATENATE( \ INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ INCBIN_STYLE_IDENT(END)); \ @@ -313,6 +334,29 @@ INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ INCBIN_STYLE_IDENT(SIZE)) +/** + * @brief Externally reference textual data included in another translation unit. + * + * Produces three external symbols that reference the textual data included in + * another translation unit. + * + * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with + * "Data", as well as "End" and "Size" after. An example is provided below. + * + * @param NAME The name given for the textual data + * + * @code + * INCBIN_EXTERN(Foo); + * + * // Now you have the following symbols: + * // extern const char Foo[]; + * // extern const char *const Foo; + * // extern const unsigned int Foo; + * @endcode + */ +#define INCTXT_EXTERN(NAME) \ + INCBIN_EXTERN_2(char, NAME) + /** * @brief Include a binary file into the current translation unit. * @@ -322,6 +366,7 @@ * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with * "Data", as well as "End" and "Size" after. An example is provided below. * + * @param TYPE Optional array type. Omitting this picks a default of `unsigned char`. * @param NAME The name to associate with this binary data (as an identifier.) * @param FILENAME The file to include (as a string literal.) * @@ -329,9 +374,20 @@ * INCBIN(Icon, "icon.png"); * * // Now you have the following symbols: - * // const unsigned char IconData[]; - * // const unsigned char *const IconEnd; - * // const unsigned int IconSize; + * // const unsigned char Icon[]; + * // const unsigned char *const Icon; + * // const unsigned int Icon; + * @endcode + * + * You may specify a custom optional data type as well as the first argument. + * These macros are specialized by arity. + * @code + * INCBIN(custom_type, Icon, "icon.png"); + * + * // Now you have the following symbols: + * // const custom_type Icon[]; + * // const custom_type *const Icon; + * // const unsigned int Icon; * @endcode * * @warning This must be used in global scope @@ -341,15 +397,28 @@ * please @see INCBIN_EXTERN. */ #ifdef _MSC_VER -#define INCBIN(NAME, FILENAME) \ - INCBIN_EXTERN(NAME) +# define INCBIN(NAME, FILENAME) \ + INCBIN_EXTERN(NAME) #else -#define INCBIN(NAME, FILENAME) \ +# define INCBIN(...) \ + INCBIN_CONCATENATE(INCBIN_, INCBIN_VA_ARGC(__VA_ARGS__))(__VA_ARGS__) +# if defined(__GNUC__) +# define INCBIN_1(...) _Pragma("GCC error \"Single argument INCBIN not allowed\"") +# elif defined(__clang__) +# define INCBIN_1(...) _Pragma("clang error \"Single argument INCBIN not allowed\"") +# else +# define INCBIN_1(...) /* Cannot do anything here */ +# endif +# define INCBIN_2(NAME, FILENAME) \ + INCBIN_3(unsigned char, NAME, FILENAME) +# define INCBIN_3(TYPE, NAME, FILENAME) INCBIN_COMMON(TYPE, NAME, FILENAME, /* No terminator for binary data */) +# define INCBIN_COMMON(TYPE, NAME, FILENAME, TERMINATOR) \ __asm__(INCBIN_SECTION \ INCBIN_GLOBAL_LABELS(NAME, DATA) \ INCBIN_ALIGN_HOST \ INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(DATA) ":\n" \ INCBIN_MACRO " \"" FILENAME "\"\n" \ + TERMINATOR \ INCBIN_GLOBAL_LABELS(NAME, END) \ INCBIN_ALIGN_BYTE \ INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(END) ":\n" \ @@ -362,7 +431,46 @@ INCBIN_ALIGN_HOST \ ".text\n" \ ); \ - INCBIN_EXTERN(NAME) + INCBIN_EXTERN(TYPE, NAME) +#endif +/** + * @brief Include a textual file into the current translation unit. + * + * This behaves the same as INCBIN except it produces char compatible arrays + * and implicitly adds a null-terminator byte, thus the size of data included + * by this is one byte larger than that of INCBIN. + * + * Includes a textual file into the current translation unit, producing three + * symbols for objects that encode the data and size respectively. + * + * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with + * "Data", as well as "End" and "Size" after. An example is provided below. + * + * @param NAME The name to associate with this binary data (as an identifier.) + * @param FILENAME The file to include (as a string literal.) + * + * @code + * INCTXT(Readme, "readme.txt"); + * + * // Now you have the following symbols: + * // const char Readme[]; + * // const char *const Readme; + * // const unsigned int Readme; + * @endcode + * + * @warning This must be used in global scope + * @warning The identifiers may be different if INCBIN_STYLE is not default + * + * To externally reference the data included by this in another translation unit + * please @see INCBIN_EXTERN. + */ +#if defined(_MSC_VER) +# define INCTXT(NAME, FILENAME) \ + INCBIN_EXTERN(NAME) +#else +# define INCTXT(NAME, FILENAME) \ + INCBIN_COMMON(char, NAME, FILENAME, INCBIN_BYTE "0\n") #endif -#endif + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 0a8a0f302..a1770ed81 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -17,32 +17,31 @@ */ #include +#include #include "bitboard.h" #include "misc.h" #include "position.h" #include "tune.h" -#include "types.h" #include "uci.h" using namespace Stockfish; int main(int argc, char* argv[]) { - - Cluster::init(); - if (Cluster::is_root()) + Distributed::init(); + if (Distributed::is_root()) std::cout << engine_info() << std::endl; Bitboards::init(); Position::init(); - UCI uci(argc, argv); + auto uci = std::make_unique(argc, argv); - Tune::init(uci.options); + Tune::init(uci->engine_options()); - uci.loop(); + uci->loop(); - Cluster::finalize(); + Distributed::finalize(); return 0; } diff --git a/src/memory.cpp b/src/memory.cpp new file mode 100644 index 000000000..94a599399 --- /dev/null +++ b/src/memory.cpp @@ -0,0 +1,199 @@ +/* + 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 . +*/ + +#include "memory.h" + +#include + +#if __has_include("features.h") + #include +#endif + +#if defined(__linux__) && !defined(__ANDROID__) + #include +#endif + +#if defined(__APPLE__) || defined(__ANDROID__) || defined(__OpenBSD__) \ + || (defined(__GLIBCXX__) && !defined(_GLIBCXX_HAVE_ALIGNED_ALLOC) && !defined(_WIN32)) \ + || defined(__e2k__) + #define POSIXALIGNEDALLOC + #include +#endif + +#ifdef _WIN32 + #if _WIN32_WINNT < 0x0601 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0601 // Force to include needed API prototypes + #endif + + #ifndef NOMINMAX + #define NOMINMAX + #endif + + #include // std::hex, std::dec + #include // std::cerr + #include // std::endl + #include + +// The needed Windows API for processor groups could be missed from old Windows +// versions, so instead of calling them directly (forcing the linker to resolve +// the calls at compile time), try to load them at runtime. To do this we need +// first to define the corresponding function pointers. + +#endif + + +namespace Stockfish { + +// Wrappers for systems where the c++17 implementation does not guarantee the +// availability of aligned_alloc(). Memory allocated with std_aligned_alloc() +// must be freed with std_aligned_free(). + +void* std_aligned_alloc(size_t alignment, size_t size) { +#if defined(_ISOC11_SOURCE) + return aligned_alloc(alignment, size); +#elif defined(POSIXALIGNEDALLOC) + void* mem = nullptr; + posix_memalign(&mem, alignment, size); + return mem; +#elif defined(_WIN32) && !defined(_M_ARM) && !defined(_M_ARM64) + return _mm_malloc(size, alignment); +#elif defined(_WIN32) + return _aligned_malloc(size, alignment); +#else + return std::aligned_alloc(alignment, size); +#endif +} + +void std_aligned_free(void* ptr) { + +#if defined(POSIXALIGNEDALLOC) + free(ptr); +#elif defined(_WIN32) && !defined(_M_ARM) && !defined(_M_ARM64) + _mm_free(ptr); +#elif defined(_WIN32) + _aligned_free(ptr); +#else + free(ptr); +#endif +} + +// aligned_large_pages_alloc() will return suitably aligned memory, +// if possible using large pages. + +#if defined(_WIN32) + +static void* aligned_large_pages_alloc_windows([[maybe_unused]] size_t allocSize) { + + return windows_try_with_large_page_priviliges( + [&](size_t largePageSize) { + // Round up size to full pages and allocate + allocSize = (allocSize + largePageSize - 1) & ~size_t(largePageSize - 1); + return VirtualAlloc(nullptr, allocSize, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES, + PAGE_READWRITE); + }, + []() { return (void*) nullptr; }); +} + +void* aligned_large_pages_alloc(size_t allocSize) { + + // Try to allocate large pages + void* mem = aligned_large_pages_alloc_windows(allocSize); + + // Fall back to regular, page-aligned, allocation if necessary + if (!mem) + mem = VirtualAlloc(nullptr, allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + + return mem; +} + +#else + +void* aligned_large_pages_alloc(size_t allocSize) { + + #if defined(__linux__) + constexpr size_t alignment = 2 * 1024 * 1024; // 2MB page size assumed + #else + constexpr size_t alignment = 4096; // small page size assumed + #endif + + // Round up to multiples of alignment + size_t size = ((allocSize + alignment - 1) / alignment) * alignment; + void* mem = std_aligned_alloc(alignment, size); + #if defined(MADV_HUGEPAGE) + madvise(mem, size, MADV_HUGEPAGE); + #endif + return mem; +} + +#endif + +bool has_large_pages() { + +#if defined(_WIN32) + + constexpr size_t page_size = 2 * 1024 * 1024; // 2MB page size assumed + void* mem = aligned_large_pages_alloc_windows(page_size); + if (mem == nullptr) + { + return false; + } + else + { + aligned_large_pages_free(mem); + return true; + } + +#elif defined(__linux__) + + #if defined(MADV_HUGEPAGE) + return true; + #else + return false; + #endif + +#else + + return false; + +#endif +} + + +// aligned_large_pages_free() will free the previously memory allocated +// by aligned_large_pages_alloc(). The effect is a nop if mem == nullptr. + +#if defined(_WIN32) + +void aligned_large_pages_free(void* mem) { + + if (mem && !VirtualFree(mem, 0, MEM_RELEASE)) + { + DWORD err = GetLastError(); + std::cerr << "Failed to free large page memory. Error code: 0x" << std::hex << err + << std::dec << std::endl; + exit(EXIT_FAILURE); + } +} + +#else + +void aligned_large_pages_free(void* mem) { std_aligned_free(mem); } + +#endif +} // namespace Stockfish diff --git a/src/memory.h b/src/memory.h new file mode 100644 index 000000000..c307a1317 --- /dev/null +++ b/src/memory.h @@ -0,0 +1,322 @@ +/* + 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 . +*/ + +#ifndef MEMORY_H_INCLUDED +#define MEMORY_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include + +#include "types.h" + +#if defined(_WIN64) + + #if _WIN32_WINNT < 0x0601 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0601 // Force to include needed API prototypes + #endif + + #if !defined(NOMINMAX) + #define NOMINMAX + #endif + #include + + // Some Windows headers (RPC/old headers) define short macros such + // as 'small' expanding to 'char', which breaks identifiers in the code. + // Undefine those macros immediately after including . + #ifdef small + #undef small + #endif + + #include + +extern "C" { +using OpenProcessToken_t = bool (*)(HANDLE, DWORD, PHANDLE); +using LookupPrivilegeValueA_t = bool (*)(LPCSTR, LPCSTR, PLUID); +using AdjustTokenPrivileges_t = + bool (*)(HANDLE, BOOL, PTOKEN_PRIVILEGES, DWORD, PTOKEN_PRIVILEGES, PDWORD); +} +#endif + + +namespace Stockfish { + +void* std_aligned_alloc(size_t alignment, size_t size); +void std_aligned_free(void* ptr); + +// Memory aligned by page size, min alignment: 4096 bytes +void* aligned_large_pages_alloc(size_t size); +void aligned_large_pages_free(void* mem); + +bool has_large_pages(); + +// Frees memory which was placed there with placement new. +// Works for both single objects and arrays of unknown bound. +template +void memory_deleter(T* ptr, FREE_FUNC free_func) { + if (!ptr) + return; + + // Explicitly needed to call the destructor + if constexpr (!std::is_trivially_destructible_v) + ptr->~T(); + + free_func(ptr); +} + +// Frees memory which was placed there with placement new. +// Works for both single objects and arrays of unknown bound. +template +void memory_deleter_array(T* ptr, FREE_FUNC free_func) { + if (!ptr) + return; + + + // Move back on the pointer to where the size is allocated + const size_t array_offset = std::max(sizeof(size_t), alignof(T)); + char* raw_memory = reinterpret_cast(ptr) - array_offset; + + if constexpr (!std::is_trivially_destructible_v) + { + const size_t size = *reinterpret_cast(raw_memory); + + // Explicitly call the destructor for each element in reverse order + for (size_t i = size; i-- > 0;) + ptr[i].~T(); + } + + free_func(raw_memory); +} + +// Allocates memory for a single object and places it there with placement new +template +inline std::enable_if_t, T*> memory_allocator(ALLOC_FUNC alloc_func, + Args&&... args) { + void* raw_memory = alloc_func(sizeof(T)); + ASSERT_ALIGNED(raw_memory, alignof(T)); + return new (raw_memory) T(std::forward(args)...); +} + +// Allocates memory for an array of unknown bound and places it there with placement new +template +inline std::enable_if_t, std::remove_extent_t*> +memory_allocator(ALLOC_FUNC alloc_func, size_t num) { + using ElementType = std::remove_extent_t; + + const size_t array_offset = std::max(sizeof(size_t), alignof(ElementType)); + + // Save the array size in the memory location + char* raw_memory = + reinterpret_cast(alloc_func(array_offset + num * sizeof(ElementType))); + ASSERT_ALIGNED(raw_memory, alignof(T)); + + new (raw_memory) size_t(num); + + for (size_t i = 0; i < num; ++i) + new (raw_memory + array_offset + i * sizeof(ElementType)) ElementType(); + + // Need to return the pointer at the start of the array so that + // the indexing in unique_ptr works. + return reinterpret_cast(raw_memory + array_offset); +} + +// +// +// aligned large page unique ptr +// +// + +template +struct LargePageDeleter { + void operator()(T* ptr) const { return memory_deleter(ptr, aligned_large_pages_free); } +}; + +template +struct LargePageArrayDeleter { + void operator()(T* ptr) const { return memory_deleter_array(ptr, aligned_large_pages_free); } +}; + +template +using LargePagePtr = + std::conditional_t, + std::unique_ptr>>, + std::unique_ptr>>; + +// make_unique_large_page for single objects +template +std::enable_if_t, LargePagePtr> make_unique_large_page(Args&&... args) { + static_assert(alignof(T) <= 4096, + "aligned_large_pages_alloc() may fail for such a big alignment requirement of T"); + + T* obj = memory_allocator(aligned_large_pages_alloc, std::forward(args)...); + + return LargePagePtr(obj); +} + +// make_unique_large_page for arrays of unknown bound +template +std::enable_if_t, LargePagePtr> make_unique_large_page(size_t num) { + using ElementType = std::remove_extent_t; + + static_assert(alignof(ElementType) <= 4096, + "aligned_large_pages_alloc() may fail for such a big alignment requirement of T"); + + ElementType* memory = memory_allocator(aligned_large_pages_alloc, num); + + return LargePagePtr(memory); +} + +// +// +// aligned unique ptr +// +// + +template +struct AlignedDeleter { + void operator()(T* ptr) const { return memory_deleter(ptr, std_aligned_free); } +}; + +template +struct AlignedArrayDeleter { + void operator()(T* ptr) const { return memory_deleter_array(ptr, std_aligned_free); } +}; + +template +using AlignedPtr = + std::conditional_t, + std::unique_ptr>>, + std::unique_ptr>>; + +// make_unique_aligned for single objects +template +std::enable_if_t, AlignedPtr> make_unique_aligned(Args&&... args) { + const auto func = [](size_t size) { return std_aligned_alloc(alignof(T), size); }; + T* obj = memory_allocator(func, std::forward(args)...); + + return AlignedPtr(obj); +} + +// make_unique_aligned for arrays of unknown bound +template +std::enable_if_t, AlignedPtr> make_unique_aligned(size_t num) { + using ElementType = std::remove_extent_t; + + const auto func = [](size_t size) { return std_aligned_alloc(alignof(ElementType), size); }; + ElementType* memory = memory_allocator(func, num); + + return AlignedPtr(memory); +} + + +// Get the first aligned element of an array. +// ptr must point to an array of size at least `sizeof(T) * N + alignment` bytes, +// where N is the number of elements in the array. +template +T* align_ptr_up(T* ptr) { + static_assert(alignof(T) < Alignment); + + const uintptr_t ptrint = reinterpret_cast(reinterpret_cast(ptr)); + return reinterpret_cast( + reinterpret_cast((ptrint + (Alignment - 1)) / Alignment * Alignment)); +} + +#if defined(_WIN32) + +template +auto windows_try_with_large_page_priviliges([[maybe_unused]] FuncYesT&& fyes, FuncNoT&& fno) { + + #if !defined(_WIN64) + return fno(); + #else + + HANDLE hProcessToken{}; + LUID luid{}; + + const size_t largePageSize = GetLargePageMinimum(); + if (!largePageSize) + return fno(); + + // Dynamically link OpenProcessToken, LookupPrivilegeValue and AdjustTokenPrivileges + + HMODULE hAdvapi32 = GetModuleHandle(TEXT("advapi32.dll")); + + if (!hAdvapi32) + hAdvapi32 = LoadLibrary(TEXT("advapi32.dll")); + + auto OpenProcessToken_f = + OpenProcessToken_t((void (*)()) GetProcAddress(hAdvapi32, "OpenProcessToken")); + if (!OpenProcessToken_f) + return fno(); + auto LookupPrivilegeValueA_f = + LookupPrivilegeValueA_t((void (*)()) GetProcAddress(hAdvapi32, "LookupPrivilegeValueA")); + if (!LookupPrivilegeValueA_f) + return fno(); + auto AdjustTokenPrivileges_f = + AdjustTokenPrivileges_t((void (*)()) GetProcAddress(hAdvapi32, "AdjustTokenPrivileges")); + if (!AdjustTokenPrivileges_f) + return fno(); + + // We need SeLockMemoryPrivilege, so try to enable it for the process + + if (!OpenProcessToken_f( // OpenProcessToken() + GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken)) + return fno(); + + if (!LookupPrivilegeValueA_f(nullptr, "SeLockMemoryPrivilege", &luid)) + return fno(); + + TOKEN_PRIVILEGES tp{}; + TOKEN_PRIVILEGES prevTp{}; + DWORD prevTpLen = 0; + + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = luid; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + // Try to enable SeLockMemoryPrivilege. Note that even if AdjustTokenPrivileges() + // succeeds, we still need to query GetLastError() to ensure that the privileges + // were actually obtained. + + if (!AdjustTokenPrivileges_f(hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp, + &prevTpLen) + || GetLastError() != ERROR_SUCCESS) + return fno(); + + auto&& ret = fyes(largePageSize); + + // Privilege no longer needed, restore previous state + AdjustTokenPrivileges_f(hProcessToken, FALSE, &prevTp, 0, nullptr, nullptr); + + CloseHandle(hProcessToken); + + return std::forward(ret); + + #endif +} + +#endif + +} // namespace Stockfish + +#endif // #ifndef MEMORY_H_INCLUDED diff --git a/src/misc.cpp b/src/misc.cpp index 270d25ad4..3ddae503c 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -18,58 +18,23 @@ #include "misc.h" -#ifdef _WIN32 - #if _WIN32_WINNT < 0x0601 - #undef _WIN32_WINNT - #define _WIN32_WINNT 0x0601 // Force to include needed API prototypes - #endif - - #ifndef NOMINMAX - #define NOMINMAX - #endif - - #include -// The needed Windows API for processor groups could be missed from old Windows -// versions, so instead of calling them directly (forcing the linker to resolve -// the calls at compile time), try to load them at runtime. To do this we need -// first to define the corresponding function pointers. -extern "C" { -using fun1_t = bool (*)(LOGICAL_PROCESSOR_RELATIONSHIP, - PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, - PDWORD); -using fun2_t = bool (*)(USHORT, PGROUP_AFFINITY); -using fun3_t = bool (*)(HANDLE, CONST GROUP_AFFINITY*, PGROUP_AFFINITY); -using fun4_t = bool (*)(USHORT, PGROUP_AFFINITY, USHORT, PUSHORT); -using fun5_t = WORD (*)(); -using fun6_t = bool (*)(HANDLE, DWORD, PHANDLE); -using fun7_t = bool (*)(LPCSTR, LPCSTR, PLUID); -using fun8_t = bool (*)(HANDLE, BOOL, PTOKEN_PRIVILEGES, DWORD, PTOKEN_PRIVILEGES, PDWORD); -} -#endif - +#include #include +#include +#include #include #include #include #include #include +#include +#include #include #include #include #include "types.h" -#if defined(__linux__) && !defined(__ANDROID__) - #include -#endif - -#if defined(__APPLE__) || defined(__ANDROID__) || defined(__OpenBSD__) \ - || (defined(__GLIBCXX__) && !defined(_GLIBCXX_HAVE_ALIGNED_ALLOC) && !defined(_WIN32)) \ - || defined(__e2k__) - #define POSIXALIGNEDALLOC - #include -#endif - namespace Stockfish { namespace { @@ -79,7 +44,7 @@ constexpr std::string_view version = "dev"; // 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 -// can toggle the logging of std::cout and std:cin at runtime whilst preserving +// can toggle the logging of std::cout and std::cin at runtime whilst preserving // usual I/O functionality, all without changing a single line of code! // Idea from http://groups.google.com/group/comp.lang.c++/msg/1d941c0f26ea0d81 @@ -149,15 +114,17 @@ class Logger { // Returns the full name of the current Stockfish version. -// For local dev compiles we try to append the commit sha and commit date -// from git if that fails only the local compilation date is set and "nogit" is specified: -// Stockfish dev-YYYYMMDD-SHA -// or -// Stockfish dev-YYYYMMDD-nogit +// +// For local dev compiles we try to append the commit SHA and +// commit date from git. If that fails only the local compilation +// date is set and "nogit" is specified: +// Stockfish dev-YYYYMMDD-SHA +// or +// Stockfish dev-YYYYMMDD-nogit // // For releases (non-dev builds) we only include the version number: -// Stockfish version -std::string engine_info(bool to_uci) { +// Stockfish version +std::string engine_version_info() { std::stringstream ss; ss << "Stockfish " << version << std::setfill('0'); @@ -168,8 +135,9 @@ std::string engine_info(bool to_uci) { ss << stringify(GIT_DATE); #else constexpr std::string_view months("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"); - std::string month, day, year; - std::stringstream date(__DATE__); // From compiler, format is "Sep 21 2008" + + std::string month, day, year; + std::stringstream date(__DATE__); // From compiler, format is "Sep 21 2008" date >> month >> day >> year; ss << year << std::setw(2) << std::setfill('0') << (1 + months.find(month) / 4) @@ -185,11 +153,14 @@ std::string engine_info(bool to_uci) { #endif } - ss << (to_uci ? "\nid author " : " by ") << "the Stockfish developers (see AUTHORS file)"; - return ss.str(); } +std::string engine_info(bool to_uci) { + return engine_version_info() + (to_uci ? "\nid author " : " by ") + + "the Stockfish developers (see AUTHORS file)"; +} + // Returns a string trying to describe the compiler we use std::string compiler_info() { @@ -266,6 +237,9 @@ std::string compiler_info() { compiler += "\nCompilation settings : "; compiler += (Is64Bit ? "64bit" : "32bit"); +#if defined(USE_AVX512ICL) + compiler += " AVX512ICL"; +#endif #if defined(USE_VNNI) compiler += " VNNI"; #endif @@ -285,12 +259,12 @@ std::string compiler_info() { #if defined(USE_SSE2) compiler += " SSE2"; #endif - compiler += (HasPopCnt ? " POPCNT" : ""); #if defined(USE_NEON_DOTPROD) compiler += " NEON_DOTPROD"; #elif defined(USE_NEON) compiler += " NEON"; #endif + compiler += (HasPopCnt ? " POPCNT" : ""); #if !defined(NDEBUG) compiler += " DEBUG"; @@ -316,46 +290,75 @@ namespace { template struct DebugInfo { - std::atomic data[N] = {0}; + std::array, N> data = {0}; - constexpr inline std::atomic& operator[](int index) { return data[index]; } + [[nodiscard]] constexpr std::atomic& operator[](size_t index) { + assert(index < N); + return data[index]; + } + + constexpr DebugInfo& operator=(const DebugInfo& other) { + for (size_t i = 0; i < N; i++) + data[i].store(other.data[i].load()); + return *this; + } }; -DebugInfo<2> hit[MaxDebugSlots]; -DebugInfo<2> mean[MaxDebugSlots]; -DebugInfo<3> stdev[MaxDebugSlots]; -DebugInfo<6> correl[MaxDebugSlots]; +struct DebugExtremes: public DebugInfo<3> { + DebugExtremes() { + data[1] = std::numeric_limits::min(); + data[2] = std::numeric_limits::max(); + } +}; + +std::array, MaxDebugSlots> hit; +std::array, MaxDebugSlots> mean; +std::array, MaxDebugSlots> stdev; +std::array, MaxDebugSlots> correl; +std::array extremes; } // namespace void dbg_hit_on(bool cond, int slot) { - ++hit[slot][0]; + ++hit.at(slot)[0]; if (cond) - ++hit[slot][1]; + ++hit.at(slot)[1]; } void dbg_mean_of(int64_t value, int slot) { - ++mean[slot][0]; - mean[slot][1] += value; + ++mean.at(slot)[0]; + mean.at(slot)[1] += value; } void dbg_stdev_of(int64_t value, int slot) { - ++stdev[slot][0]; - stdev[slot][1] += value; - stdev[slot][2] += value * value; + ++stdev.at(slot)[0]; + stdev.at(slot)[1] += value; + stdev.at(slot)[2] += value * value; +} + +void dbg_extremes_of(int64_t value, int slot) { + ++extremes.at(slot)[0]; + + int64_t current_max = extremes.at(slot)[1].load(); + while (current_max < value && !extremes.at(slot)[1].compare_exchange_weak(current_max, value)) + {} + + int64_t current_min = extremes.at(slot)[2].load(); + while (current_min > value && !extremes.at(slot)[2].compare_exchange_weak(current_min, value)) + {} } void dbg_correl_of(int64_t value1, int64_t value2, int slot) { - ++correl[slot][0]; - correl[slot][1] += value1; - correl[slot][2] += value1 * value1; - correl[slot][3] += value2; - correl[slot][4] += value2 * value2; - correl[slot][5] += value1 * value2; + ++correl.at(slot)[0]; + correl.at(slot)[1] += value1; + correl.at(slot)[2] += value1 * value1; + correl.at(slot)[3] += value2; + correl.at(slot)[4] += value2 * value2; + correl.at(slot)[5] += value1 * value2; } void dbg_print() { @@ -382,6 +385,13 @@ void dbg_print() { std::cerr << "Stdev #" << i << ": Total " << n << " Stdev " << r << std::endl; } + for (int i = 0; i < MaxDebugSlots; ++i) + if ((n = extremes[i][0])) + { + std::cerr << "Extremity #" << i << ": Total " << n << " Min " << extremes[i][2] + << " Max " << extremes[i][1] << std::endl; + } + for (int i = 0; i < MaxDebugSlots; ++i) if ((n = correl[i][0])) { @@ -392,6 +402,13 @@ void dbg_print() { } } +void dbg_clear() { + hit.fill({}); + mean.fill({}); + stdev.fill({}); + correl.fill({}); + extremes.fill({}); +} // Used to serialize access to std::cout // to avoid multiple threads writing at the same time. @@ -408,6 +425,8 @@ std::ostream& operator<<(std::ostream& os, SyncCout sc) { return os; } +void sync_cout_start() { std::cout << IO_LOCK; } +void sync_cout_end() { std::cout << IO_UNLOCK; } // Trampoline helper to avoid moving Logger to misc.h void start_logger(const std::string& fname) { Logger::start(fname); } @@ -415,14 +434,14 @@ void start_logger(const std::string& fname) { Logger::start(fname); } #ifdef NO_PREFETCH -void prefetch(void*) {} +void prefetch(const void*) {} #else -void prefetch(void* addr) { +void prefetch(const void* addr) { #if defined(_MSC_VER) - _mm_prefetch((char*) addr, _MM_HINT_T0); + _mm_prefetch((char const*) addr, _MM_HINT_T0); #else __builtin_prefetch(addr); #endif @@ -430,291 +449,6 @@ void prefetch(void* addr) { #endif - -// Wrapper for systems where the c++17 implementation -// does not guarantee the availability of aligned_alloc(). Memory allocated with -// std_aligned_alloc() must be freed with std_aligned_free(). -void* std_aligned_alloc(size_t alignment, size_t size) { - -#if defined(POSIXALIGNEDALLOC) - void* mem; - return posix_memalign(&mem, alignment, size) ? nullptr : mem; -#elif defined(_WIN32) && !defined(_M_ARM) && !defined(_M_ARM64) - return _mm_malloc(size, alignment); -#elif defined(_WIN32) - return _aligned_malloc(size, alignment); -#else - return std::aligned_alloc(alignment, size); -#endif -} - -void std_aligned_free(void* ptr) { - -#if defined(POSIXALIGNEDALLOC) - free(ptr); -#elif defined(_WIN32) && !defined(_M_ARM) && !defined(_M_ARM64) - _mm_free(ptr); -#elif defined(_WIN32) - _aligned_free(ptr); -#else - free(ptr); -#endif -} - -// aligned_large_pages_alloc() will return suitably aligned memory, if possible using large pages. - -#if defined(_WIN32) - -static void* aligned_large_pages_alloc_windows([[maybe_unused]] size_t allocSize) { - - #if !defined(_WIN64) - return nullptr; - #else - - HANDLE hProcessToken{}; - LUID luid{}; - void* mem = nullptr; - - const size_t largePageSize = GetLargePageMinimum(); - if (!largePageSize) - return nullptr; - - // Dynamically link OpenProcessToken, LookupPrivilegeValue and AdjustTokenPrivileges - - HMODULE hAdvapi32 = GetModuleHandle(TEXT("advapi32.dll")); - - if (!hAdvapi32) - hAdvapi32 = LoadLibrary(TEXT("advapi32.dll")); - - auto fun6 = fun6_t((void (*)()) GetProcAddress(hAdvapi32, "OpenProcessToken")); - if (!fun6) - return nullptr; - auto fun7 = fun7_t((void (*)()) GetProcAddress(hAdvapi32, "LookupPrivilegeValueA")); - if (!fun7) - return nullptr; - auto fun8 = fun8_t((void (*)()) GetProcAddress(hAdvapi32, "AdjustTokenPrivileges")); - if (!fun8) - return nullptr; - - // We need SeLockMemoryPrivilege, so try to enable it for the process - if (!fun6( // OpenProcessToken() - GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken)) - return nullptr; - - if (fun7( // LookupPrivilegeValue(nullptr, SE_LOCK_MEMORY_NAME, &luid) - nullptr, "SeLockMemoryPrivilege", &luid)) - { - TOKEN_PRIVILEGES tp{}; - TOKEN_PRIVILEGES prevTp{}; - DWORD prevTpLen = 0; - - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = luid; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - // Try to enable SeLockMemoryPrivilege. Note that even if AdjustTokenPrivileges() succeeds, - // we still need to query GetLastError() to ensure that the privileges were actually obtained. - if (fun8( // AdjustTokenPrivileges() - hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp, &prevTpLen) - && GetLastError() == ERROR_SUCCESS) - { - // Round up size to full pages and allocate - allocSize = (allocSize + largePageSize - 1) & ~size_t(largePageSize - 1); - mem = VirtualAlloc(nullptr, allocSize, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES, - PAGE_READWRITE); - - // Privilege no longer needed, restore previous state - fun8( // AdjustTokenPrivileges () - hProcessToken, FALSE, &prevTp, 0, nullptr, nullptr); - } - } - - CloseHandle(hProcessToken); - - return mem; - - #endif -} - -void* aligned_large_pages_alloc(size_t allocSize) { - - // Try to allocate large pages - void* mem = aligned_large_pages_alloc_windows(allocSize); - - // Fall back to regular, page-aligned, allocation if necessary - if (!mem) - mem = VirtualAlloc(nullptr, allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - - return mem; -} - -#else - -void* aligned_large_pages_alloc(size_t allocSize) { - - #if defined(__linux__) - constexpr size_t alignment = 2 * 1024 * 1024; // assumed 2MB page size - #else - constexpr size_t alignment = 4096; // assumed small page size - #endif - - // Round up to multiples of alignment - size_t size = ((allocSize + alignment - 1) / alignment) * alignment; - void* mem = std_aligned_alloc(alignment, size); - #if defined(MADV_HUGEPAGE) - madvise(mem, size, MADV_HUGEPAGE); - #endif - return mem; -} - -#endif - - -// aligned_large_pages_free() will free the previously allocated ttmem - -#if defined(_WIN32) - -void aligned_large_pages_free(void* mem) { - - if (mem && !VirtualFree(mem, 0, MEM_RELEASE)) - { - DWORD err = GetLastError(); - std::cerr << "Failed to free large page memory. Error code: 0x" << std::hex << err - << std::dec << std::endl; - exit(EXIT_FAILURE); - } -} - -#else - -void aligned_large_pages_free(void* mem) { std_aligned_free(mem); } - -#endif - - -namespace WinProcGroup { - -#ifndef _WIN32 - -void bind_this_thread(size_t) {} - -#else - -namespace { -// Retrieves logical processor information using Windows-specific -// API and returns the best node id for the thread with index idx. Original -// code from Texel by Peter Österlund. -int best_node(size_t idx) { - - int threads = 0; - int nodes = 0; - int cores = 0; - DWORD returnLength = 0; - DWORD byteOffset = 0; - - // Early exit if the needed API is not available at runtime - HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); - auto fun1 = (fun1_t) (void (*)()) GetProcAddress(k32, "GetLogicalProcessorInformationEx"); - if (!fun1) - return -1; - - // First call to GetLogicalProcessorInformationEx() to get returnLength. - // We expect the call to fail due to null buffer. - if (fun1(RelationAll, nullptr, &returnLength)) - return -1; - - // Once we know returnLength, allocate the buffer - SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *buffer, *ptr; - ptr = buffer = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) malloc(returnLength); - - // Second call to GetLogicalProcessorInformationEx(), now we expect to succeed - if (!fun1(RelationAll, buffer, &returnLength)) - { - free(buffer); - return -1; - } - - while (byteOffset < returnLength) - { - if (ptr->Relationship == RelationNumaNode) - nodes++; - - else if (ptr->Relationship == RelationProcessorCore) - { - cores++; - threads += (ptr->Processor.Flags == LTP_PC_SMT) ? 2 : 1; - } - - assert(ptr->Size); - byteOffset += ptr->Size; - ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) (((char*) ptr) + ptr->Size); - } - - free(buffer); - - std::vector groups; - - // Run as many threads as possible on the same node until the core limit is - // reached, then move on to filling the next node. - for (int n = 0; n < nodes; n++) - for (int i = 0; i < cores / nodes; i++) - groups.push_back(n); - - // In case a core has more than one logical processor (we assume 2) and we - // still have threads to allocate, spread them evenly across available nodes. - for (int t = 0; t < threads - cores; t++) - groups.push_back(t % nodes); - - // If we still have more threads than the total number of logical processors - // then return -1 and let the OS to decide what to do. - return idx < groups.size() ? groups[idx] : -1; -} -} - - -// Sets the group affinity of the current thread -void bind_this_thread(size_t idx) { - - // Use only local variables to be thread-safe - int node = best_node(idx); - - if (node == -1) - return; - - // Early exit if the needed API are not available at runtime - HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); - auto fun2 = fun2_t((void (*)()) GetProcAddress(k32, "GetNumaNodeProcessorMaskEx")); - auto fun3 = fun3_t((void (*)()) GetProcAddress(k32, "SetThreadGroupAffinity")); - auto fun4 = fun4_t((void (*)()) GetProcAddress(k32, "GetNumaNodeProcessorMask2")); - auto fun5 = fun5_t((void (*)()) GetProcAddress(k32, "GetMaximumProcessorGroupCount")); - - if (!fun2 || !fun3) - return; - - if (!fun4 || !fun5) - { - GROUP_AFFINITY affinity; - if (fun2(node, &affinity)) // GetNumaNodeProcessorMaskEx - fun3(GetCurrentThread(), &affinity, nullptr); // SetThreadGroupAffinity - } - else - { - // If a numa node has more than one processor group, we assume they are - // sized equal and we spread threads evenly across the groups. - USHORT elements, returnedElements; - elements = fun5(); // GetMaximumProcessorGroupCount - GROUP_AFFINITY* affinity = (GROUP_AFFINITY*) malloc(elements * sizeof(GROUP_AFFINITY)); - if (fun4(node, affinity, elements, &returnedElements)) // GetNumaNodeProcessorMask2 - fun3(GetCurrentThread(), &affinity[idx % returnedElements], - nullptr); // SetThreadGroupAffinity - free(affinity); - } -} - -#endif - -} // namespace WinProcGroup - #ifdef _WIN32 #include #define GETCWD _getcwd @@ -723,13 +457,30 @@ void bind_this_thread(size_t idx) { #define GETCWD getcwd #endif -CommandLine::CommandLine(int _argc, char** _argv) : - argc(_argc), - argv(_argv) { - std::string pathSeparator; +size_t str_to_size_t(const std::string& s) { + unsigned long long value = std::stoull(s); + if (value > std::numeric_limits::max()) + std::exit(EXIT_FAILURE); + return static_cast(value); +} - // Extract the path+name of the executable binary - std::string argv0 = argv[0]; +std::optional read_file_to_string(const std::string& path) { + std::ifstream f(path, std::ios_base::binary); + if (!f) + return std::nullopt; + return std::string(std::istreambuf_iterator(f), std::istreambuf_iterator()); +} + +void remove_whitespace(std::string& s) { + s.erase(std::remove_if(s.begin(), s.end(), [](char c) { return std::isspace(c); }), s.end()); +} + +bool is_whitespace(std::string_view s) { + return std::all_of(s.begin(), s.end(), [](char c) { return std::isspace(c); }); +} + +std::string CommandLine::get_binary_directory(std::string argv0) { + std::string pathSeparator; #ifdef _WIN32 pathSeparator = "\\"; @@ -745,15 +496,11 @@ CommandLine::CommandLine(int _argc, char** _argv) : #endif // Extract the working directory - workingDirectory = ""; - char buff[40000]; - char* cwd = GETCWD(buff, 40000); - if (cwd) - workingDirectory = cwd; + auto workingDirectory = CommandLine::get_working_directory(); // Extract the binary directory path from argv0 - binaryDirectory = argv0; - size_t pos = binaryDirectory.find_last_of("\\/"); + auto binaryDirectory = argv0; + size_t pos = binaryDirectory.find_last_of("\\/"); if (pos == std::string::npos) binaryDirectory = "." + pathSeparator; else @@ -762,6 +509,19 @@ CommandLine::CommandLine(int _argc, char** _argv) : // Pattern replacement: "./" at the start of path is replaced by the working directory if (binaryDirectory.find("." + pathSeparator) == 0) binaryDirectory.replace(0, 1, workingDirectory); + + return binaryDirectory; } +std::string CommandLine::get_working_directory() { + std::string workingDirectory = ""; + char buff[40000]; + char* cwd = GETCWD(buff, 40000); + if (cwd) + workingDirectory = cwd; + + return workingDirectory; +} + + } // namespace Stockfish diff --git a/src/misc.h b/src/misc.h index de34ee111..f1016b496 100644 --- a/src/misc.h +++ b/src/misc.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -20,13 +20,20 @@ #define MISC_H_INCLUDED #include +#include #include #include -#include #include +#include +#include // IWYU pragma: keep +// IWYU pragma: no_include <__exception/terminate.h> +#include #include +#include +#include #include #include +#include #include #define stringify2(x) #x @@ -34,51 +41,43 @@ namespace Stockfish { +std::string engine_version_info(); std::string engine_info(bool to_uci = false); std::string compiler_info(); // Preloads the given address in L1/L2 cache. This is a non-blocking // function that doesn't stall the CPU waiting for data to be loaded from memory, // which can be quite slow. -void prefetch(void* addr); +void prefetch(const void* addr); -void start_logger(const std::string& fname); -void* std_aligned_alloc(size_t alignment, size_t size); -void std_aligned_free(void* ptr); -// memory aligned by page size, min alignment: 4096 bytes -void* aligned_large_pages_alloc(size_t size); -// nop if mem == nullptr -void aligned_large_pages_free(void* mem); +void start_logger(const std::string& fname); -// Deleter for automating release of memory area -template -struct AlignedDeleter { - void operator()(T* ptr) const { - ptr->~T(); - std_aligned_free(ptr); +size_t str_to_size_t(const std::string& s); + +#if defined(__linux__) + +struct PipeDeleter { + void operator()(FILE* file) const { + if (file != nullptr) + { + pclose(file); + } } }; -template -struct LargePageDeleter { - void operator()(T* ptr) const { - ptr->~T(); - aligned_large_pages_free(ptr); - } -}; - -template -using AlignedPtr = std::unique_ptr>; - -template -using LargePagePtr = std::unique_ptr>; +#endif +// Reads the file as bytes. +// Returns std::nullopt if the file does not exist. +std::optional read_file_to_string(const std::string& path); void dbg_hit_on(bool cond, int slot = 0); void dbg_mean_of(int64_t value, int slot = 0); void dbg_stdev_of(int64_t value, int slot = 0); +void dbg_extremes_of(int64_t value, int slot = 0); void dbg_correl_of(int64_t value1, int64_t value2, int slot = 0); void dbg_print(); +void dbg_clear(); using TimePoint = std::chrono::milliseconds::rep; // A value in milliseconds static_assert(sizeof(TimePoint) == sizeof(int64_t), "TimePoint should be 64 bits"); @@ -88,6 +87,30 @@ inline TimePoint now() { .count(); } +inline std::vector split(std::string_view s, std::string_view delimiter) { + std::vector res; + + if (s.empty()) + return res; + + size_t begin = 0; + for (;;) + { + const size_t end = s.find(delimiter, begin); + if (end == std::string::npos) + break; + + res.emplace_back(s.substr(begin, end - begin)); + begin = end + delimiter.size(); + } + + res.emplace_back(s.substr(begin)); + + return res; +} + +void remove_whitespace(std::string& s); +bool is_whitespace(std::string_view s); enum SyncCout { IO_LOCK, @@ -98,26 +121,12 @@ std::ostream& operator<<(std::ostream&, SyncCout); #define sync_cout std::cout << IO_LOCK #define sync_endl std::endl << IO_UNLOCK - -// Get the first aligned element of an array. -// ptr must point to an array of size at least `sizeof(T) * N + alignment` bytes, -// where N is the number of elements in the array. -template -T* align_ptr_up(T* ptr) { - static_assert(alignof(T) < Alignment); - - const uintptr_t ptrint = reinterpret_cast(reinterpret_cast(ptr)); - return reinterpret_cast( - reinterpret_cast((ptrint + (Alignment - 1)) / Alignment * Alignment)); -} - +void sync_cout_start(); +void sync_cout_end(); // True if and only if the binary is compiled on a little-endian machine -static inline const union { - uint32_t i; - char c[4]; -} Le = {0x01020304}; -static inline const bool IsLittleEndian = (Le.c[0] == 4); +static inline const std::uint16_t Le = 1; +static inline const bool IsLittleEndian = *reinterpret_cast(&Le) == 1; template @@ -125,10 +134,21 @@ class ValueList { public: std::size_t size() const { return size_; } - void push_back(const T& value) { values_[size_++] = value; } - const T* begin() const { return values_; } - const T* end() const { return values_ + size_; } - const T& operator[](int index) const { return values_[index]; } + int ssize() const { return int(size_); } + void push_back(const T& value) { + assert(size_ < MaxSize); + values_[size_++] = value; + } + const T* begin() const { return values_; } + const T* end() const { return values_ + size_; } + const T& operator[](int index) const { return values_[index]; } + + T* make_space(size_t count) { + T* result = &values_[size_]; + size_ += count; + assert(size_ <= MaxSize); + return result; + } private: T values_[MaxSize]; @@ -136,6 +156,97 @@ class ValueList { }; +template +class MultiArray; + +namespace Detail { + +template +struct MultiArrayHelper { + using ChildType = MultiArray; +}; + +template +struct MultiArrayHelper { + using ChildType = T; +}; + +template +constexpr bool is_strictly_assignable_v = + std::is_assignable_v && (std::is_same_v || !std::is_convertible_v); + +} + +// MultiArray is a generic N-dimensional array. +// The template parameters (Size and Sizes) encode the dimensions of the array. +template +class MultiArray { + using ChildType = typename Detail::MultiArrayHelper::ChildType; + using ArrayType = std::array; + ArrayType data_; + + public: + using value_type = typename ArrayType::value_type; + using size_type = typename ArrayType::size_type; + using difference_type = typename ArrayType::difference_type; + using reference = typename ArrayType::reference; + using const_reference = typename ArrayType::const_reference; + using pointer = typename ArrayType::pointer; + using const_pointer = typename ArrayType::const_pointer; + using iterator = typename ArrayType::iterator; + using const_iterator = typename ArrayType::const_iterator; + using reverse_iterator = typename ArrayType::reverse_iterator; + using const_reverse_iterator = typename ArrayType::const_reverse_iterator; + + constexpr auto& at(size_type index) noexcept { return data_.at(index); } + constexpr const auto& at(size_type index) const noexcept { return data_.at(index); } + + constexpr auto& operator[](size_type index) noexcept { return data_[index]; } + constexpr const auto& operator[](size_type index) const noexcept { return data_[index]; } + + constexpr auto& front() noexcept { return data_.front(); } + constexpr const auto& front() const noexcept { return data_.front(); } + constexpr auto& back() noexcept { return data_.back(); } + constexpr const auto& back() const noexcept { return data_.back(); } + + auto* data() { return data_.data(); } + const auto* data() const { return data_.data(); } + + constexpr auto begin() noexcept { return data_.begin(); } + constexpr auto end() noexcept { return data_.end(); } + constexpr auto begin() const noexcept { return data_.begin(); } + constexpr auto end() const noexcept { return data_.end(); } + constexpr auto cbegin() const noexcept { return data_.cbegin(); } + constexpr auto cend() const noexcept { return data_.cend(); } + + constexpr auto rbegin() noexcept { return data_.rbegin(); } + constexpr auto rend() noexcept { return data_.rend(); } + constexpr auto rbegin() const noexcept { return data_.rbegin(); } + constexpr auto rend() const noexcept { return data_.rend(); } + constexpr auto crbegin() const noexcept { return data_.crbegin(); } + constexpr auto crend() const noexcept { return data_.crend(); } + + constexpr bool empty() const noexcept { return data_.empty(); } + constexpr size_type size() const noexcept { return data_.size(); } + constexpr size_type max_size() const noexcept { return data_.max_size(); } + + template + void fill(const U& v) { + static_assert(Detail::is_strictly_assignable_v, + "Cannot assign fill value to entry type"); + for (auto& ele : data_) + { + if constexpr (sizeof...(Sizes) == 0) + ele = v; + else + ele.fill(v); + } + } + + constexpr void swap(MultiArray& other) noexcept { data_.swap(other.data_); } +}; + + // xorshift64star Pseudo-Random Number Generator // This class is based on original code written and dedicated // to the public domain by Sebastiano Vigna (2014). @@ -194,25 +305,106 @@ inline uint64_t mul_hi64(uint64_t a, uint64_t b) { #endif } -// Under Windows it is not possible for a process to run on more than one -// logical processor group. This usually means being limited to using max 64 -// cores. To overcome this, some special platform-specific API should be -// called to set group affinity for each thread. Original code from Texel by -// Peter Österlund. -namespace WinProcGroup { -void bind_this_thread(size_t idx); + +template +inline void hash_combine(std::size_t& seed, const T& v) { + std::hash 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); +} + +template +inline std::size_t get_raw_data_hash(const T& value) { + return std::hash{}( + std::string_view(reinterpret_cast(&value), sizeof(value))); +} + +template +class FixedString { + public: + FixedString() : + length_(0) { + data_[0] = '\0'; + } + + FixedString(const char* str) { + size_t len = std::strlen(str); + if (len > Capacity) + std::terminate(); + std::memcpy(data_, str, len); + length_ = len; + data_[length_] = '\0'; + } + + FixedString(const std::string& str) { + if (str.size() > Capacity) + std::terminate(); + std::memcpy(data_, str.data(), str.size()); + length_ = str.size(); + data_[length_] = '\0'; + } + + std::size_t size() const { return length_; } + std::size_t capacity() const { return Capacity; } + + const char* c_str() const { return data_; } + const char* data() const { return data_; } + + char& operator[](std::size_t i) { return data_[i]; } + + const char& operator[](std::size_t i) const { return data_[i]; } + + FixedString& operator+=(const char* str) { + size_t len = std::strlen(str); + if (length_ + len > Capacity) + std::terminate(); + std::memcpy(data_ + length_, str, len); + length_ += len; + data_[length_] = '\0'; + return *this; + } + + FixedString& operator+=(const FixedString& other) { return (*this += other.c_str()); } + + operator std::string() const { return std::string(data_, length_); } + + operator std::string_view() const { return std::string_view(data_, length_); } + + template + bool operator==(const T& other) const noexcept { + return (std::string_view) (*this) == other; + } + + template + bool operator!=(const T& other) const noexcept { + return (std::string_view) (*this) != other; + } + + void clear() { + length_ = 0; + data_[0] = '\0'; + } + + private: + char data_[Capacity + 1]; // +1 for null terminator + std::size_t length_; +}; struct CommandLine { public: - CommandLine(int, char**); + CommandLine(int _argc, char** _argv) : + argc(_argc), + argv(_argv) {} + + static std::string get_binary_directory(std::string argv0); + static std::string get_working_directory(); int argc; char** argv; - - std::string binaryDirectory; // path of the executable directory - std::string workingDirectory; // path of the working directory }; namespace Utility { @@ -228,6 +420,38 @@ void move_to_front(std::vector& vec, Predicate pred) { } } +#if defined(__GNUC__) + #define sf_always_inline __attribute__((always_inline)) +#elif defined(_MSC_VER) + #define sf_always_inline __forceinline +#else + // do nothing for other compilers + #define sf_always_inline +#endif + +#if defined(__GNUC__) && !defined(__clang__) + #if __GNUC__ >= 13 + #define sf_assume(cond) __attribute__((assume(cond))) + #else + #define sf_assume(cond) \ + do \ + { \ + if (!(cond)) \ + __builtin_unreachable(); \ + } while (0) + #endif +#else + // do nothing for other compilers + #define sf_assume(cond) +#endif + } // namespace Stockfish +template +struct std::hash> { + std::size_t operator()(const Stockfish::FixedString& fstr) const noexcept { + return std::hash{}((std::string_view) fstr); + } +}; + #endif // #ifndef MISC_H_INCLUDED diff --git a/src/movegen.cpp b/src/movegen.cpp index e6923067f..d22faad25 100644 --- a/src/movegen.cpp +++ b/src/movegen.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -24,12 +24,89 @@ #include "bitboard.h" #include "position.h" +#if defined(USE_AVX512ICL) + #include + #include + #include +#endif + namespace Stockfish { namespace { +#if defined(USE_AVX512ICL) + +inline Move* write_moves(Move* moveList, uint32_t mask, __m512i vector) { + // Avoid _mm512_mask_compressstoreu_epi16() as it's 256 uOps on Zen4 + _mm512_storeu_si512(reinterpret_cast<__m512i*>(moveList), + _mm512_maskz_compress_epi16(mask, vector)); + return moveList + popcount(mask); +} + +template +inline Move* splat_pawn_moves(Move* moveList, Bitboard to_bb) { + alignas(64) static constexpr auto SPLAT_TABLE = [] { + std::array table{}; + for (int8_t i = 0; i < 64; i++) + { + Square from{std::clamp(i - offset, 0, 63)}; + table[i] = {Move(from, Square{i})}; + } + return table; + }(); + + auto table = reinterpret_cast(SPLAT_TABLE.data()); + + moveList = + write_moves(moveList, static_cast(to_bb >> 0), _mm512_load_si512(table + 0)); + moveList = + write_moves(moveList, static_cast(to_bb >> 32), _mm512_load_si512(table + 1)); + + return moveList; +} + +inline Move* splat_moves(Move* moveList, Square from, Bitboard to_bb) { + alignas(64) static constexpr auto SPLAT_TABLE = [] { + std::array table{}; + for (int8_t i = 0; i < 64; i++) + table[i] = {Move(SQUARE_ZERO, Square{i})}; + return table; + }(); + + __m512i fromVec = _mm512_set1_epi16(Move(from, SQUARE_ZERO).raw()); + + auto table = reinterpret_cast(SPLAT_TABLE.data()); + + moveList = write_moves(moveList, static_cast(to_bb >> 0), + _mm512_or_si512(_mm512_load_si512(table + 0), fromVec)); + moveList = write_moves(moveList, static_cast(to_bb >> 32), + _mm512_or_si512(_mm512_load_si512(table + 1), fromVec)); + + return moveList; +} + +#else + +template +inline Move* splat_pawn_moves(Move* moveList, Bitboard to_bb) { + while (to_bb) + { + Square to = pop_lsb(to_bb); + *moveList++ = Move(to - offset, to); + } + return moveList; +} + +inline Move* splat_moves(Move* moveList, Square from, Bitboard to_bb) { + while (to_bb) + *moveList++ = Move(from, pop_lsb(to_bb)); + return moveList; +} + +#endif + template -ExtMove* make_promotions(ExtMove* moveList, [[maybe_unused]] Square to) { +Move* make_promotions(Move* moveList, [[maybe_unused]] Square to) { constexpr bool all = Type == EVASIONS || Type == NON_EVASIONS; @@ -48,7 +125,7 @@ ExtMove* make_promotions(ExtMove* moveList, [[maybe_unused]] Square to) { template -ExtMove* generate_pawn_moves(const Position& pos, ExtMove* moveList, Bitboard target) { +Move* generate_pawn_moves(const Position& pos, Move* moveList, Bitboard target) { constexpr Color Them = ~Us; constexpr Bitboard TRank7BB = (Us == WHITE ? Rank7BB : Rank2BB); @@ -75,28 +152,8 @@ ExtMove* generate_pawn_moves(const Position& pos, ExtMove* moveList, Bitboard ta b2 &= target; } - if constexpr (Type == QUIET_CHECKS) - { - // To make a quiet check, you either make a direct check by pushing a pawn - // or push a blocker pawn that is not on the same file as the enemy king. - // Discovered check promotion has been already generated amongst the captures. - Square ksq = pos.square(Them); - Bitboard dcCandidatePawns = pos.blockers_for_king(Them) & ~file_bb(ksq); - b1 &= pawn_attacks_bb(Them, ksq) | shift(dcCandidatePawns); - b2 &= pawn_attacks_bb(Them, ksq) | shift(dcCandidatePawns); - } - - while (b1) - { - Square to = pop_lsb(b1); - *moveList++ = Move(to - Up, to); - } - - while (b2) - { - Square to = pop_lsb(b2); - *moveList++ = Move(to - Up - Up, to); - } + moveList = splat_pawn_moves(moveList, b1); + moveList = splat_pawn_moves(moveList, b2); } // Promotions and underpromotions @@ -125,17 +182,8 @@ ExtMove* generate_pawn_moves(const Position& pos, ExtMove* moveList, Bitboard ta Bitboard b1 = shift(pawnsNotOn7) & enemies; Bitboard b2 = shift(pawnsNotOn7) & enemies; - while (b1) - { - Square to = pop_lsb(b1); - *moveList++ = Move(to - UpRight, to); - } - - while (b2) - { - Square to = pop_lsb(b2); - *moveList++ = Move(to - UpLeft, to); - } + moveList = splat_pawn_moves(moveList, b1); + moveList = splat_pawn_moves(moveList, b2); if (pos.ep_square() != SQ_NONE) { @@ -145,7 +193,7 @@ ExtMove* generate_pawn_moves(const Position& pos, ExtMove* moveList, Bitboard ta if (Type == EVASIONS && (target & (pos.ep_square() + Up))) return moveList; - b1 = pawnsNotOn7 & pawn_attacks_bb(Them, pos.ep_square()); + b1 = pawnsNotOn7 & attacks_bb(pos.ep_square(), Them); assert(b1); @@ -158,8 +206,8 @@ ExtMove* generate_pawn_moves(const Position& pos, ExtMove* moveList, Bitboard ta } -template -ExtMove* generate_moves(const Position& pos, ExtMove* moveList, Bitboard target) { +template +Move* generate_moves(const Position& pos, Move* moveList, Bitboard target) { static_assert(Pt != KING && Pt != PAWN, "Unsupported piece type in generate_moves()"); @@ -170,12 +218,7 @@ ExtMove* generate_moves(const Position& pos, ExtMove* moveList, Bitboard target) Square from = pop_lsb(bb); Bitboard b = attacks_bb(from, pos.pieces()) & target; - // To check, you either move freely a blocker or make a direct check. - if (Checks && (Pt == QUEEN || !(pos.blockers_for_king(~Us) & from))) - b &= pos.check_squares(Pt); - - while (b) - *moveList++ = Move(from, pop_lsb(b)); + moveList = splat_moves(moveList, from, b); } return moveList; @@ -183,13 +226,12 @@ ExtMove* generate_moves(const Position& pos, ExtMove* moveList, Bitboard target) template -ExtMove* generate_all(const Position& pos, ExtMove* moveList) { +Move* generate_all(const Position& pos, Move* moveList) { static_assert(Type != LEGAL, "Unsupported type in generate_all()"); - constexpr bool Checks = Type == QUIET_CHECKS; // Reduce template instantiations - const Square ksq = pos.square(Us); - Bitboard target; + const Square ksq = pos.square(Us); + Bitboard target; // Skip generating non-king moves when in double check if (Type != EVASIONS || !more_than_one(pos.checkers())) @@ -197,29 +239,23 @@ ExtMove* generate_all(const Position& pos, ExtMove* moveList) { target = Type == EVASIONS ? between_bb(ksq, lsb(pos.checkers())) : Type == NON_EVASIONS ? ~pos.pieces(Us) : Type == CAPTURES ? pos.pieces(~Us) - : ~pos.pieces(); // QUIETS || QUIET_CHECKS + : ~pos.pieces(); // QUIETS moveList = generate_pawn_moves(pos, moveList, target); - moveList = generate_moves(pos, moveList, target); - moveList = generate_moves(pos, moveList, target); - moveList = generate_moves(pos, moveList, target); - moveList = generate_moves(pos, moveList, target); + moveList = generate_moves(pos, moveList, target); + moveList = generate_moves(pos, moveList, target); + moveList = generate_moves(pos, moveList, target); + moveList = generate_moves(pos, moveList, target); } - if (!Checks || pos.blockers_for_king(~Us) & ksq) - { - Bitboard b = attacks_bb(ksq) & (Type == EVASIONS ? ~pos.pieces(Us) : target); - if (Checks) - b &= ~attacks_bb(pos.square(~Us)); + Bitboard b = attacks_bb(ksq) & (Type == EVASIONS ? ~pos.pieces(Us) : target); - while (b) - *moveList++ = Move(ksq, pop_lsb(b)); + moveList = splat_moves(moveList, ksq, b); - if ((Type == QUIETS || Type == NON_EVASIONS) && pos.can_castle(Us & ANY_CASTLING)) - for (CastlingRights cr : {Us & KING_SIDE, Us & QUEEN_SIDE}) - if (!pos.castling_impeded(cr) && pos.can_castle(cr)) - *moveList++ = Move::make(ksq, pos.castling_rook_square(cr)); - } + if ((Type == QUIETS || Type == NON_EVASIONS) && pos.can_castle(Us & ANY_CASTLING)) + for (CastlingRights cr : {Us & KING_SIDE, Us & QUEEN_SIDE}) + if (!pos.castling_impeded(cr) && pos.can_castle(cr)) + *moveList++ = Move::make(ksq, pos.castling_rook_square(cr)); return moveList; } @@ -231,12 +267,10 @@ ExtMove* generate_all(const Position& pos, ExtMove* moveList) { // Generates all pseudo-legal non-captures and underpromotions // Generates all pseudo-legal check evasions // Generates all pseudo-legal captures and non-captures -// Generates all pseudo-legal non-captures giving check, -// except castling and promotions // // Returns a pointer to the end of the move list. template -ExtMove* generate(const Position& pos, ExtMove* moveList) { +Move* generate(const Position& pos, Move* moveList) { static_assert(Type != LEGAL, "Unsupported type in generate()"); assert((Type == EVASIONS) == bool(pos.checkers())); @@ -248,22 +282,20 @@ ExtMove* generate(const Position& pos, ExtMove* moveList) { } // Explicit template instantiations -template ExtMove* generate(const Position&, ExtMove*); -template ExtMove* generate(const Position&, ExtMove*); -template ExtMove* generate(const Position&, ExtMove*); -template ExtMove* generate(const Position&, ExtMove*); -template ExtMove* generate(const Position&, ExtMove*); - +template Move* generate(const Position&, Move*); +template Move* generate(const Position&, Move*); +template Move* generate(const Position&, Move*); +template Move* generate(const Position&, Move*); // generate generates all the legal moves in the given position template<> -ExtMove* generate(const Position& pos, ExtMove* moveList) { +Move* generate(const Position& pos, Move* moveList) { Color us = pos.side_to_move(); Bitboard pinned = pos.blockers_for_king(us) & pos.pieces(us); Square ksq = pos.square(us); - ExtMove* cur = moveList; + Move* cur = moveList; moveList = pos.checkers() ? generate(pos, moveList) : generate(pos, moveList); diff --git a/src/movegen.h b/src/movegen.h index 5f650d2e3..7f209f92a 100644 --- a/src/movegen.h +++ b/src/movegen.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -31,7 +31,6 @@ class Position; enum GenType { CAPTURES, QUIETS, - QUIET_CHECKS, EVASIONS, NON_EVASIONS, LEGAL @@ -50,7 +49,7 @@ struct ExtMove: public Move { inline bool operator<(const ExtMove& f, const ExtMove& s) { return f.value < s.value; } template -ExtMove* generate(const Position& pos, ExtMove* moveList); +Move* generate(const Position& pos, Move* moveList); // The MoveList struct wraps the generate() function and returns a convenient // list of moves. Using MoveList is sometimes preferable to directly calling @@ -60,13 +59,13 @@ struct MoveList { explicit MoveList(const Position& pos) : last(generate(pos, moveList)) {} - const ExtMove* begin() const { return moveList; } - const ExtMove* end() const { return last; } - size_t size() const { return last - moveList; } - bool contains(Move move) const { return std::find(begin(), end(), move) != end(); } + const Move* begin() const { return moveList; } + const Move* end() const { return last; } + size_t size() const { return last - moveList; } + bool contains(Move move) const { return std::find(begin(), end(), move) != end(); } private: - ExtMove moveList[MAX_MOVES], *last; + Move moveList[MAX_MOVES], *last; }; } // namespace Stockfish diff --git a/src/movepick.cpp b/src/movepick.cpp index c1119cf11..415d252f3 100644 --- a/src/movepick.cpp +++ b/src/movepick.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -18,12 +18,12 @@ #include "movepick.h" -#include #include -#include +#include #include #include "bitboard.h" +#include "misc.h" #include "position.h" namespace Stockfish { @@ -35,7 +35,6 @@ enum Stages { MAIN_TT, CAPTURE_INIT, GOOD_CAPTURE, - REFUTATION, QUIET_INIT, GOOD_QUIET, BAD_CAPTURE, @@ -54,13 +53,12 @@ enum Stages { // generate qsearch moves QSEARCH_TT, QCAPTURE_INIT, - QCAPTURE, - QCHECK_INIT, - QCHECK + QCAPTURE }; -// Sort moves in descending order up to and including -// a given limit. The order of moves smaller than the limit is left unspecified. + +// Sort moves in descending order up to and including a given limit. +// The order of moves smaller than the limit is left unspecified. void partial_insertion_sort(ExtMove* begin, ExtMove* end, int limit) { for (ExtMove *sortedEnd = begin, *p = begin + 1; p < end; ++p) @@ -78,56 +76,38 @@ void partial_insertion_sort(ExtMove* begin, ExtMove* end, int limit) { // Constructors of the MovePicker class. As arguments, we pass information -// to help it return the (presumably) good moves first, to decide which -// moves to return (in the quiescence search, for instance, we only want to -// search captures, promotions, and some checks) and how important a good -// move ordering is at the current node. +// to decide which class of moves to emit, to help sorting the (presumably) +// good moves first, and how important move ordering is at the current node. -// MovePicker constructor for the main search +// MovePicker constructor for the main search and for the quiescence search MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const ButterflyHistory* mh, + const LowPlyHistory* lph, const CapturePieceToHistory* cph, const PieceToHistory** ch, - const PawnHistory* ph, - Move cm, - const Move* killers) : + const SharedHistories* sh, + int pl) : pos(p), mainHistory(mh), + lowPlyHistory(lph), captureHistory(cph), continuationHistory(ch), - pawnHistory(ph), + sharedHistory(sh), ttMove(ttm), - refutations{{killers[0], 0}, {killers[1], 0}, {cm, 0}}, - depth(d) { - assert(d > 0); + depth(d), + ply(pl) { - stage = (pos.checkers() ? EVASION_TT : MAIN_TT) + !(ttm && pos.pseudo_legal(ttm)); + if (pos.checkers()) + stage = EVASION_TT + !(ttm && pos.pseudo_legal(ttm)); + + else + stage = (depth > 0 ? MAIN_TT : QSEARCH_TT) + !(ttm && pos.pseudo_legal(ttm)); } -// Constructor for quiescence search -MovePicker::MovePicker(const Position& p, - Move ttm, - Depth d, - const ButterflyHistory* mh, - const CapturePieceToHistory* cph, - const PieceToHistory** ch, - const PawnHistory* ph) : - pos(p), - mainHistory(mh), - captureHistory(cph), - continuationHistory(ch), - pawnHistory(ph), - ttMove(ttm), - depth(d) { - assert(d <= 0); - - stage = (pos.checkers() ? EVASION_TT : QSEARCH_TT) + !(ttm && pos.pseudo_legal(ttm)); -} - -// Constructor for ProbCut: we generate captures with SEE greater -// than or equal to the given threshold. +// MovePicker constructor for ProbCut: we generate captures with Static Exchange +// Evaluation (SEE) greater than or equal to the given threshold. MovePicker::MovePicker(const Position& p, Move ttm, int th, const CapturePieceToHistory* cph) : pos(p), captureHistory(cph), @@ -135,114 +115,99 @@ MovePicker::MovePicker(const Position& p, Move ttm, int th, const CapturePieceTo threshold(th) { assert(!pos.checkers()); - stage = PROBCUT_TT - + !(ttm && pos.capture_stage(ttm) && pos.pseudo_legal(ttm) && pos.see_ge(ttm, threshold)); + stage = PROBCUT_TT + !(ttm && pos.capture_stage(ttm) && pos.pseudo_legal(ttm)); } -// Assigns a numerical value to each move in a list, used -// for sorting. Captures are ordered by Most Valuable Victim (MVV), preferring -// captures with a good history. Quiets moves are ordered using the history tables. +// Assigns a numerical value to each move in a list, used for sorting. +// Captures are ordered by Most Valuable Victim (MVV), preferring captures +// with a good history. Quiets moves are ordered using the history tables. template -void MovePicker::score() { +ExtMove* MovePicker::score(MoveList& ml) { static_assert(Type == CAPTURES || Type == QUIETS || Type == EVASIONS, "Wrong type"); - [[maybe_unused]] Bitboard threatenedByPawn, threatenedByMinor, threatenedByRook, - threatenedPieces; + Color us = pos.side_to_move(); + + [[maybe_unused]] Bitboard threatByLesser[KING + 1]; if constexpr (Type == QUIETS) { - Color us = pos.side_to_move(); - - threatenedByPawn = pos.attacks_by(~us); - threatenedByMinor = - pos.attacks_by(~us) | pos.attacks_by(~us) | threatenedByPawn; - threatenedByRook = pos.attacks_by(~us) | threatenedByMinor; - - // Pieces threatened by pieces of lesser material value - threatenedPieces = (pos.pieces(us, QUEEN) & threatenedByRook) - | (pos.pieces(us, ROOK) & threatenedByMinor) - | (pos.pieces(us, KNIGHT, BISHOP) & threatenedByPawn); + threatByLesser[PAWN] = 0; + threatByLesser[KNIGHT] = threatByLesser[BISHOP] = pos.attacks_by(~us); + threatByLesser[ROOK] = + pos.attacks_by(~us) | pos.attacks_by(~us) | threatByLesser[KNIGHT]; + threatByLesser[QUEEN] = pos.attacks_by(~us) | threatByLesser[ROOK]; + threatByLesser[KING] = pos.attacks_by(~us) | threatByLesser[QUEEN]; } - for (auto& m : *this) + ExtMove* it = cur; + for (auto move : ml) + { + ExtMove& m = *it++; + m = move; + + const Square from = m.from_sq(); + const Square to = m.to_sq(); + const Piece pc = pos.moved_piece(m); + const PieceType pt = type_of(pc); + const Piece capturedPiece = pos.piece_on(to); + if constexpr (Type == CAPTURES) - m.value = - 7 * int(PieceValue[pos.piece_on(m.to_sq())]) - + (*captureHistory)[pos.moved_piece(m)][m.to_sq()][type_of(pos.piece_on(m.to_sq()))]; + m.value = (*captureHistory)[pc][to][type_of(capturedPiece)] + + 7 * int(PieceValue[capturedPiece]); else if constexpr (Type == QUIETS) { - Piece pc = pos.moved_piece(m); - PieceType pt = type_of(pc); - Square from = m.from_sq(); - Square to = m.to_sq(); - // histories - m.value = 2 * (*mainHistory)[pos.side_to_move()][m.from_to()]; - m.value += 2 * (*pawnHistory)[pawn_structure_index(pos)][pc][to]; - m.value += 2 * (*continuationHistory[0])[pc][to]; + m.value = 2 * (*mainHistory)[us][m.raw()]; + m.value += 2 * sharedHistory->pawn_entry(pos)[pc][to]; + m.value += (*continuationHistory[0])[pc][to]; m.value += (*continuationHistory[1])[pc][to]; - m.value += (*continuationHistory[2])[pc][to] / 4; + m.value += (*continuationHistory[2])[pc][to]; m.value += (*continuationHistory[3])[pc][to]; m.value += (*continuationHistory[5])[pc][to]; // bonus for checks - m.value += bool(pos.check_squares(pt) & to) * 16384; + m.value += (bool(pos.check_squares(pt) & to) && pos.see_ge(m, -75)) * 16384; - // bonus for escaping from capture - m.value += threatenedPieces & from ? (pt == QUEEN && !(to & threatenedByRook) ? 51000 - : pt == ROOK && !(to & threatenedByMinor) ? 24950 - : !(to & threatenedByPawn) ? 14450 - : 0) - : 0; + // penalty for moving to a square threatened by a lesser piece + // or bonus for escaping an attack by a lesser piece. + int v = threatByLesser[pt] & to ? -19 : 20 * bool(threatByLesser[pt] & from); + m.value += PieceValue[pt] * v; - // malus for putting piece en prise - m.value -= !(threatenedPieces & from) - ? (pt == QUEEN ? bool(to & threatenedByRook) * 48150 - + bool(to & threatenedByMinor) * 10650 - : pt == ROOK ? bool(to & threatenedByMinor) * 24500 - : pt != PAWN ? bool(to & threatenedByPawn) * 14950 - : 0) - : 0; + + if (ply < LOW_PLY_HISTORY_SIZE) + m.value += 8 * (*lowPlyHistory)[ply][m.raw()] / (1 + ply); } else // Type == EVASIONS { if (pos.capture_stage(m)) - m.value = - PieceValue[pos.piece_on(m.to_sq())] - type_of(pos.moved_piece(m)) + (1 << 28); + m.value = PieceValue[capturedPiece] + (1 << 28); else - m.value = (*mainHistory)[pos.side_to_move()][m.from_to()] - + (*continuationHistory[0])[pos.moved_piece(m)][m.to_sq()] - + (*pawnHistory)[pawn_structure_index(pos)][pos.moved_piece(m)][m.to_sq()]; + m.value = (*mainHistory)[us][m.raw()] + (*continuationHistory[0])[pc][to]; } + } + return it; } // Returns the next move satisfying a predicate function. -// It never returns the TT move. -template +// This never returns the TT move, as it was emitted before. +template Move MovePicker::select(Pred filter) { - while (cur < endMoves) - { - if constexpr (T == Best) - std::swap(*cur, *std::max_element(cur, endMoves)); - + for (; cur < endCur; ++cur) if (*cur != ttMove && filter()) return *cur++; - cur++; - } return Move::none(); } -// Most important method of the MovePicker class. It -// returns a new pseudo-legal move every time it is called until there are no more -// moves left, picking the move with the highest score from a list of generated moves. -Move MovePicker::next_move(bool skipQuiets) { - - auto quiet_threshold = [](Depth d) { return -3550 * d; }; +// This is the most important method of the MovePicker class. We emit one +// new pseudo-legal move on every call until there are no more moves left, +// picking the move with the highest score from a list of generated moves. +Move MovePicker::next_move() { + constexpr int goodQuietThreshold = -14000; top: switch (stage) { @@ -256,131 +221,93 @@ top: case CAPTURE_INIT : case PROBCUT_INIT : - case QCAPTURE_INIT : - cur = endBadCaptures = moves; - endMoves = generate(pos, cur); + case QCAPTURE_INIT : { + MoveList ml(pos); - score(); - partial_insertion_sort(cur, endMoves, std::numeric_limits::min()); + cur = endBadCaptures = moves; + endCur = endCaptures = score(ml); + + partial_insertion_sort(cur, endCur, std::numeric_limits::min()); ++stage; goto top; + } case GOOD_CAPTURE : - if (select([&]() { - // Move losing capture to endBadCaptures to be tried later - return pos.see_ge(*cur, -cur->value / 18) ? true - : (*endBadCaptures++ = *cur, false); + if (select([&]() { + if (pos.see_ge(*cur, -cur->value / 18)) + return true; + std::swap(*endBadCaptures++, *cur); + return false; })) return *(cur - 1); - // Prepare the pointers to loop over the refutations array - cur = std::begin(refutations); - endMoves = std::end(refutations); - - // If the countermove is the same as a killer, skip it - if (refutations[0] == refutations[2] || refutations[1] == refutations[2]) - --endMoves; - - ++stage; - [[fallthrough]]; - - case REFUTATION : - if (select([&]() { - return *cur != Move::none() && !pos.capture_stage(*cur) && pos.pseudo_legal(*cur); - })) - return *(cur - 1); ++stage; [[fallthrough]]; case QUIET_INIT : if (!skipQuiets) { - cur = endBadCaptures; - endMoves = beginBadQuiets = endBadQuiets = generate(pos, cur); + MoveList ml(pos); - score(); - partial_insertion_sort(cur, endMoves, quiet_threshold(depth)); + endCur = endGenerated = score(ml); + + partial_insertion_sort(cur, endCur, -3560 * depth); } ++stage; [[fallthrough]]; case GOOD_QUIET : - if (!skipQuiets && select([&]() { - return *cur != refutations[0] && *cur != refutations[1] && *cur != refutations[2]; - })) - { - if ((cur - 1)->value > -8000 || (cur - 1)->value <= quiet_threshold(depth)) - return *(cur - 1); - - // Remaining quiets are bad - beginBadQuiets = cur - 1; - } + if (!skipQuiets && select([&]() { return cur->value > goodQuietThreshold; })) + return *(cur - 1); // Prepare the pointers to loop over the bad captures - cur = moves; - endMoves = endBadCaptures; + cur = moves; + endCur = endBadCaptures; ++stage; [[fallthrough]]; case BAD_CAPTURE : - if (select([]() { return true; })) + if (select([]() { return true; })) return *(cur - 1); - // Prepare the pointers to loop over the bad quiets - cur = beginBadQuiets; - endMoves = endBadQuiets; + // Prepare the pointers to loop over quiets again + cur = endCaptures; + endCur = endGenerated; ++stage; [[fallthrough]]; case BAD_QUIET : if (!skipQuiets) - return select([&]() { - return *cur != refutations[0] && *cur != refutations[1] && *cur != refutations[2]; - }); + return select([&]() { return cur->value <= goodQuietThreshold; }); return Move::none(); - case EVASION_INIT : - cur = moves; - endMoves = generate(pos, cur); + case EVASION_INIT : { + MoveList ml(pos); - score(); + cur = moves; + endCur = endGenerated = score(ml); + + partial_insertion_sort(cur, endCur, std::numeric_limits::min()); ++stage; [[fallthrough]]; + } case EVASION : - return select([]() { return true; }); + case QCAPTURE : + return select([]() { return true; }); case PROBCUT : - return select([&]() { return pos.see_ge(*cur, threshold); }); - - case QCAPTURE : - if (select([]() { return true; })) - return *(cur - 1); - - // If we did not find any move and we do not try checks, we have finished - if (depth != DEPTH_QS_CHECKS) - return Move::none(); - - ++stage; - [[fallthrough]]; - - case QCHECK_INIT : - cur = moves; - endMoves = generate(pos, cur); - - ++stage; - [[fallthrough]]; - - case QCHECK : - return select([]() { return true; }); + return select([&]() { return pos.see_ge(*cur, threshold); }); } assert(false); return Move::none(); // Silence warning } +void MovePicker::skip_quiet_moves() { skipQuiets = true; } + } // namespace Stockfish diff --git a/src/movepick.h b/src/movepick.h index b81f76e18..08bd9a539 100644 --- a/src/movepick.h +++ b/src/movepick.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -19,141 +19,22 @@ #ifndef MOVEPICK_H_INCLUDED #define MOVEPICK_H_INCLUDED -#include -#include -#include -#include -#include -#include -#include -#include // IWYU pragma: keep - +#include "history.h" #include "movegen.h" -#include "position.h" #include "types.h" namespace Stockfish { -constexpr int PAWN_HISTORY_SIZE = 512; // has to be a power of 2 -constexpr int CORRECTION_HISTORY_SIZE = 16384; // has to be a power of 2 -constexpr int CORRECTION_HISTORY_LIMIT = 1024; +class Position; -static_assert((PAWN_HISTORY_SIZE & (PAWN_HISTORY_SIZE - 1)) == 0, - "PAWN_HISTORY_SIZE has to be a power of 2"); - -static_assert((CORRECTION_HISTORY_SIZE & (CORRECTION_HISTORY_SIZE - 1)) == 0, - "CORRECTION_HISTORY_SIZE has to be a power of 2"); - -enum PawnHistoryType { - Normal, - Correction -}; - -template -inline int pawn_structure_index(const Position& pos) { - return pos.pawn_key() & ((T == Normal ? PAWN_HISTORY_SIZE : CORRECTION_HISTORY_SIZE) - 1); -} - -// StatsEntry stores the stat table value. It is usually a number but could -// be a move or even a nested history. We use a class instead of a naked value -// to directly call history update operator<<() on the entry so to use stats -// tables at caller sites as simple multi-dim arrays. -template -class StatsEntry { - - T entry; - - public: - void operator=(const T& v) { entry = v; } - T* operator&() { return &entry; } - T* operator->() { return &entry; } - operator const T&() const { return entry; } - - void operator<<(int bonus) { - static_assert(D <= std::numeric_limits::max(), "D overflows T"); - - // Make sure that bonus is in range [-D, D] - int clampedBonus = std::clamp(bonus, -D, D); - entry += clampedBonus - entry * std::abs(clampedBonus) / D; - - assert(std::abs(entry) <= D); - } -}; - -// Stats is a generic N-dimensional array used to store various statistics. -// The first template parameter T is the base type of the array, and the second -// template parameter D limits the range of updates in [-D, D] when we update -// values with the << operator, while the last parameters (Size and Sizes) -// encode the dimensions of the array. -template -struct Stats: public std::array, Size> { - using stats = Stats; - - void fill(const T& v) { - - // For standard-layout 'this' points to the first struct member - assert(std::is_standard_layout_v); - - using entry = StatsEntry; - entry* p = reinterpret_cast(this); - std::fill(p, p + sizeof(*this) / sizeof(entry), v); - } -}; - -template -struct Stats: public std::array, Size> {}; - -// In stats table, D=0 means that the template parameter is not used -enum StatsParams { - NOT_USED = 0 -}; -enum StatsType { - NoCaptures, - Captures -}; - -// ButterflyHistory records how often quiet moves have been successful or unsuccessful -// during the current search, and is used for reduction and move ordering decisions. -// It uses 2 tables (one for each color) indexed by the move's from and to squares, -// see www.chessprogramming.org/Butterfly_Boards (~11 elo) -using ButterflyHistory = Stats; - -// CounterMoveHistory stores counter moves indexed by [piece][to] of the previous -// move, see www.chessprogramming.org/Countermove_Heuristic -using CounterMoveHistory = Stats; - -// CapturePieceToHistory is addressed by a move's [piece][to][captured piece type] -using CapturePieceToHistory = Stats; - -// PieceToHistory is like ButterflyHistory but is addressed by a move's [piece][to] -using PieceToHistory = Stats; - -// ContinuationHistory is the combined history of a given pair of moves, usually -// the current one given a previous one. The nested history table is based on -// PieceToHistory instead of ButterflyBoards. -// (~63 elo) -using ContinuationHistory = Stats; - -// PawnHistory is addressed by the pawn structure and a move's [piece][to] -using PawnHistory = Stats; - -// CorrectionHistory is addressed by color and pawn structure -using CorrectionHistory = - Stats; - -// MovePicker class is used to pick one pseudo-legal move at a time from the -// current position. The most important method is next_move(), which returns a -// new pseudo-legal move each time it is called, until there are no moves left, -// when Move::none() is returned. In order to improve the efficiency of the -// alpha-beta algorithm, MovePicker attempts to return the moves which are most -// likely to get a cut-off first. +// The MovePicker class is used to pick one pseudo-legal move at a time from the +// current position. The most important method is next_move(), which emits one +// new pseudo-legal move on every call, until there are no moves left, when +// Move::none() is returned. In order to improve the efficiency of the alpha-beta +// algorithm, MovePicker attempts to return the moves which are most likely to get +// a cut-off first. class MovePicker { - enum PickType { - Next, - Best - }; - public: MovePicker(const MovePicker&) = delete; MovePicker& operator=(const MovePicker&) = delete; @@ -161,40 +42,37 @@ class MovePicker { Move, Depth, const ButterflyHistory*, + const LowPlyHistory*, const CapturePieceToHistory*, const PieceToHistory**, - const PawnHistory*, - Move, - const Move*); - MovePicker(const Position&, - Move, - Depth, - const ButterflyHistory*, - const CapturePieceToHistory*, - const PieceToHistory**, - const PawnHistory*); + const SharedHistories*, + int); MovePicker(const Position&, Move, int, const CapturePieceToHistory*); - Move next_move(bool skipQuiets = false); + Move next_move(); + void skip_quiet_moves(); private: - template + template Move select(Pred); - template - void score(); + template + ExtMove* score(MoveList&); ExtMove* begin() { return cur; } - ExtMove* end() { return endMoves; } + ExtMove* end() { return endCur; } const Position& pos; const ButterflyHistory* mainHistory; + const LowPlyHistory* lowPlyHistory; const CapturePieceToHistory* captureHistory; const PieceToHistory** continuationHistory; - const PawnHistory* pawnHistory; + const SharedHistories* sharedHistory; Move ttMove; - ExtMove refutations[3], *cur, *endMoves, *endBadCaptures, *beginBadQuiets, *endBadQuiets; - int stage; - int threshold; - Depth depth; - ExtMove moves[MAX_MOVES]; + ExtMove * cur, *endCur, *endBadCaptures, *endCaptures, *endGenerated; + int stage; + int threshold; + Depth depth; + int ply; + bool skipQuiets = false; + ExtMove moves[MAX_MOVES]; }; } // namespace Stockfish diff --git a/src/nnue/features/full_threats.cpp b/src/nnue/features/full_threats.cpp new file mode 100644 index 000000000..4e3ba81cf --- /dev/null +++ b/src/nnue/features/full_threats.cpp @@ -0,0 +1,361 @@ +/* + 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 . +*/ + +//Definition of input features FullThreats of NNUE evaluation function + +#include "full_threats.h" + +#include +#include +#include +#include + +#include "../../bitboard.h" +#include "../../misc.h" +#include "../../position.h" +#include "../../types.h" +#include "../nnue_common.h" + +namespace Stockfish::Eval::NNUE::Features { + +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(data); } + + constexpr IndexType feature_index_base() const { return static_cast(data >> 8); } +}; + +constexpr std::array 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, +}; + +template +constexpr auto make_piece_indices_type() { + static_assert(PT != PieceType::PAWN); + + std::array, SQUARE_NB> out{}; + + for (Square from = SQ_A1; from <= SQ_H8; ++from) + { + Bitboard attacks = PseudoAttacks[PT][from]; + + for (Square to = SQ_A1; to <= SQ_H8; ++to) + { + out[from][to] = constexpr_popcount(((1ULL << to) - 1) & attacks); + } + } + + return out; +} + +template +constexpr auto make_piece_indices_piece() { + static_assert(type_of(P) == PieceType::PAWN); + + std::array, SQUARE_NB> out{}; + + constexpr Color C = color_of(P); + + for (Square from = SQ_A1; from <= SQ_H8; ++from) + { + Bitboard attacks = PseudoAttacks[C][from]; + + for (Square to = SQ_A1; to <= SQ_H8; ++to) + { + out[from][to] = constexpr_popcount(((1ULL << to) - 1) & attacks); + } + } + + return out; +} + +constexpr auto index_lut2_array() { + constexpr auto KNIGHT_ATTACKS = make_piece_indices_type(); + constexpr auto BISHOP_ATTACKS = make_piece_indices_type(); + constexpr auto ROOK_ATTACKS = make_piece_indices_type(); + constexpr auto QUEEN_ATTACKS = make_piece_indices_type(); + constexpr auto KING_ATTACKS = make_piece_indices_type(); + + std::array, SQUARE_NB>, PIECE_NB> indices{}; + + indices[W_PAWN] = make_piece_indices_piece(); + indices[B_PAWN] = make_piece_indices_piece(); + + indices[W_KNIGHT] = KNIGHT_ATTACKS; + indices[B_KNIGHT] = KNIGHT_ATTACKS; + + indices[W_BISHOP] = BISHOP_ATTACKS; + indices[B_BISHOP] = BISHOP_ATTACKS; + + indices[W_ROOK] = ROOK_ATTACKS; + indices[B_ROOK] = ROOK_ATTACKS; + + indices[W_QUEEN] = QUEEN_ATTACKS; + indices[B_QUEEN] = QUEEN_ATTACKS; + + indices[W_KING] = KING_ATTACKS; + indices[B_KING] = KING_ATTACKS; + + return indices; +} + +constexpr auto init_threat_offsets() { + std::array indices{}; + std::array, PIECE_NB> offsets{}; + + int cumulativeOffset = 0; + for (Piece piece : AllPieces) + { + int pieceIdx = piece; + int cumulativePieceOffset = 0; + + for (Square from = SQ_A1; from <= SQ_H8; ++from) + { + offsets[pieceIdx][from] = cumulativePieceOffset; + + if (type_of(piece) != PAWN) + { + Bitboard attacks = PseudoAttacks[type_of(piece)][from]; + cumulativePieceOffset += constexpr_popcount(attacks); + } + + else if (from >= SQ_A2 && from <= SQ_H7) + { + Bitboard attacks = (pieceIdx < 8) ? pawn_attacks_bb(square_bb(from)) + : pawn_attacks_bb(square_bb(from)); + cumulativePieceOffset += constexpr_popcount(attacks); + } + } + + indices[pieceIdx] = {cumulativePieceOffset, cumulativeOffset}; + + cumulativeOffset += numValidTargets[pieceIdx] * cumulativePieceOffset; + } + + return std::pair{indices, offsets}; +} + +constexpr auto helper_offsets = init_threat_offsets().first; +// Lookup array for indexing threats +constexpr auto offsets = init_threat_offsets().second; + +constexpr auto init_index_luts() { + std::array, PIECE_NB> indices{}; + + for (Piece attacker : AllPieces) + { + for (Piece attacked : AllPieces) + { + bool enemy = (attacker ^ attacked) == 8; + PieceType attackerType = type_of(attacker); + PieceType attackedType = type_of(attacked); + + int map = FullThreats::map[attackerType - 1][attackedType - 1]; + bool semi_excluded = attackerType == attackedType && (enemy || attackerType != PAWN); + IndexType feature = helper_offsets[attacker].cumulativeOffset + + (color_of(attacked) * (numValidTargets[attacker] / 2) + map) + * helper_offsets[attacker].cumulativePieceOffset; + + bool excluded = map < 0; + indices[attacker][attacked] = PiecePairData(excluded, semi_excluded, feature); + } + } + + return indices; +} + +// The final index is calculated from summing data found in these two LUTs, as well +// as offsets[attacker][from] + +// [attacker][attacked] +constexpr auto index_lut1 = init_index_luts(); +// [attacker][from][to] +constexpr auto index_lut2 = index_lut2_array(); + +// Index of a feature for a given king position and another piece on some square +inline sf_always_inline IndexType FullThreats::make_index( + Color perspective, Piece attacker, Square from, Square to, Piece attacked, Square ksq) { + const std::int8_t orientation = OrientTBL[ksq] ^ (56 * perspective); + unsigned from_oriented = uint8_t(from) ^ orientation; + unsigned to_oriented = uint8_t(to) ^ orientation; + + std::int8_t swap = 8 * perspective; + 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; +} + +// Get a list of indices for active features in ascending order + +void FullThreats::append_active_indices(Color perspective, const Position& pos, IndexList& active) { + Square ksq = pos.square(perspective); + Bitboard occupied = pos.pieces(); + + for (Color color : {WHITE, BLACK}) + { + for (PieceType pt = PAWN; pt <= KING; ++pt) + { + Color c = Color(perspective ^ color); + Piece attacker = make_piece(c, pt); + Bitboard bb = pos.pieces(c, pt); + + if (pt == PAWN) + { + auto right = (c == WHITE) ? NORTH_EAST : SOUTH_WEST; + auto left = (c == WHITE) ? NORTH_WEST : SOUTH_EAST; + auto attacks_left = + ((c == WHITE) ? shift(bb) : shift(bb)) & occupied; + auto attacks_right = + ((c == WHITE) ? shift(bb) : shift(bb)) & occupied; + + while (attacks_left) + { + Square to = pop_lsb(attacks_left); + Square from = to - right; + Piece attacked = pos.piece_on(to); + IndexType index = make_index(perspective, attacker, from, to, attacked, ksq); + + if (index < Dimensions) + active.push_back(index); + } + + while (attacks_right) + { + Square to = pop_lsb(attacks_right); + Square from = to - left; + Piece attacked = pos.piece_on(to); + IndexType index = make_index(perspective, attacker, from, to, attacked, ksq); + + if (index < Dimensions) + active.push_back(index); + } + } + else + { + while (bb) + { + Square from = pop_lsb(bb); + Bitboard attacks = (attacks_bb(pt, from, occupied)) & occupied; + + while (attacks) + { + Square to = pop_lsb(attacks); + Piece attacked = pos.piece_on(to); + IndexType index = + make_index(perspective, attacker, from, to, attacked, ksq); + + if (index < Dimensions) + active.push_back(index); + } + } + } + } + } +} + +// Get a list of indices for recently changed features + +void FullThreats::append_changed_indices(Color perspective, + Square ksq, + const DiffType& diff, + IndexList& removed, + IndexList& added, + FusedUpdateData* fusedData, + bool first) { + + for (const auto& dirty : diff.list) + { + auto attacker = dirty.pc(); + auto attacked = dirty.threatened_pc(); + auto from = dirty.pc_sq(); + auto to = dirty.threatened_sq(); + auto add = dirty.add(); + + if (fusedData) + { + if (from == fusedData->dp2removed) + { + if (add) + { + if (first) + { + fusedData->dp2removedOriginBoard |= to; + continue; + } + } + else if (fusedData->dp2removedOriginBoard & to) + continue; + } + + if (to != SQ_NONE && to == fusedData->dp2removed) + { + if (add) + { + if (first) + { + fusedData->dp2removedTargetBoard |= from; + continue; + } + } + else if (fusedData->dp2removedTargetBoard & from) + continue; + } + } + + auto& insert = add ? added : removed; + const IndexType index = make_index(perspective, attacker, from, to, attacked, ksq); + + if (index < Dimensions) + insert.push_back(index); + } +} + +bool FullThreats::requires_refresh(const DiffType& diff, Color perspective) { + return perspective == diff.us && (int8_t(diff.ksq) & 0b100) != (int8_t(diff.prevKsq) & 0b100); +} + +} // namespace Stockfish::Eval::NNUE::Features diff --git a/src/nnue/features/full_threats.h b/src/nnue/features/full_threats.h new file mode 100644 index 000000000..5b2582954 --- /dev/null +++ b/src/nnue/features/full_threats.h @@ -0,0 +1,104 @@ +/* + 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 . +*/ + +//Definition of input features Simplified_Threats of NNUE evaluation function + +#ifndef NNUE_FEATURES_FULL_THREATS_INCLUDED +#define NNUE_FEATURES_FULL_THREATS_INCLUDED + +#include + +#include "../../misc.h" +#include "../../types.h" +#include "../nnue_common.h" + +namespace Stockfish { +class Position; +} + +namespace Stockfish::Eval::NNUE::Features { + +static constexpr int numValidTargets[PIECE_NB] = {0, 6, 12, 10, 10, 12, 8, 0, + 0, 6, 12, 10, 10, 12, 8, 0}; + +class FullThreats { + public: + // Feature name + static constexpr const char* Name = "Full_Threats(Friend)"; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t HashValue = 0x8f234cb8u; + + // Number of feature dimensions + static constexpr IndexType Dimensions = 79856; + + // clang-format off + // Orient a square according to perspective (rotates by 180 for black) + static constexpr std::int8_t OrientTBL[SQUARE_NB] = { + SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, + SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, + SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, + SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, + SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, + SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, + SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, + SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, + }; + + static constexpr int map[PIECE_TYPE_NB-2][PIECE_TYPE_NB-2] = { + {0, 1, -1, 2, -1, -1}, + {0, 1, 2, 3, 4, 5}, + {0, 1, 2, 3, -1, 4}, + {0, 1, 2, 3, -1, 4}, + {0, 1, 2, 3, 4, 5}, + {0, 1, 2, 3, -1, -1} + }; + // clang-format on + + struct FusedUpdateData { + Bitboard dp2removedOriginBoard = 0; + Bitboard dp2removedTargetBoard = 0; + + Square dp2removed; + }; + + // Maximum number of simultaneously active features. + static constexpr IndexType MaxActiveDimensions = 128; + using IndexList = ValueList; + using DiffType = DirtyThreats; + + static IndexType + make_index(Color perspective, Piece attkr, Square from, Square to, Piece attkd, Square ksq); + + // Get a list of indices for active features + static void append_active_indices(Color perspective, const Position& pos, IndexList& active); + + // Get a list of indices for recently changed features + static void append_changed_indices(Color perspective, + Square ksq, + const DiffType& diff, + IndexList& removed, + IndexList& added, + FusedUpdateData* fd = nullptr, + bool first = false); + + // Returns whether the change stored in this DirtyPiece means + // that a full accumulator refresh is required. + static bool requires_refresh(const DiffType& diff, Color perspective); +}; + +} // namespace Stockfish::Eval::NNUE::Features + +#endif // #ifndef NNUE_FEATURES_FULL_THREATS_INCLUDED diff --git a/src/nnue/features/half_ka_v2_hm.cpp b/src/nnue/features/half_ka_v2_hm.cpp index 5789db484..a82e89de4 100644 --- a/src/nnue/features/half_ka_v2_hm.cpp +++ b/src/nnue/features/half_ka_v2_hm.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -28,59 +28,42 @@ namespace Stockfish::Eval::NNUE::Features { // Index of a feature for a given king position and another piece on some square -template -inline IndexType HalfKAv2_hm::make_index(Square s, Piece pc, Square ksq) { - return IndexType((int(s) ^ OrientTBL[Perspective][ksq]) + PieceSquareIndex[Perspective][pc] - + KingBuckets[Perspective][ksq]); + +IndexType HalfKAv2_hm::make_index(Color perspective, Square s, Piece pc, Square ksq) { + const IndexType flip = 56 * perspective; + return (IndexType(s) ^ OrientTBL[ksq] ^ flip) + PieceSquareIndex[perspective][pc] + + KingBuckets[int(ksq) ^ flip]; } // Get a list of indices for active features -template -void HalfKAv2_hm::append_active_indices(const Position& pos, IndexList& active) { - Square ksq = pos.square(Perspective); + +void HalfKAv2_hm::append_active_indices(Color perspective, const Position& pos, IndexList& active) { + Square ksq = pos.square(perspective); Bitboard bb = pos.pieces(); while (bb) { Square s = pop_lsb(bb); - active.push_back(make_index(s, pos.piece_on(s), ksq)); + active.push_back(make_index(perspective, s, pos.piece_on(s), ksq)); } } -// Explicit template instantiations -template void HalfKAv2_hm::append_active_indices(const Position& pos, IndexList& active); -template void HalfKAv2_hm::append_active_indices(const Position& pos, IndexList& active); - // Get a list of indices for recently changed features -template -void HalfKAv2_hm::append_changed_indices(Square ksq, - const DirtyPiece& dp, - IndexList& removed, - IndexList& added) { - for (int i = 0; i < dp.dirty_num; ++i) - { - if (dp.from[i] != SQ_NONE) - removed.push_back(make_index(dp.from[i], dp.piece[i], ksq)); - if (dp.to[i] != SQ_NONE) - added.push_back(make_index(dp.to[i], dp.piece[i], ksq)); - } + +void HalfKAv2_hm::append_changed_indices( + Color perspective, Square ksq, const DiffType& diff, IndexList& removed, IndexList& added) { + removed.push_back(make_index(perspective, diff.from, diff.pc, ksq)); + if (diff.to != SQ_NONE) + added.push_back(make_index(perspective, diff.to, diff.pc, ksq)); + + if (diff.remove_sq != SQ_NONE) + removed.push_back(make_index(perspective, diff.remove_sq, diff.remove_pc, ksq)); + + if (diff.add_sq != SQ_NONE) + added.push_back(make_index(perspective, diff.add_sq, diff.add_pc, ksq)); } -// Explicit template instantiations -template void HalfKAv2_hm::append_changed_indices(Square ksq, - const DirtyPiece& dp, - IndexList& removed, - IndexList& added); -template void HalfKAv2_hm::append_changed_indices(Square ksq, - const DirtyPiece& dp, - IndexList& removed, - IndexList& added); - -int HalfKAv2_hm::update_cost(const StateInfo* st) { return st->dirtyPiece.dirty_num; } - -int HalfKAv2_hm::refresh_cost(const Position& pos) { return pos.count(); } - -bool HalfKAv2_hm::requires_refresh(const StateInfo* st, Color perspective) { - return st->dirtyPiece.piece[0] == make_piece(perspective, KING); +bool HalfKAv2_hm::requires_refresh(const DiffType& diff, Color perspective) { + return diff.pc == make_piece(perspective, KING); } } // namespace Stockfish::Eval::NNUE::Features diff --git a/src/nnue/features/half_ka_v2_hm.h b/src/nnue/features/half_ka_v2_hm.h index 8363184f4..49b0a87a4 100644 --- a/src/nnue/features/half_ka_v2_hm.h +++ b/src/nnue/features/half_ka_v2_hm.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -28,7 +28,6 @@ #include "../nnue_common.h" namespace Stockfish { -struct StateInfo; class Position; } @@ -63,10 +62,6 @@ class HalfKAv2_hm { {PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_KING, PS_NONE, PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_KING, PS_NONE}}; - // Index of a feature for a given king position and another piece on some square - template - static IndexType make_index(Square s, Piece pc, Square ksq); - public: // Feature name static constexpr const char* Name = "HalfKAv2_hm(Friend)"; @@ -80,69 +75,52 @@ class HalfKAv2_hm { #define B(v) (v * PS_NB) // clang-format off - static constexpr int KingBuckets[COLOR_NB][SQUARE_NB] = { - { B(28), B(29), B(30), B(31), B(31), B(30), B(29), B(28), + static constexpr IndexType KingBuckets[SQUARE_NB] = { + B(28), B(29), B(30), B(31), B(31), B(30), B(29), B(28), B(24), B(25), B(26), B(27), B(27), B(26), B(25), B(24), B(20), B(21), B(22), B(23), B(23), B(22), B(21), B(20), B(16), B(17), B(18), B(19), B(19), B(18), B(17), B(16), B(12), B(13), B(14), B(15), B(15), B(14), B(13), B(12), B( 8), B( 9), B(10), B(11), B(11), B(10), B( 9), B( 8), B( 4), B( 5), B( 6), B( 7), B( 7), B( 6), B( 5), B( 4), - B( 0), B( 1), B( 2), B( 3), B( 3), B( 2), B( 1), B( 0) }, - { B( 0), B( 1), B( 2), B( 3), B( 3), B( 2), B( 1), B( 0), - B( 4), B( 5), B( 6), B( 7), B( 7), B( 6), B( 5), B( 4), - B( 8), B( 9), B(10), B(11), B(11), B(10), B( 9), B( 8), - B(12), B(13), B(14), B(15), B(15), B(14), B(13), B(12), - B(16), B(17), B(18), B(19), B(19), B(18), B(17), B(16), - B(20), B(21), B(22), B(23), B(23), B(22), B(21), B(20), - B(24), B(25), B(26), B(27), B(27), B(26), B(25), B(24), - B(28), B(29), B(30), B(31), B(31), B(30), B(29), B(28) } + B( 0), B( 1), B( 2), B( 3), B( 3), B( 2), B( 1), B( 0), }; // clang-format on #undef B // clang-format off // Orient a square according to perspective (rotates by 180 for black) - static constexpr int OrientTBL[COLOR_NB][SQUARE_NB] = { - { SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + static constexpr IndexType OrientTBL[SQUARE_NB] = { SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, - SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1 }, - { SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, - SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, - SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, - SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, - SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, - SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, - SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, - SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8 } + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1 , }; // clang-format on // Maximum number of simultaneously active features. static constexpr IndexType MaxActiveDimensions = 32; using IndexList = ValueList; + using DiffType = DirtyPiece; + + // Index of a feature for a given king position and another piece on some square + + static IndexType make_index(Color perspective, Square s, Piece pc, Square ksq); // Get a list of indices for active features - template - static void append_active_indices(const Position& pos, IndexList& active); + + static void append_active_indices(Color perspective, const Position& pos, IndexList& active); // Get a list of indices for recently changed features - template - static void - append_changed_indices(Square ksq, const DirtyPiece& dp, IndexList& removed, IndexList& added); + static void append_changed_indices( + Color perspective, Square ksq, const DiffType& diff, IndexList& removed, IndexList& added); - // Returns the cost of updating one perspective, the most costly one. - // Assumes no refresh needed. - static int update_cost(const StateInfo* st); - static int refresh_cost(const Position& pos); - - // Returns whether the change stored in this StateInfo means + // Returns whether the change stored in this DirtyPiece means // that a full accumulator refresh is required. - static bool requires_refresh(const StateInfo* st, Color perspective); + static bool requires_refresh(const DiffType& diff, Color perspective); }; } // namespace Stockfish::Eval::NNUE::Features diff --git a/src/nnue/layers/affine_transform.h b/src/nnue/layers/affine_transform.h index ad9167c05..a3d072f67 100644 --- a/src/nnue/layers/affine_transform.h +++ b/src/nnue/layers/affine_transform.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -25,7 +25,7 @@ #include #include "../nnue_common.h" -#include "simd.h" +#include "../simd.h" /* This file contains the definition for a fully connected layer (aka affine transform). @@ -39,25 +39,26 @@ namespace Stockfish::Eval::NNUE::Layers { +#if defined(USE_SSSE3) || defined(USE_NEON_DOTPROD) + #define ENABLE_SEQ_OPT +#endif + // Fallback implementation for older/other architectures. // Requires the input to be padded to at least 16 values. -#if !defined(USE_SSSE3) +#ifndef ENABLE_SEQ_OPT + template static void affine_transform_non_ssse3(std::int32_t* output, const std::int8_t* weights, const std::int32_t* biases, const std::uint8_t* input) { - #if defined(USE_SSE2) || defined(USE_NEON_DOTPROD) || defined(USE_NEON) + #if defined(USE_SSE2) || defined(USE_NEON) #if defined(USE_SSE2) // At least a multiple of 16, with SSE2. constexpr IndexType NumChunks = ceil_to_multiple(InputDimensions, 16) / 16; const __m128i Zeros = _mm_setzero_si128(); const auto inputVector = reinterpret_cast(input); - #elif defined(USE_NEON_DOTPROD) - constexpr IndexType NumChunks = ceil_to_multiple(InputDimensions, 16) / 16; - const auto inputVector = reinterpret_cast(input); - #elif defined(USE_NEON) constexpr IndexType NumChunks = ceil_to_multiple(InputDimensions, 16) / 16; const auto inputVector = reinterpret_cast(input); @@ -91,16 +92,8 @@ static void affine_transform_non_ssse3(std::int32_t* output, sum = _mm_add_epi32(sum, sum_second_32); output[i] = _mm_cvtsi128_si32(sum); - #elif defined(USE_NEON_DOTPROD) - int32x4_t sum = {biases[i]}; - const auto row = reinterpret_cast(&weights[offset]); - for (IndexType j = 0; j < NumChunks; ++j) - { - sum = vdotq_s32(sum, inputVector[j], row[j]); - } - output[i] = vaddvq_s32(sum); - #elif defined(USE_NEON) + int32x4_t sum = {biases[i]}; const auto row = reinterpret_cast(&weights[offset]); for (IndexType j = 0; j < NumChunks; ++j) @@ -109,7 +102,7 @@ static void affine_transform_non_ssse3(std::int32_t* output, product = vmlal_s8(product, inputVector[j * 2 + 1], row[j * 2 + 1]); sum = vpadalq_s16(sum, product); } - output[i] = sum[0] + sum[1] + sum[2] + sum[3]; + output[i] = SIMD::neon_m128_reduce_add_epi32(sum); #endif } @@ -127,7 +120,8 @@ static void affine_transform_non_ssse3(std::int32_t* output, } #endif } -#endif + +#endif // !ENABLE_SEQ_OPT template class AffineTransform { @@ -162,7 +156,7 @@ class AffineTransform { } static constexpr IndexType get_weight_index(IndexType i) { -#if defined(USE_SSSE3) +#ifdef ENABLE_SEQ_OPT return get_weight_index_scrambled(i); #else return i; @@ -187,32 +181,40 @@ class AffineTransform { return !stream.fail(); } + + std::size_t get_content_hash() const { + std::size_t h = 0; + hash_combine(h, get_raw_data_hash(biases)); + hash_combine(h, get_raw_data_hash(weights)); + hash_combine(h, get_hash_value(0)); + return h; + } + // Forward propagation void propagate(const InputType* input, OutputType* output) const { -#if defined(USE_SSSE3) +#ifdef ENABLE_SEQ_OPT if constexpr (OutputDimensions > 1) { - #if defined(USE_AVX512) using vec_t = __m512i; - #define vec_setzero _mm512_setzero_si512 #define vec_set_32 _mm512_set1_epi32 - #define vec_add_dpbusd_32 Simd::m512_add_dpbusd_epi32 - #define vec_hadd Simd::m512_hadd + #define vec_add_dpbusd_32 SIMD::m512_add_dpbusd_epi32 #elif defined(USE_AVX2) using vec_t = __m256i; - #define vec_setzero _mm256_setzero_si256 #define vec_set_32 _mm256_set1_epi32 - #define vec_add_dpbusd_32 Simd::m256_add_dpbusd_epi32 - #define vec_hadd Simd::m256_hadd + #define vec_add_dpbusd_32 SIMD::m256_add_dpbusd_epi32 #elif defined(USE_SSSE3) using vec_t = __m128i; - #define vec_setzero _mm_setzero_si128 #define vec_set_32 _mm_set1_epi32 - #define vec_add_dpbusd_32 Simd::m128_add_dpbusd_epi32 - #define vec_hadd Simd::m128_hadd + #define vec_add_dpbusd_32 SIMD::m128_add_dpbusd_epi32 + #elif defined(USE_NEON_DOTPROD) + using vec_t = int32x4_t; + #define vec_set_32 vdupq_n_s32 + #define vec_add_dpbusd_32(acc, a, b) \ + SIMD::dotprod_m128_add_dpbusd_epi32(acc, vreinterpretq_s8_s32(a), \ + vreinterpretq_s8_s32(b)) #endif static constexpr IndexType OutputSimdWidth = sizeof(vec_t) / sizeof(OutputType); @@ -242,28 +244,30 @@ class AffineTransform { for (IndexType k = 0; k < NumRegs; ++k) outptr[k] = acc[k]; - #undef vec_setzero #undef vec_set_32 #undef vec_add_dpbusd_32 - #undef vec_hadd } else if constexpr (OutputDimensions == 1) { - // We cannot use AVX512 for the last layer because there are only 32 inputs // and the buffer is not padded to 64 elements. #if defined(USE_AVX2) using vec_t = __m256i; - #define vec_setzero _mm256_setzero_si256 - #define vec_set_32 _mm256_set1_epi32 - #define vec_add_dpbusd_32 Simd::m256_add_dpbusd_epi32 - #define vec_hadd Simd::m256_hadd + #define vec_setzero() _mm256_setzero_si256() + #define vec_add_dpbusd_32 SIMD::m256_add_dpbusd_epi32 + #define vec_hadd SIMD::m256_hadd #elif defined(USE_SSSE3) using vec_t = __m128i; - #define vec_setzero _mm_setzero_si128 - #define vec_set_32 _mm_set1_epi32 - #define vec_add_dpbusd_32 Simd::m128_add_dpbusd_epi32 - #define vec_hadd Simd::m128_hadd + #define vec_setzero() _mm_setzero_si128() + #define vec_add_dpbusd_32 SIMD::m128_add_dpbusd_epi32 + #define vec_hadd SIMD::m128_hadd + #elif defined(USE_NEON_DOTPROD) + using vec_t = int32x4_t; + #define vec_setzero() vdupq_n_s32(0) + #define vec_add_dpbusd_32(acc, a, b) \ + SIMD::dotprod_m128_add_dpbusd_epi32(acc, vreinterpretq_s8_s32(a), \ + vreinterpretq_s8_s32(b)) + #define vec_hadd SIMD::neon_m128_hadd #endif const auto inputVector = reinterpret_cast(input); @@ -284,7 +288,6 @@ class AffineTransform { output[0] = vec_hadd(sum0, biases[0]); #undef vec_setzero - #undef vec_set_32 #undef vec_add_dpbusd_32 #undef vec_hadd } diff --git a/src/nnue/layers/affine_transform_sparse_input.h b/src/nnue/layers/affine_transform_sparse_input.h index 0ac557aba..5e0551f69 100644 --- a/src/nnue/layers/affine_transform_sparse_input.h +++ b/src/nnue/layers/affine_transform_sparse_input.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -22,14 +22,13 @@ #define NNUE_LAYERS_AFFINE_TRANSFORM_SPARSE_INPUT_H_INCLUDED #include -#include +#include #include #include #include "../../bitboard.h" +#include "../simd.h" #include "../nnue_common.h" -#include "affine_transform.h" -#include "simd.h" /* This file contains the definition for a fully connected layer (aka affine transform) with block sparse input. @@ -38,64 +37,115 @@ namespace Stockfish::Eval::NNUE::Layers { #if (USE_SSSE3 | (USE_NEON >= 8)) -alignas(CacheLineSize) static inline const - std::array, 256> lookup_indices = []() { - std::array, 256> v{}; - for (unsigned i = 0; i < 256; ++i) - { - std::uint64_t j = i, k = 0; - while (j) - v[i][k++] = pop_lsb(j); - } - return v; - }(); +static constexpr int lsb_index64[64] = { + 0, 47, 1, 56, 48, 27, 2, 60, 57, 49, 41, 37, 28, 16, 3, 61, 54, 58, 35, 52, 50, 42, + 21, 44, 38, 32, 29, 23, 17, 11, 4, 62, 46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, + 31, 22, 10, 45, 25, 39, 14, 33, 19, 30, 9, 24, 13, 18, 8, 12, 7, 6, 5, 63}; + +constexpr int constexpr_lsb(uint64_t bb) { + assert(bb != 0); + constexpr uint64_t debruijn64 = 0x03F79D71B4CB0A89ULL; + return lsb_index64[((bb ^ (bb - 1)) * debruijn64) >> 58]; +} + +alignas(CacheLineSize) static constexpr struct OffsetIndices { + + std::uint16_t offset_indices[256][8]; + + constexpr OffsetIndices() : + offset_indices() { + for (int i = 0; i < 256; ++i) + { + std::uint64_t j = i, k = 0; + while (j) + { + offset_indices[i][k++] = constexpr_lsb(j); + j &= j - 1; + } + while (k < 8) + offset_indices[i][k++] = 0; + } + } + +} Lookup; + + #if defined(__GNUC__) || defined(__clang__) + #define RESTRICT __restrict__ + #elif defined(_MSC_VER) + #define RESTRICT __restrict + #else + #define RESTRICT + #endif // Find indices of nonzero numbers in an int32_t array template -void find_nnz(const std::int32_t* input, std::uint16_t* out, IndexType& count_out) { - #if defined(USE_SSSE3) - #if defined(USE_AVX512) - using vec_t = __m512i; - #define vec_nnz(a) _mm512_cmpgt_epi32_mask(a, _mm512_setzero_si512()) - #elif defined(USE_AVX2) - using vec_t = __m256i; - #if defined(USE_VNNI) && !defined(USE_AVXVNNI) - #define vec_nnz(a) _mm256_cmpgt_epi32_mask(a, _mm256_setzero_si256()) - #else - #define vec_nnz(a) \ - _mm256_movemask_ps( \ - _mm256_castsi256_ps(_mm256_cmpgt_epi32(a, _mm256_setzero_si256()))) - #endif - #elif defined(USE_SSSE3) - using vec_t = __m128i; - #define vec_nnz(a) \ - _mm_movemask_ps(_mm_castsi128_ps(_mm_cmpgt_epi32(a, _mm_setzero_si128()))) - #endif - using vec128_t = __m128i; - #define vec128_zero _mm_setzero_si128() - #define vec128_set_16(a) _mm_set1_epi16(a) - #define vec128_load(a) _mm_load_si128(a) - #define vec128_storeu(a, b) _mm_storeu_si128(a, b) - #define vec128_add(a, b) _mm_add_epi16(a, b) - #elif defined(USE_NEON) - using vec_t = uint32x4_t; - static const std::uint32_t Mask[4] = {1, 2, 4, 8}; - #define vec_nnz(a) vaddvq_u32(vandq_u32(vtstq_u32(a, a), vld1q_u32(Mask))) - using vec128_t = uint16x8_t; - #define vec128_zero vdupq_n_u16(0) - #define vec128_set_16(a) vdupq_n_u16(a) - #define vec128_load(a) vld1q_u16(reinterpret_cast(a)) - #define vec128_storeu(a, b) vst1q_u16(reinterpret_cast(a), b) - #define vec128_add(a, b) vaddq_u16(a, b) - #endif - constexpr IndexType InputSimdWidth = sizeof(vec_t) / sizeof(std::int32_t); - // Inputs are processed InputSimdWidth at a time and outputs are processed 8 at a time so we process in chunks of max(InputSimdWidth, 8) - constexpr IndexType ChunkSize = std::max(InputSimdWidth, 8); - constexpr IndexType NumChunks = InputDimensions / ChunkSize; - constexpr IndexType InputsPerChunk = ChunkSize / InputSimdWidth; - constexpr IndexType OutputsPerChunk = ChunkSize / 8; +void find_nnz(const std::int32_t* RESTRICT input, + std::uint16_t* RESTRICT out, + IndexType& count_out) { - const auto inputVector = reinterpret_cast(input); + #if defined(USE_AVX512ICL) + + constexpr IndexType SimdWidthIn = 16; // 512 bits / 32 bits + constexpr IndexType SimdWidthOut = 32; // 512 bits / 16 bits + constexpr IndexType NumChunks = InputDimensions / SimdWidthOut; + const __m512i increment = _mm512_set1_epi16(SimdWidthOut); + __m512i base = _mm512_set_epi16( // Same permute order as _mm512_packus_epi32() + 31, 30, 29, 28, 15, 14, 13, 12, 27, 26, 25, 24, 11, 10, 9, 8, 23, 22, 21, 20, 7, 6, 5, 4, 19, + 18, 17, 16, 3, 2, 1, 0); + + IndexType count = 0; + for (IndexType i = 0; i < NumChunks; ++i) + { + const __m512i inputV0 = _mm512_load_si512(input + i * 2 * SimdWidthIn); + const __m512i inputV1 = _mm512_load_si512(input + i * 2 * SimdWidthIn + SimdWidthIn); + + // Get a bitmask and gather non zero indices + const __m512i inputV01 = _mm512_packus_epi32(inputV0, inputV1); + const __mmask32 nnzMask = _mm512_test_epi16_mask(inputV01, inputV01); + + // Avoid _mm512_mask_compressstoreu_epi16() as it's 256 uOps on Zen4 + __m512i nnz = _mm512_maskz_compress_epi16(nnzMask, base); + _mm512_storeu_si512(out + count, nnz); + + count += popcount(nnzMask); + base = _mm512_add_epi16(base, increment); + } + count_out = count; + + #elif defined(USE_AVX512) + + constexpr IndexType SimdWidth = 16; // 512 bits / 32 bits + constexpr IndexType NumChunks = InputDimensions / SimdWidth; + const __m512i increment = _mm512_set1_epi32(SimdWidth); + __m512i base = _mm512_set_epi32(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0); + + IndexType count = 0; + for (IndexType i = 0; i < NumChunks; ++i) + { + const __m512i inputV = _mm512_load_si512(input + i * SimdWidth); + + // Get a bitmask and gather non zero indices + const __mmask16 nnzMask = _mm512_test_epi32_mask(inputV, inputV); + const __m512i nnzV = _mm512_maskz_compress_epi32(nnzMask, base); + _mm512_mask_cvtepi32_storeu_epi16(out + count, 0xFFFF, nnzV); + count += popcount(nnzMask); + base = _mm512_add_epi32(base, increment); + } + count_out = count; + + #else + + using namespace SIMD; + + constexpr IndexType InputSimdWidth = sizeof(vec_uint_t) / sizeof(std::int32_t); + // Outputs are processed 8 elements at a time, even if the SIMD width is narrower + constexpr IndexType ChunkSize = 8; + constexpr IndexType NumChunks = InputDimensions / ChunkSize; + constexpr IndexType InputsPerChunk = ChunkSize / InputSimdWidth; + + static_assert(InputsPerChunk > 0 && "SIMD width too wide"); + + const auto inputVector = reinterpret_cast(input); IndexType count = 0; vec128_t base = vec128_zero; const vec128_t increment = vec128_set_16(8); @@ -105,27 +155,19 @@ void find_nnz(const std::int32_t* input, std::uint16_t* out, IndexType& count_ou unsigned nnz = 0; for (IndexType j = 0; j < InputsPerChunk; ++j) { - const vec_t inputChunk = inputVector[i * InputsPerChunk + j]; + const vec_uint_t inputChunk = inputVector[i * InputsPerChunk + j]; nnz |= unsigned(vec_nnz(inputChunk)) << (j * InputSimdWidth); } - for (IndexType j = 0; j < OutputsPerChunk; ++j) - { - const auto lookup = (nnz >> (j * 8)) & 0xFF; - const auto offsets = - vec128_load(reinterpret_cast(&lookup_indices[lookup])); - vec128_storeu(reinterpret_cast(out + count), vec128_add(base, offsets)); - count += popcount(lookup); - base = vec128_add(base, increment); - } + const vec128_t offsets = + vec128_load(reinterpret_cast(&Lookup.offset_indices[nnz])); + vec128_storeu(reinterpret_cast(out + count), vec128_add(base, offsets)); + count += popcount(nnz); + base = vec128_add(base, increment); } count_out = count; + #endif } - #undef vec_nnz - #undef vec128_zero - #undef vec128_set_16 - #undef vec128_load - #undef vec128_storeu - #undef vec128_add + #endif // Sparse input implementation @@ -196,6 +238,15 @@ class AffineTransformSparseInput { return !stream.fail(); } + + std::size_t get_content_hash() const { + std::size_t h = 0; + hash_combine(h, get_raw_data_hash(biases)); + hash_combine(h, get_raw_data_hash(weights)); + hash_combine(h, get_hash_value(0)); + return h; + } + // Forward propagation void propagate(const InputType* input, OutputType* output) const { @@ -203,35 +254,44 @@ class AffineTransformSparseInput { #if defined(USE_AVX512) using invec_t = __m512i; using outvec_t = __m512i; + #define vec_add_32 _mm512_add_epi32 #define vec_set_32 _mm512_set1_epi32 - #define vec_add_dpbusd_32 Simd::m512_add_dpbusd_epi32 + #define vec_add_dpbusd_32 SIMD::m512_add_dpbusd_epi32 #elif defined(USE_AVX2) using invec_t = __m256i; using outvec_t = __m256i; + #define vec_add_32 _mm256_add_epi32 #define vec_set_32 _mm256_set1_epi32 - #define vec_add_dpbusd_32 Simd::m256_add_dpbusd_epi32 + #define vec_add_dpbusd_32 SIMD::m256_add_dpbusd_epi32 #elif defined(USE_SSSE3) using invec_t = __m128i; using outvec_t = __m128i; #define vec_set_32 _mm_set1_epi32 - #define vec_add_dpbusd_32 Simd::m128_add_dpbusd_epi32 + #define vec_add_dpbusd_32 SIMD::m128_add_dpbusd_epi32 #elif defined(USE_NEON_DOTPROD) using invec_t = int8x16_t; using outvec_t = int32x4_t; #define vec_set_32(a) vreinterpretq_s8_u32(vdupq_n_u32(a)) - #define vec_add_dpbusd_32 Simd::dotprod_m128_add_dpbusd_epi32 + #define vec_add_dpbusd_32 SIMD::dotprod_m128_add_dpbusd_epi32 #elif defined(USE_NEON) using invec_t = int8x16_t; using outvec_t = int32x4_t; #define vec_set_32(a) vreinterpretq_s8_u32(vdupq_n_u32(a)) - #define vec_add_dpbusd_32 Simd::neon_m128_add_dpbusd_epi32 + #define vec_add_dpbusd_32 SIMD::neon_m128_add_dpbusd_epi32 #endif - static constexpr IndexType OutputSimdWidth = sizeof(outvec_t) / sizeof(OutputType); - + constexpr IndexType OutputSimdWidth = sizeof(outvec_t) / sizeof(OutputType); constexpr IndexType NumChunks = ceil_to_multiple(InputDimensions, 8) / ChunkSize; - constexpr IndexType NumRegs = OutputDimensions / OutputSimdWidth; - std::uint16_t nnz[NumChunks]; - IndexType count; + constexpr IndexType NumAccums = OutputDimensions / OutputSimdWidth; + // If we're using high-latency dot product instructions, split the accumulators + // to create 3 separate dependency chains and merge at the end + constexpr IndexType NumRegs = + #if defined(USE_VNNI) + 3 * NumAccums; + #else + NumAccums; + #endif + std::uint16_t nnz[NumChunks]; + IndexType count; const auto input32 = reinterpret_cast(input); @@ -240,24 +300,61 @@ class AffineTransformSparseInput { const outvec_t* biasvec = reinterpret_cast(biases); outvec_t acc[NumRegs]; - for (IndexType k = 0; k < NumRegs; ++k) + for (IndexType k = 0; k < NumAccums; ++k) acc[k] = biasvec[k]; - for (IndexType j = 0; j < count; ++j) + const auto* start = nnz; + const auto* end = nnz + count; + + // convince GCC to not do weird pointer arithmetic in the following loop + const std::int8_t* weights_cp = weights; + #if defined(USE_VNNI) + for (IndexType k = NumAccums; k < NumRegs; ++k) + acc[k] = vec_zero(); + + while (start < end - 2) { - const auto i = nnz[j]; - const invec_t in = vec_set_32(input32[i]); - const auto col = - reinterpret_cast(&weights[i * OutputDimensions * ChunkSize]); - for (IndexType k = 0; k < NumRegs; ++k) + 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 = + reinterpret_cast(&weights_cp[i0 * OutputDimensions * ChunkSize]); + const auto col1 = + reinterpret_cast(&weights_cp[i1 * OutputDimensions * ChunkSize]); + const auto col2 = + reinterpret_cast(&weights_cp[i2 * OutputDimensions * ChunkSize]); + for (IndexType k = 0; k < NumAccums; ++k) + { + vec_add_dpbusd_32(acc[k], in0, col0[k]); + vec_add_dpbusd_32(acc[k + NumAccums], in1, col1[k]); + vec_add_dpbusd_32(acc[k + 2 * NumAccums], in2, col2[k]); + } + } + for (IndexType k = 0; k < NumAccums; ++k) + acc[k] = vec_add_32(vec_add_32(acc[k], acc[k + NumAccums]), acc[k + 2 * NumAccums]); + #endif + while (start < end) + { + const std::ptrdiff_t i = *start++; + const invec_t in = vec_set_32(input32[i]); + const auto col = + reinterpret_cast(&weights_cp[i * OutputDimensions * ChunkSize]); + for (IndexType k = 0; k < NumAccums; ++k) vec_add_dpbusd_32(acc[k], in, col[k]); } outvec_t* outptr = reinterpret_cast(output); - for (IndexType k = 0; k < NumRegs; ++k) + for (IndexType k = 0; k < NumAccums; ++k) outptr[k] = acc[k]; + #undef vec_set_32 #undef vec_add_dpbusd_32 + #ifdef vec_add_32 + #undef vec_add_32 + #endif #else // Use dense implementation for the other architectures. affine_transform_non_ssse3( diff --git a/src/nnue/layers/clipped_relu.h b/src/nnue/layers/clipped_relu.h index 813234c59..7284c1033 100644 --- a/src/nnue/layers/clipped_relu.h +++ b/src/nnue/layers/clipped_relu.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -58,6 +58,12 @@ class ClippedReLU { // Write network parameters bool write_parameters(std::ostream&) const { return true; } + std::size_t get_content_hash() const { + std::size_t h = 0; + hash_combine(h, get_hash_value(0)); + return h; + } + // Forward propagation void propagate(const InputType* input, OutputType* output) const { @@ -65,41 +71,37 @@ class ClippedReLU { if constexpr (InputDimensions % SimdWidth == 0) { constexpr IndexType NumChunks = InputDimensions / SimdWidth; - const __m256i Zero = _mm256_setzero_si256(); const __m256i Offsets = _mm256_set_epi32(7, 3, 6, 2, 5, 1, 4, 0); const auto in = reinterpret_cast(input); const auto out = reinterpret_cast<__m256i*>(output); for (IndexType i = 0; i < NumChunks; ++i) { const __m256i words0 = - _mm256_srai_epi16(_mm256_packs_epi32(_mm256_load_si256(&in[i * 4 + 0]), - _mm256_load_si256(&in[i * 4 + 1])), + _mm256_srli_epi16(_mm256_packus_epi32(_mm256_load_si256(&in[i * 4 + 0]), + _mm256_load_si256(&in[i * 4 + 1])), WeightScaleBits); const __m256i words1 = - _mm256_srai_epi16(_mm256_packs_epi32(_mm256_load_si256(&in[i * 4 + 2]), - _mm256_load_si256(&in[i * 4 + 3])), + _mm256_srli_epi16(_mm256_packus_epi32(_mm256_load_si256(&in[i * 4 + 2]), + _mm256_load_si256(&in[i * 4 + 3])), WeightScaleBits); - _mm256_store_si256( - &out[i], _mm256_permutevar8x32_epi32( - _mm256_max_epi8(_mm256_packs_epi16(words0, words1), Zero), Offsets)); + _mm256_store_si256(&out[i], _mm256_permutevar8x32_epi32( + _mm256_packs_epi16(words0, words1), Offsets)); } } else { constexpr IndexType NumChunks = InputDimensions / (SimdWidth / 2); - const __m128i Zero = _mm_setzero_si128(); const auto in = reinterpret_cast(input); const auto out = reinterpret_cast<__m128i*>(output); for (IndexType i = 0; i < NumChunks; ++i) { - const __m128i words0 = _mm_srai_epi16( - _mm_packs_epi32(_mm_load_si128(&in[i * 4 + 0]), _mm_load_si128(&in[i * 4 + 1])), + const __m128i words0 = _mm_srli_epi16( + _mm_packus_epi32(_mm_load_si128(&in[i * 4 + 0]), _mm_load_si128(&in[i * 4 + 1])), WeightScaleBits); - const __m128i words1 = _mm_srai_epi16( - _mm_packs_epi32(_mm_load_si128(&in[i * 4 + 2]), _mm_load_si128(&in[i * 4 + 3])), + const __m128i words1 = _mm_srli_epi16( + _mm_packus_epi32(_mm_load_si128(&in[i * 4 + 2]), _mm_load_si128(&in[i * 4 + 3])), WeightScaleBits); - const __m128i packedbytes = _mm_packs_epi16(words0, words1); - _mm_store_si128(&out[i], _mm_max_epi8(packedbytes, Zero)); + _mm_store_si128(&out[i], _mm_packs_epi16(words0, words1)); } } constexpr IndexType Start = InputDimensions % SimdWidth == 0 @@ -109,9 +111,7 @@ class ClippedReLU { #elif defined(USE_SSE2) constexpr IndexType NumChunks = InputDimensions / SimdWidth; - #ifdef USE_SSE41 - const __m128i Zero = _mm_setzero_si128(); - #else + #ifndef USE_SSE41 const __m128i k0x80s = _mm_set1_epi8(-128); #endif @@ -119,6 +119,15 @@ class ClippedReLU { const auto out = reinterpret_cast<__m128i*>(output); for (IndexType i = 0; i < NumChunks; ++i) { + #if defined(USE_SSE41) + const __m128i words0 = _mm_srli_epi16( + _mm_packus_epi32(_mm_load_si128(&in[i * 4 + 0]), _mm_load_si128(&in[i * 4 + 1])), + WeightScaleBits); + const __m128i words1 = _mm_srli_epi16( + _mm_packus_epi32(_mm_load_si128(&in[i * 4 + 2]), _mm_load_si128(&in[i * 4 + 3])), + WeightScaleBits); + _mm_store_si128(&out[i], _mm_packs_epi16(words0, words1)); + #else const __m128i words0 = _mm_srai_epi16( _mm_packs_epi32(_mm_load_si128(&in[i * 4 + 0]), _mm_load_si128(&in[i * 4 + 1])), WeightScaleBits); @@ -126,15 +135,8 @@ class ClippedReLU { _mm_packs_epi32(_mm_load_si128(&in[i * 4 + 2]), _mm_load_si128(&in[i * 4 + 3])), WeightScaleBits); const __m128i packedbytes = _mm_packs_epi16(words0, words1); - _mm_store_si128(&out[i], - - #ifdef USE_SSE41 - _mm_max_epi8(packedbytes, Zero) - #else - _mm_subs_epi8(_mm_adds_epi8(packedbytes, k0x80s), k0x80s) + _mm_store_si128(&out[i], _mm_subs_epi8(_mm_adds_epi8(packedbytes, k0x80s), k0x80s)); #endif - - ); } constexpr IndexType Start = NumChunks * SimdWidth; diff --git a/src/nnue/layers/simd.h b/src/nnue/layers/simd.h deleted file mode 100644 index cec41474b..000000000 --- a/src/nnue/layers/simd.h +++ /dev/null @@ -1,167 +0,0 @@ -/* - Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 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 . -*/ - -#ifndef STOCKFISH_SIMD_H_INCLUDED -#define STOCKFISH_SIMD_H_INCLUDED - -#if defined(USE_AVX2) - #include - -#elif defined(USE_SSE41) - #include - -#elif defined(USE_SSSE3) - #include - -#elif defined(USE_SSE2) - #include - -#elif defined(USE_NEON) - #include -#endif - -namespace Stockfish::Simd { - -#if defined(USE_AVX512) - -[[maybe_unused]] static int m512_hadd(__m512i sum, int bias) { - return _mm512_reduce_add_epi32(sum) + bias; -} - -/* - Parameters: - sum0 = [zmm0.i128[0], zmm0.i128[1], zmm0.i128[2], zmm0.i128[3]] - sum1 = [zmm1.i128[0], zmm1.i128[1], zmm1.i128[2], zmm1.i128[3]] - sum2 = [zmm2.i128[0], zmm2.i128[1], zmm2.i128[2], zmm2.i128[3]] - sum3 = [zmm3.i128[0], zmm3.i128[1], zmm3.i128[2], zmm3.i128[3]] - - Returns: - ret = [ - reduce_add_epi32(zmm0.i128[0]), reduce_add_epi32(zmm1.i128[0]), reduce_add_epi32(zmm2.i128[0]), reduce_add_epi32(zmm3.i128[0]), - reduce_add_epi32(zmm0.i128[1]), reduce_add_epi32(zmm1.i128[1]), reduce_add_epi32(zmm2.i128[1]), reduce_add_epi32(zmm3.i128[1]), - reduce_add_epi32(zmm0.i128[2]), reduce_add_epi32(zmm1.i128[2]), reduce_add_epi32(zmm2.i128[2]), reduce_add_epi32(zmm3.i128[2]), - reduce_add_epi32(zmm0.i128[3]), reduce_add_epi32(zmm1.i128[3]), reduce_add_epi32(zmm2.i128[3]), reduce_add_epi32(zmm3.i128[3]) - ] - */ -[[maybe_unused]] static __m512i -m512_hadd128x16_interleave(__m512i sum0, __m512i sum1, __m512i sum2, __m512i sum3) { - - __m512i sum01a = _mm512_unpacklo_epi32(sum0, sum1); - __m512i sum01b = _mm512_unpackhi_epi32(sum0, sum1); - - __m512i sum23a = _mm512_unpacklo_epi32(sum2, sum3); - __m512i sum23b = _mm512_unpackhi_epi32(sum2, sum3); - - __m512i sum01 = _mm512_add_epi32(sum01a, sum01b); - __m512i sum23 = _mm512_add_epi32(sum23a, sum23b); - - __m512i sum0123a = _mm512_unpacklo_epi64(sum01, sum23); - __m512i sum0123b = _mm512_unpackhi_epi64(sum01, sum23); - - return _mm512_add_epi32(sum0123a, sum0123b); -} - -[[maybe_unused]] static void m512_add_dpbusd_epi32(__m512i& acc, __m512i a, __m512i b) { - - #if defined(USE_VNNI) - acc = _mm512_dpbusd_epi32(acc, a, b); - #else - __m512i product0 = _mm512_maddubs_epi16(a, b); - product0 = _mm512_madd_epi16(product0, _mm512_set1_epi16(1)); - acc = _mm512_add_epi32(acc, product0); - #endif -} - -#endif - -#if defined(USE_AVX2) - -[[maybe_unused]] static int m256_hadd(__m256i sum, int bias) { - __m128i sum128 = _mm_add_epi32(_mm256_castsi256_si128(sum), _mm256_extracti128_si256(sum, 1)); - sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_BADC)); - sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_CDAB)); - return _mm_cvtsi128_si32(sum128) + bias; -} - -[[maybe_unused]] static void m256_add_dpbusd_epi32(__m256i& acc, __m256i a, __m256i b) { - - #if defined(USE_VNNI) - acc = _mm256_dpbusd_epi32(acc, a, b); - #else - __m256i product0 = _mm256_maddubs_epi16(a, b); - product0 = _mm256_madd_epi16(product0, _mm256_set1_epi16(1)); - acc = _mm256_add_epi32(acc, product0); - #endif -} - -#endif - -#if defined(USE_SSSE3) - -[[maybe_unused]] static int m128_hadd(__m128i sum, int bias) { - sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0x4E)); //_MM_PERM_BADC - sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0xB1)); //_MM_PERM_CDAB - return _mm_cvtsi128_si32(sum) + bias; -} - -[[maybe_unused]] static void m128_add_dpbusd_epi32(__m128i& acc, __m128i a, __m128i b) { - - __m128i product0 = _mm_maddubs_epi16(a, b); - product0 = _mm_madd_epi16(product0, _mm_set1_epi16(1)); - acc = _mm_add_epi32(acc, product0); -} - -#endif - -#if defined(USE_NEON_DOTPROD) - -[[maybe_unused]] static void -dotprod_m128_add_dpbusd_epi32(int32x4_t& acc, int8x16_t a, int8x16_t b) { - - acc = vdotq_s32(acc, a, b); -} -#endif - -#if defined(USE_NEON) - -[[maybe_unused]] static int neon_m128_reduce_add_epi32(int32x4_t s) { - #if USE_NEON >= 8 - return vaddvq_s32(s); - #else - return s[0] + s[1] + s[2] + s[3]; - #endif -} - -[[maybe_unused]] static int neon_m128_hadd(int32x4_t sum, int bias) { - return neon_m128_reduce_add_epi32(sum) + bias; -} - -#endif - -#if USE_NEON >= 8 -[[maybe_unused]] static void neon_m128_add_dpbusd_epi32(int32x4_t& acc, int8x16_t a, int8x16_t b) { - - int16x8_t product0 = vmull_s8(vget_low_s8(a), vget_low_s8(b)); - int16x8_t product1 = vmull_high_s8(a, b); - int16x8_t sum = vpaddq_s16(product0, product1); - acc = vpadalq_s16(acc, sum); -} -#endif -} - -#endif // STOCKFISH_SIMD_H_INCLUDED diff --git a/src/nnue/layers/sqr_clipped_relu.h b/src/nnue/layers/sqr_clipped_relu.h index 9c20df9d6..53412d014 100644 --- a/src/nnue/layers/sqr_clipped_relu.h +++ b/src/nnue/layers/sqr_clipped_relu.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -58,6 +58,12 @@ class SqrClippedReLU { // Write network parameters bool write_parameters(std::ostream&) const { return true; } + std::size_t get_content_hash() const { + std::size_t h = 0; + hash_combine(h, get_hash_value(0)); + return h; + } + // Forward propagation void propagate(const InputType* input, OutputType* output) const { diff --git a/src/nnue/network.cpp b/src/nnue/network.cpp index dd38e6b16..4d5b683fe 100644 --- a/src/nnue/network.cpp +++ b/src/nnue/network.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -18,18 +18,18 @@ #include "network.h" -#include #include -#include #include #include #include #include #include +#define INCBIN_SILENCE_BITCODE_WARNING +#include "../incbin/incbin.h" + #include "../cluster.h" #include "../evaluate.h" -#include "../incbin/incbin.h" #include "../misc.h" #include "../position.h" #include "../types.h" @@ -37,7 +37,6 @@ #include "nnue_common.h" #include "nnue_misc.h" -namespace { // Macro to embed the default efficiently updatable neural network (NNUE) file // data in the engine binary (using incbin.h, by Dale Weiler). // This macro invocation will declare the following three variables @@ -57,6 +56,8 @@ const unsigned char* const gEmbeddedNNUESmallEnd = &gEmbeddedNNUESmallData[1 const unsigned int gEmbeddedNNUESmallSize = 1; #endif +namespace { + struct EmbeddedNNUE { EmbeddedNNUE(const unsigned char* embeddedData, const unsigned char* embeddedEnd, @@ -86,23 +87,6 @@ namespace Stockfish::Eval::NNUE { namespace Detail { -// Initialize the evaluation function parameters -template -void initialize(AlignedPtr& pointer) { - - pointer.reset(reinterpret_cast(std_aligned_alloc(alignof(T), sizeof(T)))); - std::memset(pointer.get(), 0, sizeof(T)); -} - -template -void initialize(LargePagePtr& pointer) { - - static_assert(alignof(T) <= 4096, - "aligned_large_pages_alloc() may fail for such a big alignment requirement of T"); - pointer.reset(reinterpret_cast(aligned_large_pages_alloc(sizeof(T)))); - std::memset(pointer.get(), 0, sizeof(T)); -} - // Read evaluation function parameters template bool read_parameters(std::istream& stream, T& reference) { @@ -124,7 +108,6 @@ bool write_parameters(std::ostream& stream, const T& reference) { } // namespace Detail - template void Network::load(const std::string& rootDirectory, std::string evalfilePath) { #if defined(DEFAULT_NNUE_DIRECTORY) @@ -139,14 +122,14 @@ void Network::load(const std::string& rootDirectory, std::str for (const auto& directory : dirs) { - if (evalFile.current != evalfilePath) + if (std::string(evalFile.current) != evalfilePath) { if (directory != "") { load_user_net(directory, evalfilePath); } - if (directory == "" && evalfilePath == evalFile.defaultName) + if (directory == "" && evalfilePath == std::string(evalFile.defaultName)) { load_internal(); } @@ -164,7 +147,7 @@ bool Network::save(const std::optional& filename actualFilename = filename.value(); else { - if (evalFile.current != evalFile.defaultName) + if (std::string(evalFile.current) != std::string(evalFile.defaultName)) { msg = "Failed to export a net. " "A non-embedded net can only be saved if the filename is specified"; @@ -187,97 +170,78 @@ bool Network::save(const std::optional& filename template -Value Network::evaluate(const Position& pos, - bool adjusted, - int* complexity, - bool psqtOnly) const { - // We manually align the arrays on the stack because with gcc < 9.3 - // overaligning stack variables with alignas() doesn't work correctly. +NetworkOutput +Network::evaluate(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache& cache) const { constexpr uint64_t alignment = CacheLineSize; - constexpr int delta = 24; -#if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN) - TransformedFeatureType transformedFeaturesUnaligned - [FeatureTransformer::BufferSize - + alignment / sizeof(TransformedFeatureType)]; - - auto* transformedFeatures = align_ptr_up(&transformedFeaturesUnaligned[0]); -#else - alignas(alignment) TransformedFeatureType transformedFeatures - [FeatureTransformer::BufferSize]; -#endif + alignas(alignment) + TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; ASSERT_ALIGNED(transformedFeatures, alignment); const int bucket = (pos.count() - 1) / 4; - const auto psqt = featureTransformer->transform(pos, transformedFeatures, bucket, psqtOnly); - const auto positional = !psqtOnly ? (network[bucket]->propagate(transformedFeatures)) : 0; - - if (complexity) - *complexity = !psqtOnly ? std::abs(psqt - positional) / OutputScale : 0; - - // Give more value to positional evaluation when adjusted flag is set - if (adjusted) - return static_cast(((1024 - delta) * psqt + (1024 + delta) * positional) - / (1024 * OutputScale)); - else - return static_cast((psqt + positional) / OutputScale); + const auto psqt = + featureTransformer.transform(pos, accumulatorStack, cache, transformedFeatures, bucket); + const auto positional = network[bucket].propagate(transformedFeatures); + return {static_cast(psqt / OutputScale), static_cast(positional / OutputScale)}; } template -void Network::verify(std::string evalfilePath) const { +void Network::verify(std::string evalfilePath, + const std::function& f) const { if (evalfilePath.empty()) evalfilePath = evalFile.defaultName; - if (evalFile.current != evalfilePath) + if (std::string(evalFile.current) != evalfilePath) { - std::string msg1 = - "Network evaluation parameters compatible with the engine must be available."; - std::string msg2 = "The network file " + evalfilePath + " was not loaded successfully."; - std::string msg3 = "The UCI option EvalFile might need to specify the full path, " - "including the directory name, to the network file."; - std::string msg4 = "The default net can be downloaded from: " - "https://tests.stockfishchess.org/api/nn/" - + evalFile.defaultName; - std::string msg5 = "The engine will be terminated now."; + if (f) + { + std::string msg1 = + "Network evaluation parameters compatible with the engine must be available."; + std::string msg2 = "The network file " + evalfilePath + " was not loaded successfully."; + std::string msg3 = "The UCI option EvalFile might need to specify the full path, " + "including the directory name, to the network file."; + std::string msg4 = "The default net can be downloaded from: " + "https://tests.stockfishchess.org/api/nn/" + + std::string(evalFile.defaultName); + std::string msg5 = "The engine will be terminated now."; + + std::string msg = "ERROR: " + msg1 + '\n' + "ERROR: " + msg2 + '\n' + "ERROR: " + msg3 + + '\n' + "ERROR: " + msg4 + '\n' + "ERROR: " + msg5 + '\n'; + + f(msg); + } - sync_cout << "info string ERROR: " << msg1 << sync_endl; - sync_cout << "info string ERROR: " << msg2 << sync_endl; - sync_cout << "info string ERROR: " << msg3 << sync_endl; - sync_cout << "info string ERROR: " << msg4 << sync_endl; - sync_cout << "info string ERROR: " << msg5 << sync_endl; exit(EXIT_FAILURE); } - if (Cluster::is_root()) - sync_cout << "info string NNUE evaluation using " << evalfilePath << sync_endl; + 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))"); + } } template -void Network::hint_common_access(const Position& pos, bool psqtOnl) const { - featureTransformer->hint_common_access(pos, psqtOnl); -} +NnueEvalTrace +Network::trace_evaluate(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache& cache) const { - -template -NnueEvalTrace Network::trace_evaluate(const Position& pos) const { - // We manually align the arrays on the stack because with gcc < 9.3 - // overaligning stack variables with alignas() doesn't work correctly. constexpr uint64_t alignment = CacheLineSize; -#if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN) - TransformedFeatureType transformedFeaturesUnaligned - [FeatureTransformer::BufferSize - + alignment / sizeof(TransformedFeatureType)]; - - auto* transformedFeatures = align_ptr_up(&transformedFeaturesUnaligned[0]); -#else - alignas(alignment) TransformedFeatureType transformedFeatures - [FeatureTransformer::BufferSize]; -#endif + alignas(alignment) + TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; ASSERT_ALIGNED(transformedFeatures, alignment); @@ -286,8 +250,8 @@ NnueEvalTrace Network::trace_evaluate(const Position& pos) co for (IndexType bucket = 0; bucket < LayerStacks; ++bucket) { const auto materialist = - featureTransformer->transform(pos, transformedFeatures, bucket, false); - const auto positional = network[bucket]->propagate(transformedFeatures); + featureTransformer.transform(pos, accumulatorStack, cache, transformedFeatures, bucket); + const auto positional = network[bucket].propagate(transformedFeatures); t.psqt[bucket] = static_cast(materialist / OutputScale); t.positional[bucket] = static_cast(positional / OutputScale); @@ -340,9 +304,7 @@ void Network::load_internal() { template void Network::initialize() { - Detail::initialize(featureTransformer); - for (std::size_t i = 0; i < LayerStacks; ++i) - Detail::initialize(network[i]); + initialized = true; } @@ -366,6 +328,20 @@ std::optional Network::load(std::istream& stream } +template +std::size_t Network::get_content_hash() const { + if (!initialized) + return 0; + + std::size_t h = 0; + hash_combine(h, featureTransformer); + for (auto&& layerstack : network) + hash_combine(h, layerstack); + hash_combine(h, evalFile); + hash_combine(h, static_cast(embeddedType)); + return h; +} + // Read network header template bool Network::read_header(std::istream& stream, @@ -399,17 +375,17 @@ bool Network::write_header(std::ostream& stream, template bool Network::read_parameters(std::istream& stream, - std::string& netDescription) const { + std::string& netDescription) { std::uint32_t hashValue; if (!read_header(stream, &hashValue, &netDescription)) return false; if (hashValue != Network::hash) return false; - if (!Detail::read_parameters(stream, *featureTransformer)) + if (!Detail::read_parameters(stream, featureTransformer)) return false; for (std::size_t i = 0; i < LayerStacks; ++i) { - if (!Detail::read_parameters(stream, *(network[i]))) + if (!Detail::read_parameters(stream, network[i])) return false; } return stream && stream.peek() == std::ios::traits_type::eof(); @@ -421,24 +397,22 @@ bool Network::write_parameters(std::ostream& stream, const std::string& netDescription) const { if (!write_header(stream, Network::hash, netDescription)) return false; - if (!Detail::write_parameters(stream, *featureTransformer)) + if (!Detail::write_parameters(stream, featureTransformer)) return false; for (std::size_t i = 0; i < LayerStacks; ++i) { - if (!Detail::write_parameters(stream, *(network[i]))) + if (!Detail::write_parameters(stream, network[i])) return false; } return bool(stream); } -// Explicit template instantiation +// Explicit template instantiations -template class Network< - NetworkArchitecture, - FeatureTransformer>; +template class Network, + FeatureTransformer>; -template class Network< - NetworkArchitecture, - FeatureTransformer>; +template class Network, + FeatureTransformer>; } // namespace Stockfish::Eval::NNUE diff --git a/src/nnue/network.h b/src/nnue/network.h index 21e1c6222..d0e3218ca 100644 --- a/src/nnue/network.h +++ b/src/nnue/network.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -19,49 +19,70 @@ #ifndef NETWORK_H_INCLUDED #define NETWORK_H_INCLUDED +#include #include +#include #include +#include #include #include +#include +#include #include #include "../misc.h" -#include "../position.h" #include "../types.h" +#include "nnue_accumulator.h" #include "nnue_architecture.h" +#include "nnue_common.h" #include "nnue_feature_transformer.h" #include "nnue_misc.h" -namespace Stockfish::Eval::NNUE { +namespace Stockfish { +class Position; +} +namespace Stockfish::Eval::NNUE { enum class EmbeddedNNUEType { BIG, SMALL, }; +using NetworkOutput = std::tuple; +// The network must be a trivial type, i.e. the memory must be in-line. +// This is required to allow sharing the network via shared memory, as +// there is no way to run destructors. template class Network { + static constexpr IndexType FTDimensions = Arch::TransformedFeatureDimensions; + public: Network(EvalFile file, EmbeddedNNUEType type) : evalFile(file), embeddedType(type) {} + Network(const Network& other) = default; + Network(Network&& other) = default; + + Network& operator=(const Network& other) = default; + Network& operator=(Network&& other) = default; + void load(const std::string& rootDirectory, std::string evalfilePath); bool save(const std::optional& filename) const; + std::size_t get_content_hash() const; - Value evaluate(const Position& pos, - bool adjusted = false, - int* complexity = nullptr, - bool psqtOnly = false) const; + NetworkOutput evaluate(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache& cache) const; - void hint_common_access(const Position& pos, bool psqtOnl) const; - - void verify(std::string evalfilePath) const; - NnueEvalTrace trace_evaluate(const Position& pos) const; + void verify(std::string evalfilePath, const std::function&) const; + NnueEvalTrace trace_evaluate(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache& cache) const; private: void load_user_net(const std::string&, const std::string&); @@ -75,30 +96,33 @@ class Network { bool read_header(std::istream&, std::uint32_t*, std::string*) const; bool write_header(std::ostream&, std::uint32_t, const std::string&) const; - bool read_parameters(std::istream&, std::string&) const; + bool read_parameters(std::istream&, std::string&); bool write_parameters(std::ostream&, const std::string&) const; // Input feature converter - LargePagePtr featureTransformer; + Transformer featureTransformer; // Evaluation function - AlignedPtr network[LayerStacks]; + Arch network[LayerStacks]; EvalFile evalFile; EmbeddedNNUEType embeddedType; + bool initialized = false; + // Hash value of evaluation function structure static constexpr std::uint32_t hash = Transformer::get_hash_value() ^ Arch::get_hash_value(); + + template + friend struct AccumulatorCaches::Cache; }; // Definitions of the network types -using SmallFeatureTransformer = - FeatureTransformer; +using SmallFeatureTransformer = FeatureTransformer; using SmallNetworkArchitecture = NetworkArchitecture; -using BigFeatureTransformer = - FeatureTransformer; +using BigFeatureTransformer = FeatureTransformer; using BigNetworkArchitecture = NetworkArchitecture; using NetworkBig = Network; @@ -106,9 +130,9 @@ using NetworkSmall = Network; struct Networks { - Networks(NetworkBig&& nB, NetworkSmall&& nS) : - big(std::move(nB)), - small(std::move(nS)) {} + Networks(std::unique_ptr&& nB, std::unique_ptr&& nS) : + big(std::move(*nB)), + small(std::move(*nS)) {} NetworkBig big; NetworkSmall small; @@ -117,4 +141,22 @@ struct Networks { } // namespace Stockfish +template +struct std::hash> { + std::size_t operator()( + const Stockfish::Eval::NNUE::Network& network) const noexcept { + return network.get_content_hash(); + } +}; + +template<> +struct std::hash { + std::size_t operator()(const Stockfish::Eval::NNUE::Networks& networks) const noexcept { + std::size_t h = 0; + Stockfish::hash_combine(h, networks.big); + Stockfish::hash_combine(h, networks.small); + return h; + } +}; + #endif diff --git a/src/nnue/nnue_accumulator.cpp b/src/nnue/nnue_accumulator.cpp new file mode 100644 index 000000000..16af8d5f6 --- /dev/null +++ b/src/nnue/nnue_accumulator.cpp @@ -0,0 +1,926 @@ +/* + 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 . +*/ + +#include "nnue_accumulator.h" + +#include +#include +#include +#include + +#include "../bitboard.h" +#include "../misc.h" +#include "../position.h" +#include "../types.h" +#include "features/half_ka_v2_hm.h" +#include "nnue_architecture.h" +#include "nnue_common.h" +#include "nnue_feature_transformer.h" // IWYU pragma: keep +#include "simd.h" + +namespace Stockfish::Eval::NNUE { + +using namespace SIMD; + +namespace { + +template +void double_inc_update(Color perspective, + const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& middle_state, + AccumulatorState& target_state, + const AccumulatorState& computed); + +template +void double_inc_update(Color perspective, + const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& middle_state, + AccumulatorState& target_state, + const AccumulatorState& computed, + const DirtyPiece& dp2); + +template +void update_accumulator_incremental( + Color perspective, + const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& target_state, + const AccumulatorState& computed); + +template +void update_accumulator_refresh_cache(Color perspective, + const FeatureTransformer& featureTransformer, + const Position& pos, + AccumulatorState& accumulatorState, + AccumulatorCaches::Cache& cache); + +template +void update_threats_accumulator_full(Color perspective, + const FeatureTransformer& featureTransformer, + const Position& pos, + AccumulatorState& accumulatorState); +} + +template +const AccumulatorState& AccumulatorStack::latest() const noexcept { + return accumulators()[size - 1]; +} + +// Explicit template instantiations +template const AccumulatorState& AccumulatorStack::latest() const noexcept; +template const AccumulatorState& AccumulatorStack::latest() const noexcept; + +template +AccumulatorState& AccumulatorStack::mut_latest() noexcept { + return mut_accumulators()[size - 1]; +} + +template +const std::array, AccumulatorStack::MaxSize>& +AccumulatorStack::accumulators() const noexcept { + static_assert(std::is_same_v || std::is_same_v, + "Invalid Feature Set Type"); + + if constexpr (std::is_same_v) + return psq_accumulators; + + if constexpr (std::is_same_v) + return threat_accumulators; +} + +template +std::array, AccumulatorStack::MaxSize>& +AccumulatorStack::mut_accumulators() noexcept { + static_assert(std::is_same_v || std::is_same_v, + "Invalid Feature Set Type"); + + if constexpr (std::is_same_v) + return psq_accumulators; + + if constexpr (std::is_same_v) + return threat_accumulators; +} + +void AccumulatorStack::reset() noexcept { + psq_accumulators[0].reset({}); + threat_accumulators[0].reset({}); + size = 1; +} + +std::pair AccumulatorStack::push() noexcept { + assert(size < MaxSize); + auto& dp = psq_accumulators[size].reset(); + auto& dts = threat_accumulators[size].reset(); + new (&dts) DirtyThreats; + size++; + return {dp, dts}; +} + +void AccumulatorStack::pop() noexcept { + assert(size > 1); + size--; +} + +template +void AccumulatorStack::evaluate(const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept { + constexpr bool UseThreats = (Dimensions == TransformedFeatureDimensionsBig); + + evaluate_side(WHITE, pos, featureTransformer, cache); + + if (UseThreats) + evaluate_side(WHITE, pos, featureTransformer, cache); + + evaluate_side(BLACK, pos, featureTransformer, cache); + + if (UseThreats) + evaluate_side(BLACK, pos, featureTransformer, cache); +} + +template +void AccumulatorStack::evaluate_side(Color perspective, + const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept { + + const auto last_usable_accum = + find_last_usable_accumulator(perspective); + + if ((accumulators()[last_usable_accum].template acc()) + .computed[perspective]) + forward_update_incremental(perspective, pos, featureTransformer, + last_usable_accum); + + else + { + if constexpr (std::is_same_v) + update_accumulator_refresh_cache(perspective, featureTransformer, pos, + mut_latest(), cache); + else + update_threats_accumulator_full(perspective, featureTransformer, pos, + mut_latest()); + + backward_update_incremental(perspective, pos, featureTransformer, + last_usable_accum); + } +} + +// Find the earliest usable accumulator, this can either be a computed accumulator or the accumulator +// state just before a change that requires full refresh. +template +std::size_t AccumulatorStack::find_last_usable_accumulator(Color perspective) const noexcept { + + for (std::size_t curr_idx = size - 1; curr_idx > 0; curr_idx--) + { + if ((accumulators()[curr_idx].template acc()).computed[perspective]) + return curr_idx; + + if (FeatureSet::requires_refresh(accumulators()[curr_idx].diff, perspective)) + return curr_idx; + } + + return 0; +} + +template +void AccumulatorStack::forward_update_incremental( + Color perspective, + const Position& pos, + const FeatureTransformer& featureTransformer, + const std::size_t begin) noexcept { + + assert(begin < accumulators().size()); + assert((accumulators()[begin].template acc()).computed[perspective]); + + const Square ksq = pos.square(perspective); + + for (std::size_t next = begin + 1; next < size; next++) + { + if (next + 1 < size) + { + DirtyPiece& dp1 = mut_accumulators()[next].diff; + DirtyPiece& dp2 = mut_accumulators()[next + 1].diff; + + auto& accumulators = mut_accumulators(); + + if constexpr (std::is_same_v) + { + if (dp2.remove_sq != SQ_NONE + && (accumulators[next].diff.threateningSqs & square_bb(dp2.remove_sq))) + { + double_inc_update(perspective, featureTransformer, ksq, accumulators[next], + accumulators[next + 1], accumulators[next - 1], dp2); + next++; + continue; + } + } + + if constexpr (std::is_same_v) + { + if (dp1.to != SQ_NONE && dp1.to == dp2.remove_sq) + { + const Square captureSq = dp1.to; + dp1.to = dp2.remove_sq = SQ_NONE; + double_inc_update(perspective, featureTransformer, ksq, accumulators[next], + accumulators[next + 1], accumulators[next - 1]); + dp1.to = dp2.remove_sq = captureSq; + next++; + continue; + } + } + } + + update_accumulator_incremental(perspective, featureTransformer, ksq, + mut_accumulators()[next], + accumulators()[next - 1]); + } + + assert((latest().acc()).computed[perspective]); +} + +template +void AccumulatorStack::backward_update_incremental( + Color perspective, + + const Position& pos, + const FeatureTransformer& featureTransformer, + const std::size_t end) noexcept { + + assert(end < accumulators().size()); + assert(end < size); + assert((latest().template acc()).computed[perspective]); + + const Square ksq = pos.square(perspective); + + for (std::int64_t next = std::int64_t(size) - 2; next >= std::int64_t(end); next--) + update_accumulator_incremental(perspective, featureTransformer, ksq, + mut_accumulators()[next], + accumulators()[next + 1]); + + assert((accumulators()[end].template acc()).computed[perspective]); +} + +// Explicit template instantiations +template void AccumulatorStack::evaluate( + const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept; +template void AccumulatorStack::evaluate( + const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept; + + +namespace { + +template, bool> = true> +void fused_row_reduce(const ElementType* in, ElementType* out, const Ts* const... rows) { + constexpr IndexType size = Width * sizeof(ElementType) / sizeof(typename VectorWrapper::type); + + auto* vecIn = reinterpret_cast(in); + auto* vecOut = reinterpret_cast(out); + + for (IndexType i = 0; i < size; ++i) + vecOut[i] = fused( + vecIn[i], reinterpret_cast(rows)[i]...); +} + +template +struct AccumulatorUpdateContext { + Color perspective; + const FeatureTransformer& featureTransformer; + const AccumulatorState& from; + AccumulatorState& to; + + AccumulatorUpdateContext(Color persp, + const FeatureTransformer& ft, + const AccumulatorState& accF, + AccumulatorState& accT) noexcept : + perspective{persp}, + featureTransformer{ft}, + from{accF}, + to{accT} {} + + template, bool> = true> + void apply(const Ts... indices) { + auto to_weight_vector = [&](const IndexType index) { + return &featureTransformer.weights[index * Dimensions]; + }; + + auto to_psqt_weight_vector = [&](const IndexType index) { + return &featureTransformer.psqtWeights[index * PSQTBuckets]; + }; + + fused_row_reduce( + (from.template acc()).accumulation[perspective].data(), + (to.template acc()).accumulation[perspective].data(), + to_weight_vector(indices)...); + + fused_row_reduce( + (from.template acc()).psqtAccumulation[perspective].data(), + (to.template acc()).psqtAccumulation[perspective].data(), + to_psqt_weight_vector(indices)...); + } + + void apply(const typename FeatureSet::IndexList& added, + const typename FeatureSet::IndexList& removed) { + const auto& fromAcc = from.template acc().accumulation[perspective]; + auto& toAcc = to.template acc().accumulation[perspective]; + + const auto& fromPsqtAcc = from.template acc().psqtAccumulation[perspective]; + auto& toPsqtAcc = to.template acc().psqtAccumulation[perspective]; + +#ifdef VECTOR + using Tiling = SIMDTiling; + vec_t acc[Tiling::NumRegs]; + psqt_vec_t psqt[Tiling::NumPsqtRegs]; + + const auto* threatWeights = &featureTransformer.threatWeights[0]; + + for (IndexType j = 0; j < Dimensions / Tiling::TileHeight; ++j) + { + auto* fromTile = reinterpret_cast(&fromAcc[j * Tiling::TileHeight]); + auto* toTile = reinterpret_cast(&toAcc[j * Tiling::TileHeight]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = fromTile[k]; + + for (int i = 0; i < removed.ssize(); ++i) + { + size_t index = removed[i]; + const size_t offset = Dimensions * index; + auto* column = reinterpret_cast(&threatWeights[offset]); + + #ifdef USE_NEON + for (IndexType k = 0; k < Tiling::NumRegs; k += 2) + { + acc[k] = vec_sub_16(acc[k], vmovl_s8(vget_low_s8(column[k / 2]))); + acc[k + 1] = vec_sub_16(acc[k + 1], vmovl_high_s8(column[k / 2])); + } + #else + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = vec_sub_16(acc[k], vec_convert_8_16(column[k])); + #endif + } + + for (int i = 0; i < added.ssize(); ++i) + { + size_t index = added[i]; + const size_t offset = Dimensions * index; + auto* column = reinterpret_cast(&threatWeights[offset]); + + #ifdef USE_NEON + for (IndexType k = 0; k < Tiling::NumRegs; k += 2) + { + acc[k] = vec_add_16(acc[k], vmovl_s8(vget_low_s8(column[k / 2]))); + acc[k + 1] = vec_add_16(acc[k + 1], vmovl_high_s8(column[k / 2])); + } + #else + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = vec_add_16(acc[k], vec_convert_8_16(column[k])); + #endif + } + + for (IndexType k = 0; k < Tiling::NumRegs; k++) + vec_store(&toTile[k], acc[k]); + + threatWeights += Tiling::TileHeight; + } + + for (IndexType j = 0; j < PSQTBuckets / Tiling::PsqtTileHeight; ++j) + { + auto* fromTilePsqt = + reinterpret_cast(&fromPsqtAcc[j * Tiling::PsqtTileHeight]); + auto* toTilePsqt = + reinterpret_cast(&toPsqtAcc[j * Tiling::PsqtTileHeight]); + + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = fromTilePsqt[k]; + + for (int i = 0; i < removed.ssize(); ++i) + { + size_t index = removed[i]; + const size_t offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = reinterpret_cast( + &featureTransformer.threatPsqtWeights[offset]); + + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = vec_sub_psqt_32(psqt[k], columnPsqt[k]); + } + + for (int i = 0; i < added.ssize(); ++i) + { + size_t index = added[i]; + const size_t offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = reinterpret_cast( + &featureTransformer.threatPsqtWeights[offset]); + + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); + } + + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + vec_store_psqt(&toTilePsqt[k], psqt[k]); + } + +#else + + toAcc = fromAcc; + toPsqtAcc = fromPsqtAcc; + + for (const auto index : removed) + { + const IndexType offset = Dimensions * index; + + for (IndexType j = 0; j < Dimensions; ++j) + toAcc[j] -= featureTransformer.threatWeights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + toPsqtAcc[k] -= featureTransformer.threatPsqtWeights[index * PSQTBuckets + k]; + } + + for (const auto index : added) + { + const IndexType offset = Dimensions * index; + + for (IndexType j = 0; j < Dimensions; ++j) + toAcc[j] += featureTransformer.threatWeights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + toPsqtAcc[k] += featureTransformer.threatPsqtWeights[index * PSQTBuckets + k]; + } + +#endif + } +}; + +template +auto make_accumulator_update_context(Color perspective, + const FeatureTransformer& featureTransformer, + const AccumulatorState& accumulatorFrom, + AccumulatorState& accumulatorTo) noexcept { + return AccumulatorUpdateContext{perspective, featureTransformer, + accumulatorFrom, accumulatorTo}; +} + +template +void double_inc_update(Color perspective, + const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& middle_state, + AccumulatorState& target_state, + const AccumulatorState& computed) { + + assert(computed.acc().computed[perspective]); + assert(!middle_state.acc().computed[perspective]); + assert(!target_state.acc().computed[perspective]); + + PSQFeatureSet::IndexList removed, added; + PSQFeatureSet::append_changed_indices(perspective, ksq, middle_state.diff, removed, added); + // you can't capture a piece that was just involved in castling since the rook ends up + // in a square that the king passed + assert(added.size() < 2); + PSQFeatureSet::append_changed_indices(perspective, ksq, target_state.diff, removed, added); + + assert(added.size() == 1); + assert(removed.size() == 2 || removed.size() == 3); + + // Workaround compiler warning for uninitialized variables, replicated on + // profile builds on windows with gcc 14.2.0. + // TODO remove once unneeded + sf_assume(added.size() == 1); + sf_assume(removed.size() == 2 || removed.size() == 3); + + auto updateContext = + make_accumulator_update_context(perspective, featureTransformer, computed, target_state); + + if (removed.size() == 2) + { + updateContext.template apply(added[0], removed[0], removed[1]); + } + else + { + updateContext.template apply(added[0], removed[0], removed[1], + removed[2]); + } + + target_state.acc().computed[perspective] = true; +} + +template +void double_inc_update(Color perspective, + const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& middle_state, + AccumulatorState& target_state, + const AccumulatorState& computed, + const DirtyPiece& dp2) { + + assert(computed.acc().computed[perspective]); + assert(!middle_state.acc().computed[perspective]); + assert(!target_state.acc().computed[perspective]); + + ThreatFeatureSet::FusedUpdateData fusedData; + + fusedData.dp2removed = dp2.remove_sq; + + ThreatFeatureSet::IndexList removed, added; + ThreatFeatureSet::append_changed_indices(perspective, ksq, middle_state.diff, removed, added, + &fusedData, true); + ThreatFeatureSet::append_changed_indices(perspective, ksq, target_state.diff, removed, added, + &fusedData, false); + + auto updateContext = + make_accumulator_update_context(perspective, featureTransformer, computed, target_state); + + updateContext.apply(added, removed); + + target_state.acc().computed[perspective] = true; +} + +template +void update_accumulator_incremental( + Color perspective, + const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& target_state, + const AccumulatorState& computed) { + + assert((computed.template acc()).computed[perspective]); + assert(!(target_state.template acc()).computed[perspective]); + + // The size must be enough to contain the largest possible update. + // That might depend on the feature set and generally relies on the + // feature set's update cost calculation to be correct and never allow + // updates with more added/removed features than MaxActiveDimensions. + // In this case, the maximum size of both feature addition and removal + // is 2, since we are incrementally updating one move at a time. + typename FeatureSet::IndexList removed, added; + if constexpr (Forward) + FeatureSet::append_changed_indices(perspective, ksq, target_state.diff, removed, added); + else + FeatureSet::append_changed_indices(perspective, ksq, computed.diff, added, removed); + + auto updateContext = + make_accumulator_update_context(perspective, featureTransformer, computed, target_state); + + if constexpr (std::is_same_v) + updateContext.apply(added, removed); + else + { + assert(added.size() == 1 || added.size() == 2); + assert(removed.size() == 1 || removed.size() == 2); + assert((Forward && added.size() <= removed.size()) + || (!Forward && added.size() >= removed.size())); + + // Workaround compiler warning for uninitialized variables, replicated + // on profile builds on windows with gcc 14.2.0. + // TODO remove once unneeded + sf_assume(added.size() == 1 || added.size() == 2); + sf_assume(removed.size() == 1 || removed.size() == 2); + + if ((Forward && removed.size() == 1) || (!Forward && added.size() == 1)) + { + assert(added.size() == 1 && removed.size() == 1); + updateContext.template apply(added[0], removed[0]); + } + else if (Forward && added.size() == 1) + { + assert(removed.size() == 2); + updateContext.template apply(added[0], removed[0], removed[1]); + } + else if (!Forward && removed.size() == 1) + { + assert(added.size() == 2); + updateContext.template apply(added[0], added[1], removed[0]); + } + else + { + assert(added.size() == 2 && removed.size() == 2); + updateContext.template apply(added[0], added[1], removed[0], + removed[1]); + } + } + + (target_state.template acc()).computed[perspective] = true; +} + +Bitboard get_changed_pieces(const std::array& oldPieces, + const std::array& newPieces) { +#if defined(USE_AVX512) || defined(USE_AVX2) + static_assert(sizeof(Piece) == 1); + Bitboard sameBB = 0; + + for (int i = 0; i < 64; i += 32) + { + const __m256i old_v = _mm256_loadu_si256(reinterpret_cast(&oldPieces[i])); + const __m256i new_v = _mm256_loadu_si256(reinterpret_cast(&newPieces[i])); + const __m256i cmpEqual = _mm256_cmpeq_epi8(old_v, new_v); + const std::uint32_t equalMask = _mm256_movemask_epi8(cmpEqual); + sameBB |= static_cast(equalMask) << i; + } + return ~sameBB; +#elif defined(USE_NEON) + uint8x16x4_t old_v = vld4q_u8(reinterpret_cast(oldPieces.data())); + uint8x16x4_t new_v = vld4q_u8(reinterpret_cast(newPieces.data())); + auto cmp = [=](const int i) { return vceqq_u8(old_v.val[i], new_v.val[i]); }; + + uint8x16_t cmp0_1 = vsriq_n_u8(cmp(1), cmp(0), 1); + uint8x16_t cmp2_3 = vsriq_n_u8(cmp(3), cmp(2), 1); + uint8x16_t merged = vsriq_n_u8(cmp2_3, cmp0_1, 2); + merged = vsriq_n_u8(merged, merged, 4); + uint8x8_t sameBB = vshrn_n_u16(vreinterpretq_u16_u8(merged), 4); + + return ~vget_lane_u64(vreinterpret_u64_u8(sameBB), 0); +#else + Bitboard changed = 0; + + for (Square sq = SQUARE_ZERO; sq < SQUARE_NB; ++sq) + changed |= static_cast(oldPieces[sq] != newPieces[sq]) << sq; + + return changed; +#endif +} + +template +void update_accumulator_refresh_cache(Color perspective, + const FeatureTransformer& featureTransformer, + const Position& pos, + AccumulatorState& accumulatorState, + AccumulatorCaches::Cache& cache) { + + using Tiling [[maybe_unused]] = SIMDTiling; + + const Square ksq = pos.square(perspective); + auto& entry = cache[ksq][perspective]; + PSQFeatureSet::IndexList removed, added; + + const Bitboard changedBB = get_changed_pieces(entry.pieces, pos.piece_array()); + Bitboard removedBB = changedBB & entry.pieceBB; + Bitboard addedBB = changedBB & pos.pieces(); + + while (removedBB) + { + Square sq = pop_lsb(removedBB); + removed.push_back(PSQFeatureSet::make_index(perspective, sq, entry.pieces[sq], ksq)); + } + while (addedBB) + { + Square sq = pop_lsb(addedBB); + added.push_back(PSQFeatureSet::make_index(perspective, sq, pos.piece_on(sq), ksq)); + } + + entry.pieceBB = pos.pieces(); + entry.pieces = pos.piece_array(); + + auto& accumulator = accumulatorState.acc(); + accumulator.computed[perspective] = true; + +#ifdef VECTOR + vec_t acc[Tiling::NumRegs]; + psqt_vec_t psqt[Tiling::NumPsqtRegs]; + + const auto* weights = &featureTransformer.weights[0]; + + for (IndexType j = 0; j < Dimensions / Tiling::TileHeight; ++j) + { + auto* accTile = + reinterpret_cast(&accumulator.accumulation[perspective][j * Tiling::TileHeight]); + auto* entryTile = reinterpret_cast(&entry.accumulation[j * Tiling::TileHeight]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = entryTile[k]; + + int i = 0; + for (; i < std::min(removed.ssize(), added.ssize()); ++i) + { + size_t indexR = removed[i]; + const size_t offsetR = Dimensions * indexR; + auto* columnR = reinterpret_cast(&weights[offsetR]); + size_t indexA = added[i]; + const size_t offsetA = Dimensions * indexA; + auto* columnA = reinterpret_cast(&weights[offsetA]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = fused(acc[k], columnA[k], columnR[k]); + } + for (; i < removed.ssize(); ++i) + { + size_t index = removed[i]; + const size_t offset = Dimensions * index; + auto* column = reinterpret_cast(&weights[offset]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = vec_sub_16(acc[k], column[k]); + } + for (; i < added.ssize(); ++i) + { + size_t index = added[i]; + const size_t offset = Dimensions * index; + auto* column = reinterpret_cast(&weights[offset]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = vec_add_16(acc[k], column[k]); + } + + for (IndexType k = 0; k < Tiling::NumRegs; k++) + vec_store(&entryTile[k], acc[k]); + for (IndexType k = 0; k < Tiling::NumRegs; k++) + vec_store(&accTile[k], acc[k]); + + weights += Tiling::TileHeight; + } + + for (IndexType j = 0; j < PSQTBuckets / Tiling::PsqtTileHeight; ++j) + { + auto* accTilePsqt = reinterpret_cast( + &accumulator.psqtAccumulation[perspective][j * Tiling::PsqtTileHeight]); + auto* entryTilePsqt = + reinterpret_cast(&entry.psqtAccumulation[j * Tiling::PsqtTileHeight]); + + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = entryTilePsqt[k]; + + for (int i = 0; i < removed.ssize(); ++i) + { + size_t index = removed[i]; + const size_t offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = + reinterpret_cast(&featureTransformer.psqtWeights[offset]); + + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = vec_sub_psqt_32(psqt[k], columnPsqt[k]); + } + for (int i = 0; i < added.ssize(); ++i) + { + size_t index = added[i]; + const size_t offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = + reinterpret_cast(&featureTransformer.psqtWeights[offset]); + + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); + } + + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + vec_store_psqt(&entryTilePsqt[k], psqt[k]); + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + vec_store_psqt(&accTilePsqt[k], psqt[k]); + } + +#else + + for (const auto index : removed) + { + const IndexType offset = Dimensions * index; + for (IndexType j = 0; j < Dimensions; ++j) + entry.accumulation[j] -= featureTransformer.weights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + entry.psqtAccumulation[k] -= featureTransformer.psqtWeights[index * PSQTBuckets + k]; + } + for (const auto index : added) + { + const IndexType offset = Dimensions * index; + for (IndexType j = 0; j < Dimensions; ++j) + entry.accumulation[j] += featureTransformer.weights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + entry.psqtAccumulation[k] += featureTransformer.psqtWeights[index * PSQTBuckets + k]; + } + + // The accumulator of the refresh entry has been updated. + // Now copy its content to the actual accumulator we were refreshing. + accumulator.accumulation[perspective] = entry.accumulation; + accumulator.psqtAccumulation[perspective] = entry.psqtAccumulation; +#endif +} + +template +void update_threats_accumulator_full(Color perspective, + const FeatureTransformer& featureTransformer, + const Position& pos, + AccumulatorState& accumulatorState) { + using Tiling [[maybe_unused]] = SIMDTiling; + + ThreatFeatureSet::IndexList active; + ThreatFeatureSet::append_active_indices(perspective, pos, active); + + auto& accumulator = accumulatorState.acc(); + accumulator.computed[perspective] = true; + +#ifdef VECTOR + vec_t acc[Tiling::NumRegs]; + psqt_vec_t psqt[Tiling::NumPsqtRegs]; + + const auto* threatWeights = &featureTransformer.threatWeights[0]; + + for (IndexType j = 0; j < Dimensions / Tiling::TileHeight; ++j) + { + auto* accTile = + reinterpret_cast(&accumulator.accumulation[perspective][j * Tiling::TileHeight]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = vec_zero(); + + int i = 0; + + for (; i < active.ssize(); ++i) + { + size_t index = active[i]; + const size_t offset = Dimensions * index; + auto* column = reinterpret_cast(&threatWeights[offset]); + + #ifdef USE_NEON + for (IndexType k = 0; k < Tiling::NumRegs; k += 2) + { + acc[k] = vec_add_16(acc[k], vmovl_s8(vget_low_s8(column[k / 2]))); + acc[k + 1] = vec_add_16(acc[k + 1], vmovl_high_s8(column[k / 2])); + } + #else + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = vec_add_16(acc[k], vec_convert_8_16(column[k])); + #endif + } + + for (IndexType k = 0; k < Tiling::NumRegs; k++) + vec_store(&accTile[k], acc[k]); + + threatWeights += Tiling::TileHeight; + } + + for (IndexType j = 0; j < PSQTBuckets / Tiling::PsqtTileHeight; ++j) + { + auto* accTilePsqt = reinterpret_cast( + &accumulator.psqtAccumulation[perspective][j * Tiling::PsqtTileHeight]); + + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = vec_zero_psqt(); + + for (int i = 0; i < active.ssize(); ++i) + { + size_t index = active[i]; + const size_t offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = + reinterpret_cast(&featureTransformer.threatPsqtWeights[offset]); + + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); + } + + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + vec_store_psqt(&accTilePsqt[k], psqt[k]); + } + +#else + + for (IndexType j = 0; j < Dimensions; ++j) + accumulator.accumulation[perspective][j] = 0; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + accumulator.psqtAccumulation[perspective][k] = 0; + + for (const auto index : active) + { + const IndexType offset = Dimensions * index; + + for (IndexType j = 0; j < Dimensions; ++j) + accumulator.accumulation[perspective][j] += + featureTransformer.threatWeights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + accumulator.psqtAccumulation[perspective][k] += + featureTransformer.threatPsqtWeights[index * PSQTBuckets + k]; + } + +#endif +} + +} + +} diff --git a/src/nnue/nnue_accumulator.h b/src/nnue/nnue_accumulator.h index c0746b4ee..438074f43 100644 --- a/src/nnue/nnue_accumulator.h +++ b/src/nnue/nnue_accumulator.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -21,20 +21,184 @@ #ifndef NNUE_ACCUMULATOR_H_INCLUDED #define NNUE_ACCUMULATOR_H_INCLUDED +#include +#include #include +#include +#include +#include "../types.h" #include "nnue_architecture.h" #include "nnue_common.h" +namespace Stockfish { +class Position; +} + namespace Stockfish::Eval::NNUE { +template +struct alignas(CacheLineSize) Accumulator; + +template +class FeatureTransformer; + // Class that holds the result of affine transformation of input features template struct alignas(CacheLineSize) Accumulator { - std::int16_t accumulation[2][Size]; - std::int32_t psqtAccumulation[2][PSQTBuckets]; - bool computed[2]; - bool computedPSQT[2]; + std::array, COLOR_NB> accumulation; + std::array, COLOR_NB> psqtAccumulation; + std::array computed = {}; +}; + + +// AccumulatorCaches struct provides per-thread accumulator caches, where each +// cache contains multiple entries for each of the possible king squares. +// When the accumulator needs to be refreshed, the cached entry is used to more +// efficiently update the accumulator, instead of rebuilding it from scratch. +// This idea, was first described by Luecx (author of Koivisto) and +// is commonly referred to as "Finny Tables". +struct AccumulatorCaches { + + template + AccumulatorCaches(const Networks& networks) { + clear(networks); + } + + template + struct alignas(CacheLineSize) Cache { + + struct alignas(CacheLineSize) Entry { + std::array accumulation; + std::array psqtAccumulation; + std::array pieces; + Bitboard pieceBB; + + // To initialize a refresh entry, we set all its bitboards empty, + // so we put the biases in the accumulation, without any weights on top + void clear(const std::array& biases) { + accumulation = biases; + std::memset(reinterpret_cast(this) + offsetof(Entry, psqtAccumulation), + 0, sizeof(Entry) - offsetof(Entry, psqtAccumulation)); + } + }; + + template + void clear(const Network& network) { + for (auto& entries1D : entries) + for (auto& entry : entries1D) + entry.clear(network.featureTransformer.biases); + } + + std::array& operator[](Square sq) { return entries[sq]; } + + std::array, SQUARE_NB> entries; + }; + + template + void clear(const Networks& networks) { + big.clear(networks.big); + small.clear(networks.small); + } + + Cache big; + Cache small; +}; + + +template +struct AccumulatorState { + Accumulator accumulatorBig; + Accumulator accumulatorSmall; + typename FeatureSet::DiffType diff; + + template + auto& acc() noexcept { + static_assert(Size == TransformedFeatureDimensionsBig + || Size == TransformedFeatureDimensionsSmall, + "Invalid size for accumulator"); + + if constexpr (Size == TransformedFeatureDimensionsBig) + return accumulatorBig; + else if constexpr (Size == TransformedFeatureDimensionsSmall) + return accumulatorSmall; + } + + template + const auto& acc() const noexcept { + static_assert(Size == TransformedFeatureDimensionsBig + || Size == TransformedFeatureDimensionsSmall, + "Invalid size for accumulator"); + + if constexpr (Size == TransformedFeatureDimensionsBig) + return accumulatorBig; + else if constexpr (Size == TransformedFeatureDimensionsSmall) + return accumulatorSmall; + } + + void reset(const typename FeatureSet::DiffType& dp) noexcept { + diff = dp; + accumulatorBig.computed.fill(false); + accumulatorSmall.computed.fill(false); + } + + typename FeatureSet::DiffType& reset() noexcept { + accumulatorBig.computed.fill(false); + accumulatorSmall.computed.fill(false); + return diff; + } +}; + +class AccumulatorStack { + public: + static constexpr std::size_t MaxSize = MAX_PLY + 1; + + template + [[nodiscard]] const AccumulatorState& latest() const noexcept; + + void reset() noexcept; + std::pair push() noexcept; + void pop() noexcept; + + template + void evaluate(const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept; + + private: + template + [[nodiscard]] AccumulatorState& mut_latest() noexcept; + + template + [[nodiscard]] const std::array, MaxSize>& accumulators() const noexcept; + + template + [[nodiscard]] std::array, MaxSize>& mut_accumulators() noexcept; + + template + void evaluate_side(Color perspective, + const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept; + + template + [[nodiscard]] std::size_t find_last_usable_accumulator(Color perspective) const noexcept; + + template + void forward_update_incremental(Color perspective, + const Position& pos, + const FeatureTransformer& featureTransformer, + const std::size_t begin) noexcept; + + template + void backward_update_incremental(Color perspective, + const Position& pos, + const FeatureTransformer& featureTransformer, + const std::size_t end) noexcept; + + std::array, MaxSize> psq_accumulators; + std::array, MaxSize> threat_accumulators; + std::size_t size = 1; }; } // namespace Stockfish::Eval::NNUE diff --git a/src/nnue/nnue_architecture.h b/src/nnue/nnue_architecture.h index 7f73f87fd..71fce9bd5 100644 --- a/src/nnue/nnue_architecture.h +++ b/src/nnue/nnue_architecture.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -26,6 +26,7 @@ #include #include "features/half_ka_v2_hm.h" +#include "features/full_threats.h" #include "layers/affine_transform.h" #include "layers/affine_transform_sparse_input.h" #include "layers/clipped_relu.h" @@ -35,10 +36,11 @@ namespace Stockfish::Eval::NNUE { // Input features used in evaluation function -using FeatureSet = Features::HalfKAv2_hm; +using ThreatFeatureSet = Features::FullThreats; +using PSQFeatureSet = Features::HalfKAv2_hm; // Number of input feature dimensions after conversion -constexpr IndexType TransformedFeatureDimensionsBig = 3072; +constexpr IndexType TransformedFeatureDimensionsBig = 1024; constexpr int L2Big = 15; constexpr int L3Big = 32; @@ -49,6 +51,12 @@ constexpr int L3Small = 32; constexpr IndexType PSQTBuckets = 8; constexpr IndexType LayerStacks = 8; +// If vector instructions are enabled, we update and refresh the +// accumulator tile by tile such that each tile fits in the CPU's +// vector registers. +static_assert(PSQTBuckets % 8 == 0, + "Per feature PSQT values cannot be processed at granularity lower than 8 at a time."); + template struct NetworkArchitecture { static constexpr IndexType TransformedFeatureDimensions = L1; @@ -91,7 +99,7 @@ struct NetworkArchitecture { && fc_2.write_parameters(stream); } - std::int32_t propagate(const TransformedFeatureType* transformedFeatures) { + std::int32_t propagate(const TransformedFeatureType* transformedFeatures) const { struct alignas(CacheLineSize) Buffer { alignas(CacheLineSize) typename decltype(fc_0)::OutputBuffer fc_0_out; alignas(CacheLineSize) typename decltype(ac_sqr_0)::OutputType @@ -130,8 +138,28 @@ struct NetworkArchitecture { return outputValue; } + + std::size_t get_content_hash() const { + std::size_t h = 0; + hash_combine(h, fc_0.get_content_hash()); + hash_combine(h, ac_sqr_0.get_content_hash()); + hash_combine(h, ac_0.get_content_hash()); + hash_combine(h, fc_1.get_content_hash()); + hash_combine(h, ac_1.get_content_hash()); + hash_combine(h, fc_2.get_content_hash()); + hash_combine(h, get_hash_value()); + return h; + } }; } // namespace Stockfish::Eval::NNUE +template +struct std::hash> { + std::size_t + operator()(const Stockfish::Eval::NNUE::NetworkArchitecture& arch) const noexcept { + return arch.get_content_hash(); + } +}; + #endif // #ifndef NNUE_ARCHITECTURE_H_INCLUDED diff --git a/src/nnue/nnue_common.h b/src/nnue/nnue_common.h index 4bc3408f1..febe7ca70 100644 --- a/src/nnue/nnue_common.h +++ b/src/nnue/nnue_common.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -48,6 +48,12 @@ namespace Stockfish::Eval::NNUE { +using BiasType = std::int16_t; +using ThreatWeightType = std::int8_t; +using WeightType = std::int16_t; +using PSQTWeightType = std::int32_t; +using IndexType = std::uint32_t; + // Version of the evaluation file constexpr std::uint32_t Version = 0x7AF32F20u; @@ -76,7 +82,6 @@ constexpr std::size_t MaxSimdWidth = 32; // Type of input feature after conversion using TransformedFeatureType = std::uint8_t; -using IndexType = std::uint32_t; // Round n up to be a multiple of base template @@ -168,8 +173,8 @@ inline void write_little_endian(std::ostream& stream, const IntType* values, std // 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 -inline void read_leb_128(std::istream& stream, IntType* out, std::size_t count) { +template +inline void read_leb_128(std::istream& stream, std::array& out) { // Check the presence of our LEB128 magic string char leb128MagicString[Leb128MagicStringSize]; @@ -184,7 +189,7 @@ inline void read_leb_128(std::istream& stream, IntType* out, std::size_t count) auto bytes_left = read_little_endian(stream); std::uint32_t buf_pos = BUF_SIZE; - for (std::size_t i = 0; i < count; ++i) + for (std::size_t i = 0; i < Count; ++i) { IntType result = 0; size_t shift = 0; @@ -219,8 +224,8 @@ inline void read_leb_128(std::istream& stream, IntType* out, std::size_t count) // This takes N integers from array values, compresses them with // the LEB128 algorithm and writes the result on the stream s. // See https://en.wikipedia.org/wiki/LEB128 for a description of the compression scheme. -template -inline void write_leb_128(std::ostream& stream, const IntType* values, std::size_t count) { +template +inline void write_leb_128(std::ostream& stream, const std::array& values) { // Write our LEB128 magic string stream.write(Leb128MagicString, Leb128MagicStringSize); @@ -228,7 +233,7 @@ inline void write_leb_128(std::ostream& stream, const IntType* values, std::size static_assert(std::is_signed_v, "Not implemented for unsigned types"); std::uint32_t byte_count = 0; - for (std::size_t i = 0; i < count; ++i) + for (std::size_t i = 0; i < Count; ++i) { IntType value = values[i]; std::uint8_t byte; @@ -254,13 +259,13 @@ inline void write_leb_128(std::ostream& stream, const IntType* values, std::size } }; - auto write = [&](std::uint8_t byte) { - buf[buf_pos++] = byte; + auto write = [&](std::uint8_t b) { + buf[buf_pos++] = b; if (buf_pos == BUF_SIZE) flush(); }; - for (std::size_t i = 0; i < count; ++i) + for (std::size_t i = 0; i < Count; ++i) { IntType value = values[i]; while (true) diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h index 888edebbd..99cda2a69 100644 --- a/src/nnue/nnue_feature_transformer.h +++ b/src/nnue/nnue_feature_transformer.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -22,249 +22,254 @@ #define NNUE_FEATURE_TRANSFORMER_H_INCLUDED #include -#include #include #include #include -#include +#include #include "../position.h" #include "../types.h" #include "nnue_accumulator.h" #include "nnue_architecture.h" #include "nnue_common.h" +#include "simd.h" namespace Stockfish::Eval::NNUE { -using BiasType = std::int16_t; -using WeightType = std::int16_t; -using PSQTWeightType = std::int32_t; - -// If vector instructions are enabled, we update and refresh the -// accumulator tile by tile such that each tile fits in the CPU's -// vector registers. -#define VECTOR - -static_assert(PSQTBuckets % 8 == 0, - "Per feature PSQT values cannot be processed at granularity lower than 8 at a time."); - -#ifdef USE_AVX512 -using vec_t = __m512i; -using psqt_vec_t = __m256i; - #define vec_load(a) _mm512_load_si512(a) - #define vec_store(a, b) _mm512_store_si512(a, b) - #define vec_add_16(a, b) _mm512_add_epi16(a, b) - #define vec_sub_16(a, b) _mm512_sub_epi16(a, b) - #define vec_mul_16(a, b) _mm512_mullo_epi16(a, b) - #define vec_zero() _mm512_setzero_epi32() - #define vec_set_16(a) _mm512_set1_epi16(a) - #define vec_max_16(a, b) _mm512_max_epi16(a, b) - #define vec_min_16(a, b) _mm512_min_epi16(a, b) -inline vec_t vec_msb_pack_16(vec_t a, vec_t b) { - vec_t compacted = _mm512_packs_epi16(_mm512_srli_epi16(a, 7), _mm512_srli_epi16(b, 7)); - return _mm512_permutexvar_epi64(_mm512_setr_epi64(0, 2, 4, 6, 1, 3, 5, 7), compacted); +// Returns the inverse of a permutation +template +constexpr std::array +invert_permutation(const std::array& order) { + std::array inverse{}; + for (std::size_t i = 0; i < order.size(); i++) + inverse[order[i]] = i; + return inverse; } - #define vec_load_psqt(a) _mm256_load_si256(a) - #define vec_store_psqt(a, b) _mm256_store_si256(a, b) - #define vec_add_psqt_32(a, b) _mm256_add_epi32(a, b) - #define vec_sub_psqt_32(a, b) _mm256_sub_epi32(a, b) - #define vec_zero_psqt() _mm256_setzero_si256() - #define NumRegistersSIMD 16 - #define MaxChunkSize 64 -#elif USE_AVX2 -using vec_t = __m256i; -using psqt_vec_t = __m256i; - #define vec_load(a) _mm256_load_si256(a) - #define vec_store(a, b) _mm256_store_si256(a, b) - #define vec_add_16(a, b) _mm256_add_epi16(a, b) - #define vec_sub_16(a, b) _mm256_sub_epi16(a, b) - #define vec_mul_16(a, b) _mm256_mullo_epi16(a, b) - #define vec_zero() _mm256_setzero_si256() - #define vec_set_16(a) _mm256_set1_epi16(a) - #define vec_max_16(a, b) _mm256_max_epi16(a, b) - #define vec_min_16(a, b) _mm256_min_epi16(a, b) -inline vec_t vec_msb_pack_16(vec_t a, vec_t b) { - vec_t compacted = _mm256_packs_epi16(_mm256_srli_epi16(a, 7), _mm256_srli_epi16(b, 7)); - return _mm256_permute4x64_epi64(compacted, 0b11011000); +// Divide a byte region of size TotalSize to chunks of size +// BlockSize, and permute the blocks by a given order +template +void permute(std::array& data, const std::array& order) { + constexpr std::size_t TotalSize = N * sizeof(T); + + static_assert(TotalSize % (BlockSize * OrderSize) == 0, + "ChunkSize * OrderSize must perfectly divide TotalSize"); + + constexpr std::size_t ProcessChunkSize = BlockSize * OrderSize; + + std::array buffer{}; + + std::byte* const bytes = reinterpret_cast(data.data()); + + for (std::size_t i = 0; i < TotalSize; i += ProcessChunkSize) + { + std::byte* const values = &bytes[i]; + + for (std::size_t j = 0; j < OrderSize; j++) + { + auto* const buffer_chunk = &buffer[j * BlockSize]; + auto* const value_chunk = &values[order[j] * BlockSize]; + + std::copy(value_chunk, value_chunk + BlockSize, buffer_chunk); + } + + std::copy(std::begin(buffer), std::end(buffer), values); + } } - #define vec_load_psqt(a) _mm256_load_si256(a) - #define vec_store_psqt(a, b) _mm256_store_si256(a, b) - #define vec_add_psqt_32(a, b) _mm256_add_epi32(a, b) - #define vec_sub_psqt_32(a, b) _mm256_sub_epi32(a, b) - #define vec_zero_psqt() _mm256_setzero_si256() - #define NumRegistersSIMD 16 - #define MaxChunkSize 32 - -#elif USE_SSE2 -using vec_t = __m128i; -using psqt_vec_t = __m128i; - #define vec_load(a) (*(a)) - #define vec_store(a, b) *(a) = (b) - #define vec_add_16(a, b) _mm_add_epi16(a, b) - #define vec_sub_16(a, b) _mm_sub_epi16(a, b) - #define vec_mul_16(a, b) _mm_mullo_epi16(a, b) - #define vec_zero() _mm_setzero_si128() - #define vec_set_16(a) _mm_set1_epi16(a) - #define vec_max_16(a, b) _mm_max_epi16(a, b) - #define vec_min_16(a, b) _mm_min_epi16(a, b) - #define vec_msb_pack_16(a, b) _mm_packs_epi16(_mm_srli_epi16(a, 7), _mm_srli_epi16(b, 7)) - #define vec_load_psqt(a) (*(a)) - #define vec_store_psqt(a, b) *(a) = (b) - #define vec_add_psqt_32(a, b) _mm_add_epi32(a, b) - #define vec_sub_psqt_32(a, b) _mm_sub_epi32(a, b) - #define vec_zero_psqt() _mm_setzero_si128() - #define NumRegistersSIMD (Is64Bit ? 16 : 8) - #define MaxChunkSize 16 - -#elif USE_NEON -using vec_t = int16x8_t; -using psqt_vec_t = int32x4_t; - #define vec_load(a) (*(a)) - #define vec_store(a, b) *(a) = (b) - #define vec_add_16(a, b) vaddq_s16(a, b) - #define vec_sub_16(a, b) vsubq_s16(a, b) - #define vec_mul_16(a, b) vmulq_s16(a, b) - #define vec_zero() \ - vec_t { 0 } - #define vec_set_16(a) vdupq_n_s16(a) - #define vec_max_16(a, b) vmaxq_s16(a, b) - #define vec_min_16(a, b) vminq_s16(a, b) -inline vec_t vec_msb_pack_16(vec_t a, vec_t b) { - const int8x8_t shifta = vshrn_n_s16(a, 7); - const int8x8_t shiftb = vshrn_n_s16(b, 7); - const int8x16_t compacted = vcombine_s8(shifta, shiftb); - return *reinterpret_cast(&compacted); -} - #define vec_load_psqt(a) (*(a)) - #define vec_store_psqt(a, b) *(a) = (b) - #define vec_add_psqt_32(a, b) vaddq_s32(a, b) - #define vec_sub_psqt_32(a, b) vsubq_s32(a, b) - #define vec_zero_psqt() \ - psqt_vec_t { 0 } - #define NumRegistersSIMD 16 - #define MaxChunkSize 16 - -#else - #undef VECTOR - -#endif - - -#ifdef VECTOR - - // Compute optimal SIMD register count for feature transformer accumulation. - - // We use __m* types as template arguments, which causes GCC to emit warnings - // about losing some attribute information. This is irrelevant to us as we - // only take their size, so the following pragma are harmless. - #if defined(__GNUC__) - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wignored-attributes" - #endif - -template -static constexpr int BestRegisterCount() { - #define RegisterSize sizeof(SIMDRegisterType) - #define LaneSize sizeof(LaneType) - - static_assert(RegisterSize >= LaneSize); - static_assert(MaxRegisters <= NumRegistersSIMD); - static_assert(MaxRegisters > 0); - static_assert(NumRegistersSIMD > 0); - static_assert(RegisterSize % LaneSize == 0); - static_assert((NumLanes * LaneSize) % RegisterSize == 0); - - const int ideal = (NumLanes * LaneSize) / RegisterSize; - if (ideal <= MaxRegisters) - return ideal; - - // Look for the largest divisor of the ideal register count that is smaller than MaxRegisters - for (int divisor = MaxRegisters; divisor > 1; --divisor) - if (ideal % divisor == 0) - return divisor; - - return 1; -} - #if defined(__GNUC__) - #pragma GCC diagnostic pop - #endif -#endif - // Input feature converter -template StateInfo::*accPtr> +template class FeatureTransformer { - - private: + static constexpr bool UseThreats = + (TransformedFeatureDimensions == TransformedFeatureDimensionsBig); // Number of output dimensions for one side static constexpr IndexType HalfDimensions = TransformedFeatureDimensions; -#ifdef VECTOR - static constexpr int NumRegs = - BestRegisterCount(); - static constexpr int NumPsqtRegs = - BestRegisterCount(); - - static constexpr IndexType TileHeight = NumRegs * sizeof(vec_t) / 2; - static constexpr IndexType PsqtTileHeight = NumPsqtRegs * sizeof(psqt_vec_t) / 4; - static_assert(HalfDimensions % TileHeight == 0, "TileHeight must divide HalfDimensions"); - static_assert(PSQTBuckets % PsqtTileHeight == 0, "PsqtTileHeight must divide PSQTBuckets"); -#endif - public: // Output type using OutputType = TransformedFeatureType; // Number of input/output dimensions - static constexpr IndexType InputDimensions = FeatureSet::Dimensions; + static constexpr IndexType InputDimensions = PSQFeatureSet::Dimensions; + static constexpr IndexType ThreatInputDimensions = ThreatFeatureSet::Dimensions; + static constexpr IndexType TotalInputDimensions = + InputDimensions + (UseThreats ? ThreatInputDimensions : 0); static constexpr IndexType OutputDimensions = HalfDimensions; // Size of forward propagation buffer static constexpr std::size_t BufferSize = OutputDimensions * sizeof(OutputType); + // Store the order by which 128-bit blocks of a 1024-bit data must + // be permuted so that calling packus on adjacent vectors of 16-bit + // integers loaded from the data results in the pre-permutation order + static constexpr auto PackusEpi16Order = []() -> std::array { +#if defined(USE_AVX512) + // _mm512_packus_epi16 after permutation: + // | 0 | 2 | 4 | 6 | // Vector 0 + // | 1 | 3 | 5 | 7 | // Vector 1 + // | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | // Packed Result + return {0, 2, 4, 6, 1, 3, 5, 7}; +#elif defined(USE_AVX2) + // _mm256_packus_epi16 after permutation: + // | 0 | 2 | | 4 | 6 | // Vector 0, 2 + // | 1 | 3 | | 5 | 7 | // Vector 1, 3 + // | 0 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | // Packed Result + return {0, 2, 1, 3, 4, 6, 5, 7}; +#else + return {0, 1, 2, 3, 4, 5, 6, 7}; +#endif + }(); + + static constexpr auto InversePackusEpi16Order = invert_permutation(PackusEpi16Order); + // Hash value embedded in the evaluation file static constexpr std::uint32_t get_hash_value() { - return FeatureSet::HashValue ^ (OutputDimensions * 2); + return (UseThreats ? ThreatFeatureSet::HashValue : PSQFeatureSet::HashValue) + ^ (OutputDimensions * 2); + } + + void permute_weights() { + permute<16>(biases, PackusEpi16Order); + permute<16>(weights, PackusEpi16Order); + + if (UseThreats) + permute<8>(threatWeights, PackusEpi16Order); + } + + void unpermute_weights() { + permute<16>(biases, InversePackusEpi16Order); + permute<16>(weights, InversePackusEpi16Order); + + if (UseThreats) + permute<8>(threatWeights, InversePackusEpi16Order); + } + + inline void scale_weights(bool read) { + for (auto& w : weights) + w = read ? w * 2 : w / 2; + for (auto& b : biases) + b = read ? b * 2 : b / 2; } // 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(stream, biases); - read_leb_128(stream, biases, HalfDimensions); - read_leb_128(stream, weights, HalfDimensions * InputDimensions); - read_leb_128(stream, psqtWeights, PSQTBuckets * InputDimensions); + if (UseThreats) + { + read_little_endian(stream, threatWeights.data(), + ThreatInputDimensions * HalfDimensions); + read_leb_128(stream, weights); + + auto combinedPsqtWeights = + std::make_unique>(); + + read_leb_128(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)); + } + else + { + read_leb_128(stream, weights); + read_leb_128(stream, psqtWeights); + } + + permute_weights(); + + if (!UseThreats) + scale_weights(true); return !stream.fail(); } // Write network parameters bool write_parameters(std::ostream& stream) const { + std::unique_ptr copy = std::make_unique(*this); - write_leb_128(stream, biases, HalfDimensions); - write_leb_128(stream, weights, HalfDimensions * InputDimensions); - write_leb_128(stream, psqtWeights, PSQTBuckets * InputDimensions); + copy->unpermute_weights(); + + if (!UseThreats) + copy->scale_weights(false); + + write_leb_128(stream, copy->biases); + + if (UseThreats) + { + write_little_endian(stream, copy->threatWeights.data(), + ThreatInputDimensions * HalfDimensions); + write_leb_128(stream, copy->weights); + + auto combinedPsqtWeights = + std::make_unique>(); + + std::copy(std::begin(copy->threatPsqtWeights), + std::begin(copy->threatPsqtWeights) + ThreatInputDimensions * PSQTBuckets, + combinedPsqtWeights->begin()); + + std::copy(std::begin(copy->psqtWeights), + std::begin(copy->psqtWeights) + InputDimensions * PSQTBuckets, + combinedPsqtWeights->begin() + ThreatInputDimensions * PSQTBuckets); + + write_leb_128(stream, *combinedPsqtWeights); + } + else + { + write_leb_128(stream, copy->weights); + write_leb_128(stream, copy->psqtWeights); + } return !stream.fail(); } + std::size_t get_content_hash() const { + std::size_t h = 0; + hash_combine(h, get_raw_data_hash(biases)); + hash_combine(h, get_raw_data_hash(weights)); + hash_combine(h, get_raw_data_hash(psqtWeights)); + hash_combine(h, get_hash_value()); + return h; + } + // Convert input features - std::int32_t - transform(const Position& pos, OutputType* output, int bucket, bool psqtOnly) const { - update_accumulator(pos, psqtOnly); - update_accumulator(pos, psqtOnly); + std::int32_t transform(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache& cache, + OutputType* output, + int bucket) const { + + using namespace SIMD; + accumulatorStack.evaluate(pos, *this, cache); + const auto& accumulatorState = accumulatorStack.latest(); + const auto& threatAccumulatorState = accumulatorStack.latest(); const Color perspectives[2] = {pos.side_to_move(), ~pos.side_to_move()}; - const auto& psqtAccumulation = (pos.state()->*accPtr).psqtAccumulation; - const auto psqt = - (psqtAccumulation[perspectives[0]][bucket] - psqtAccumulation[perspectives[1]][bucket]) - / 2; + const auto& psqtAccumulation = (accumulatorState.acc()).psqtAccumulation; + auto psqt = + (psqtAccumulation[perspectives[0]][bucket] - psqtAccumulation[perspectives[1]][bucket]); - if (psqtOnly) - return psqt; + if (UseThreats) + { + const auto& threatPsqtAccumulation = + (threatAccumulatorState.acc()).psqtAccumulation; + psqt = (psqt + threatPsqtAccumulation[perspectives[0]][bucket] + - threatPsqtAccumulation[perspectives[1]][bucket]) + / 2; + } + else + psqt /= 2; - const auto& accumulation = (pos.state()->*accPtr).accumulation; + const auto& accumulation = (accumulatorState.acc()).accumulation; + const auto& threatAccumulation = + (threatAccumulatorState.acc()).accumulation; for (IndexType p = 0; p < 2; ++p) { @@ -276,25 +281,115 @@ class FeatureTransformer { static_assert((HalfDimensions / 2) % OutputChunkSize == 0); constexpr IndexType NumOutputChunks = HalfDimensions / 2 / OutputChunkSize; - vec_t Zero = vec_zero(); - vec_t One = vec_set_16(127); + const vec_t Zero = vec_zero(); + const vec_t One = vec_set_16(UseThreats ? 255 : 127 * 2); const vec_t* in0 = reinterpret_cast(&(accumulation[perspectives[p]][0])); const vec_t* in1 = reinterpret_cast(&(accumulation[perspectives[p]][HalfDimensions / 2])); vec_t* out = reinterpret_cast(output + offset); - for (IndexType j = 0; j < NumOutputChunks; ++j) + // Per the NNUE architecture, here we want to multiply pairs of + // clipped elements and divide the product by 128. To do this, + // we can naively perform min/max operation to clip each of the + // four int16 vectors, mullo pairs together, then pack them into + // one int8 vector. However, there exists a faster way. + + // The idea here is to use the implicit clipping from packus to + // save us two vec_max_16 instructions. This clipping works due + // to the fact that any int16 integer below zero will be zeroed + // on packus. + + // Consider the case where the second element is negative. + // If we do standard clipping, that element will be zero, which + // means our pairwise product is zero. If we perform packus and + // remove the lower-side clip for the second element, then our + // product before packus will be negative, and is zeroed on pack. + // The two operation produce equivalent results, but the second + // one (using packus) saves one max operation per pair. + + // But here we run into a problem: mullo does not preserve the + // sign of the multiplication. We can get around this by doing + // mulhi, which keeps the sign. But that requires an additional + // tweak. + + // mulhi cuts off the last 16 bits of the resulting product, + // which is the same as performing a rightward shift of 16 bits. + // We can use this to our advantage. Recall that we want to + // divide the final product by 128, which is equivalent to a + // 7-bit right shift. Intuitively, if we shift the clipped + // value left by 9, and perform mulhi, which shifts the product + // right by 16 bits, then we will net a right shift of 7 bits. + // However, this won't work as intended. Since we clip the + // values to have a maximum value of 127, shifting it by 9 bits + // might occupy the signed bit, resulting in some positive + // values being interpreted as negative after the shift. + + // There is a way, however, to get around this limitation. When + // loading the network, scale accumulator weights and biases by + // 2. To get the same pairwise multiplication result as before, + // we need to divide the product by 128 * 2 * 2 = 512, which + // amounts to a right shift of 9 bits. So now we only have to + // shift left by 7 bits, perform mulhi (shifts right by 16 bits) + // and net a 9 bit right shift. Since we scaled everything by + // two, the values are clipped at 127 * 2 = 254, which occupies + // 8 bits. Shifting it by 7 bits left will no longer occupy the + // signed bit, so we are safe. + + // Note that on NEON processors, we shift left by 6 instead + // because the instruction "vqdmulhq_s16" also doubles the + // return value after the multiplication, adding an extra shift + // to the left by 1, so we compensate by shifting less before + // the multiplication. + + constexpr int shift = + #if defined(USE_SSE2) + 7; + #else + 6; + #endif + if (UseThreats) { - const vec_t sum0a = vec_max_16(vec_min_16(in0[j * 2 + 0], One), Zero); - const vec_t sum0b = vec_max_16(vec_min_16(in0[j * 2 + 1], One), Zero); - const vec_t sum1a = vec_max_16(vec_min_16(in1[j * 2 + 0], One), Zero); - const vec_t sum1b = vec_max_16(vec_min_16(in1[j * 2 + 1], One), Zero); + const vec_t* tin0 = + reinterpret_cast(&(threatAccumulation[perspectives[p]][0])); + const vec_t* tin1 = reinterpret_cast( + &(threatAccumulation[perspectives[p]][HalfDimensions / 2])); + for (IndexType j = 0; j < NumOutputChunks; ++j) + { + const vec_t acc0a = vec_add_16(in0[j * 2 + 0], tin0[j * 2 + 0]); + const vec_t acc0b = vec_add_16(in0[j * 2 + 1], tin0[j * 2 + 1]); + const vec_t acc1a = vec_add_16(in1[j * 2 + 0], tin1[j * 2 + 0]); + const vec_t acc1b = vec_add_16(in1[j * 2 + 1], tin1[j * 2 + 1]); - const vec_t pa = vec_mul_16(sum0a, sum1a); - const vec_t pb = vec_mul_16(sum0b, sum1b); + const vec_t sum0a = + vec_slli_16(vec_max_16(vec_min_16(acc0a, One), Zero), shift); + const vec_t sum0b = + vec_slli_16(vec_max_16(vec_min_16(acc0b, One), Zero), shift); + const vec_t sum1a = vec_min_16(acc1a, One); + const vec_t sum1b = vec_min_16(acc1b, One); - out[j] = vec_msb_pack_16(pa, pb); + const vec_t pa = vec_mulhi_16(sum0a, sum1a); + const vec_t pb = vec_mulhi_16(sum0b, sum1b); + + out[j] = vec_packus_16(pa, pb); + } + } + else + { + for (IndexType j = 0; j < NumOutputChunks; ++j) + { + const vec_t sum0a = + vec_slli_16(vec_max_16(vec_min_16(in0[j * 2 + 0], One), Zero), shift); + const vec_t sum0b = + vec_slli_16(vec_max_16(vec_min_16(in0[j * 2 + 1], One), Zero), shift); + const vec_t sum1a = vec_min_16(in1[j * 2 + 0], One); + const vec_t sum1b = vec_min_16(in1[j * 2 + 1], One); + + const vec_t pa = vec_mulhi_16(sum0a, sum1a); + const vec_t pb = vec_mulhi_16(sum0b, sum1b); + + out[j] = vec_packus_16(pa, pb); + } } #else @@ -304,9 +399,22 @@ class FeatureTransformer { BiasType sum0 = accumulation[static_cast(perspectives[p])][j + 0]; BiasType sum1 = accumulation[static_cast(perspectives[p])][j + HalfDimensions / 2]; - sum0 = std::clamp(sum0, 0, 127); - sum1 = std::clamp(sum1, 0, 127); - output[offset + j] = static_cast(unsigned(sum0 * sum1) / 128); + + if (UseThreats) + { + BiasType sum0t = threatAccumulation[static_cast(perspectives[p])][j + 0]; + BiasType sum1t = + threatAccumulation[static_cast(perspectives[p])][j + HalfDimensions / 2]; + sum0 = std::clamp(sum0 + sum0t, 0, 255); + sum1 = std::clamp(sum1 + sum1t, 0, 255); + } + else + { + sum0 = std::clamp(sum0, 0, 127 * 2); + sum1 = std::clamp(sum1, 0, 127 * 2); + } + + output[offset + j] = static_cast(unsigned(sum0 * sum1) / 512); } #endif @@ -315,456 +423,27 @@ class FeatureTransformer { return psqt; } // end of function transform() - void hint_common_access(const Position& pos, bool psqtOnly) const { - hint_common_access_for_perspective(pos, psqtOnly); - hint_common_access_for_perspective(pos, psqtOnly); - } - - private: - template - [[nodiscard]] std::pair - try_find_computed_accumulator(const Position& pos, bool psqtOnly) const { - // Look for a usable accumulator of an earlier position. We keep track - // of the estimated gain in terms of features to be added/subtracted. - StateInfo *st = pos.state(), *next = nullptr; - int gain = FeatureSet::refresh_cost(pos); - while (st->previous - && (!(st->*accPtr).computedPSQT[Perspective] - || (!psqtOnly && !(st->*accPtr).computed[Perspective]))) - { - // This governs when a full feature refresh is needed and how many - // updates are better than just one full refresh. - if (FeatureSet::requires_refresh(st, Perspective) - || (gain -= FeatureSet::update_cost(st) + 1) < 0) - break; - next = st; - st = st->previous; - } - return {st, next}; - } - - // NOTE: The parameter states_to_update is an array of position states, ending with nullptr. - // All states must be sequential, that is states_to_update[i] must either be reachable - // by repeatedly applying ->previous from states_to_update[i+1] or - // states_to_update[i] == nullptr. - // computed_st must be reachable by repeatedly applying ->previous on - // states_to_update[0], if not nullptr. - template - void update_accumulator_incremental(const Position& pos, - StateInfo* computed_st, - StateInfo* states_to_update[N], - bool psqtOnly) const { - static_assert(N > 0); - assert(states_to_update[N - 1] == nullptr); - -#ifdef VECTOR - // Gcc-10.2 unnecessarily spills AVX2 registers if this array - // is defined in the VECTOR code below, once in each branch - vec_t acc[NumRegs]; - psqt_vec_t psqt[NumPsqtRegs]; -#endif - - if (states_to_update[0] == nullptr) - return; - - // Update incrementally going back through states_to_update. - - // Gather all features to be updated. - const Square ksq = pos.square(Perspective); - - // The size must be enough to contain the largest possible update. - // That might depend on the feature set and generally relies on the - // feature set's update cost calculation to be correct and never allow - // updates with more added/removed features than MaxActiveDimensions. - FeatureSet::IndexList removed[N - 1], added[N - 1]; - - { - int i = - N - - 2; // Last potential state to update. Skip last element because it must be nullptr. - while (states_to_update[i] == nullptr) - --i; - - StateInfo* st2 = states_to_update[i]; - - for (; i >= 0; --i) - { - (states_to_update[i]->*accPtr).computed[Perspective] = !psqtOnly; - (states_to_update[i]->*accPtr).computedPSQT[Perspective] = true; - - const StateInfo* end_state = i == 0 ? computed_st : states_to_update[i - 1]; - - for (; st2 != end_state; st2 = st2->previous) - FeatureSet::append_changed_indices(ksq, st2->dirtyPiece, - removed[i], added[i]); - } - } - - StateInfo* st = computed_st; - - // Now update the accumulators listed in states_to_update[], where the last element is a sentinel. -#ifdef VECTOR - - if (states_to_update[1] == nullptr && (removed[0].size() == 1 || removed[0].size() == 2) - && added[0].size() == 1) - { - assert(states_to_update[0]); - - if (!psqtOnly) - { - auto accIn = - reinterpret_cast(&(st->*accPtr).accumulation[Perspective][0]); - auto accOut = reinterpret_cast( - &(states_to_update[0]->*accPtr).accumulation[Perspective][0]); - - const IndexType offsetR0 = HalfDimensions * removed[0][0]; - auto columnR0 = reinterpret_cast(&weights[offsetR0]); - const IndexType offsetA = HalfDimensions * added[0][0]; - auto columnA = reinterpret_cast(&weights[offsetA]); - - if (removed[0].size() == 1) - { - for (IndexType k = 0; k < HalfDimensions * sizeof(std::int16_t) / sizeof(vec_t); - ++k) - accOut[k] = vec_add_16(vec_sub_16(accIn[k], columnR0[k]), columnA[k]); - } - else - { - const IndexType offsetR1 = HalfDimensions * removed[0][1]; - auto columnR1 = reinterpret_cast(&weights[offsetR1]); - - for (IndexType k = 0; k < HalfDimensions * sizeof(std::int16_t) / sizeof(vec_t); - ++k) - accOut[k] = vec_sub_16(vec_add_16(accIn[k], columnA[k]), - vec_add_16(columnR0[k], columnR1[k])); - } - } - - auto accPsqtIn = - reinterpret_cast(&(st->*accPtr).psqtAccumulation[Perspective][0]); - auto accPsqtOut = reinterpret_cast( - &(states_to_update[0]->*accPtr).psqtAccumulation[Perspective][0]); - - const IndexType offsetPsqtR0 = PSQTBuckets * removed[0][0]; - auto columnPsqtR0 = reinterpret_cast(&psqtWeights[offsetPsqtR0]); - const IndexType offsetPsqtA = PSQTBuckets * added[0][0]; - auto columnPsqtA = reinterpret_cast(&psqtWeights[offsetPsqtA]); - - if (removed[0].size() == 1) - { - for (std::size_t k = 0; k < PSQTBuckets * sizeof(std::int32_t) / sizeof(psqt_vec_t); - ++k) - accPsqtOut[k] = vec_add_psqt_32(vec_sub_psqt_32(accPsqtIn[k], columnPsqtR0[k]), - columnPsqtA[k]); - } - else - { - const IndexType offsetPsqtR1 = PSQTBuckets * removed[0][1]; - auto columnPsqtR1 = reinterpret_cast(&psqtWeights[offsetPsqtR1]); - - for (std::size_t k = 0; k < PSQTBuckets * sizeof(std::int32_t) / sizeof(psqt_vec_t); - ++k) - accPsqtOut[k] = - vec_sub_psqt_32(vec_add_psqt_32(accPsqtIn[k], columnPsqtA[k]), - vec_add_psqt_32(columnPsqtR0[k], columnPsqtR1[k])); - } - } - else - { - if (!psqtOnly) - for (IndexType j = 0; j < HalfDimensions / TileHeight; ++j) - { - // Load accumulator - auto accTileIn = reinterpret_cast( - &(st->*accPtr).accumulation[Perspective][j * TileHeight]); - for (IndexType k = 0; k < NumRegs; ++k) - acc[k] = vec_load(&accTileIn[k]); - - for (IndexType i = 0; states_to_update[i]; ++i) - { - // Difference calculation for the deactivated features - for (const auto index : removed[i]) - { - const IndexType offset = HalfDimensions * index + j * TileHeight; - auto column = reinterpret_cast(&weights[offset]); - for (IndexType k = 0; k < NumRegs; ++k) - acc[k] = vec_sub_16(acc[k], column[k]); - } - - // Difference calculation for the activated features - for (const auto index : added[i]) - { - const IndexType offset = HalfDimensions * index + j * TileHeight; - auto column = reinterpret_cast(&weights[offset]); - for (IndexType k = 0; k < NumRegs; ++k) - acc[k] = vec_add_16(acc[k], column[k]); - } - - // Store accumulator - auto accTileOut = - reinterpret_cast(&(states_to_update[i]->*accPtr) - .accumulation[Perspective][j * TileHeight]); - for (IndexType k = 0; k < NumRegs; ++k) - vec_store(&accTileOut[k], acc[k]); - } - } - - for (IndexType j = 0; j < PSQTBuckets / PsqtTileHeight; ++j) - { - // Load accumulator - auto accTilePsqtIn = reinterpret_cast( - &(st->*accPtr).psqtAccumulation[Perspective][j * PsqtTileHeight]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) - psqt[k] = vec_load_psqt(&accTilePsqtIn[k]); - - for (IndexType i = 0; states_to_update[i]; ++i) - { - // Difference calculation for the deactivated features - for (const auto index : removed[i]) - { - const IndexType offset = PSQTBuckets * index + j * PsqtTileHeight; - auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) - psqt[k] = vec_sub_psqt_32(psqt[k], columnPsqt[k]); - } - - // Difference calculation for the activated features - for (const auto index : added[i]) - { - const IndexType offset = PSQTBuckets * index + j * PsqtTileHeight; - auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) - psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); - } - - // Store accumulator - auto accTilePsqtOut = reinterpret_cast( - &(states_to_update[i]->*accPtr) - .psqtAccumulation[Perspective][j * PsqtTileHeight]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) - vec_store_psqt(&accTilePsqtOut[k], psqt[k]); - } - } - } -#else - for (IndexType i = 0; states_to_update[i]; ++i) - { - if (!psqtOnly) - std::memcpy((states_to_update[i]->*accPtr).accumulation[Perspective], - (st->*accPtr).accumulation[Perspective], - HalfDimensions * sizeof(BiasType)); - - for (std::size_t k = 0; k < PSQTBuckets; ++k) - (states_to_update[i]->*accPtr).psqtAccumulation[Perspective][k] = - (st->*accPtr).psqtAccumulation[Perspective][k]; - - st = states_to_update[i]; - - // Difference calculation for the deactivated features - for (const auto index : removed[i]) - { - if (!psqtOnly) - { - const IndexType offset = HalfDimensions * index; - for (IndexType j = 0; j < HalfDimensions; ++j) - (st->*accPtr).accumulation[Perspective][j] -= weights[offset + j]; - } - - for (std::size_t k = 0; k < PSQTBuckets; ++k) - (st->*accPtr).psqtAccumulation[Perspective][k] -= - psqtWeights[index * PSQTBuckets + k]; - } - - // Difference calculation for the activated features - for (const auto index : added[i]) - { - if (!psqtOnly) - { - const IndexType offset = HalfDimensions * index; - for (IndexType j = 0; j < HalfDimensions; ++j) - (st->*accPtr).accumulation[Perspective][j] += weights[offset + j]; - } - - for (std::size_t k = 0; k < PSQTBuckets; ++k) - (st->*accPtr).psqtAccumulation[Perspective][k] += - psqtWeights[index * PSQTBuckets + k]; - } - } -#endif - } - - template - void update_accumulator_refresh(const Position& pos, bool psqtOnly) const { -#ifdef VECTOR - // Gcc-10.2 unnecessarily spills AVX2 registers if this array - // is defined in the VECTOR code below, once in each branch - vec_t acc[NumRegs]; - psqt_vec_t psqt[NumPsqtRegs]; -#endif - - // Refresh the accumulator - // Could be extracted to a separate function because it's done in 2 places, - // but it's unclear if compilers would correctly handle register allocation. - auto& accumulator = pos.state()->*accPtr; - accumulator.computed[Perspective] = !psqtOnly; - accumulator.computedPSQT[Perspective] = true; - FeatureSet::IndexList active; - FeatureSet::append_active_indices(pos, active); - -#ifdef VECTOR - if (!psqtOnly) - for (IndexType j = 0; j < HalfDimensions / TileHeight; ++j) - { - auto biasesTile = reinterpret_cast(&biases[j * TileHeight]); - for (IndexType k = 0; k < NumRegs; ++k) - acc[k] = biasesTile[k]; - - int i = 0; - for (; i < int(active.size()) - 1; i += 2) - { - IndexType index0 = active[i]; - IndexType index1 = active[i + 1]; - const IndexType offset0 = HalfDimensions * index0 + j * TileHeight; - const IndexType offset1 = HalfDimensions * index1 + j * TileHeight; - auto column0 = reinterpret_cast(&weights[offset0]); - auto column1 = reinterpret_cast(&weights[offset1]); - - for (unsigned k = 0; k < NumRegs; ++k) - acc[k] = vec_add_16(acc[k], vec_add_16(column0[k], column1[k])); - } - for (; i < int(active.size()); ++i) - { - IndexType index = active[i]; - const IndexType offset = HalfDimensions * index + j * TileHeight; - auto column = reinterpret_cast(&weights[offset]); - - for (unsigned k = 0; k < NumRegs; ++k) - acc[k] = vec_add_16(acc[k], column[k]); - } - - auto accTile = - reinterpret_cast(&accumulator.accumulation[Perspective][j * TileHeight]); - for (unsigned k = 0; k < NumRegs; k++) - vec_store(&accTile[k], acc[k]); - } - - for (IndexType j = 0; j < PSQTBuckets / PsqtTileHeight; ++j) - { - for (std::size_t k = 0; k < NumPsqtRegs; ++k) - psqt[k] = vec_zero_psqt(); - - int i = 0; - for (; i < int(active.size()) - 1; i += 2) - { - IndexType index0 = active[i]; - IndexType index1 = active[i + 1]; - const IndexType offset0 = PSQTBuckets * index0 + j * PsqtTileHeight; - const IndexType offset1 = PSQTBuckets * index1 + j * PsqtTileHeight; - auto columnPsqt0 = reinterpret_cast(&psqtWeights[offset0]); - auto columnPsqt1 = reinterpret_cast(&psqtWeights[offset1]); - - for (std::size_t k = 0; k < NumPsqtRegs; ++k) - psqt[k] = - vec_add_psqt_32(psqt[k], vec_add_psqt_32(columnPsqt0[k], columnPsqt1[k])); - } - for (; i < int(active.size()); ++i) - { - IndexType index = active[i]; - const IndexType offset = PSQTBuckets * index + j * PsqtTileHeight; - auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); - - for (std::size_t k = 0; k < NumPsqtRegs; ++k) - psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); - } - - auto accTilePsqt = reinterpret_cast( - &accumulator.psqtAccumulation[Perspective][j * PsqtTileHeight]); - for (std::size_t k = 0; k < NumPsqtRegs; ++k) - vec_store_psqt(&accTilePsqt[k], psqt[k]); - } - -#else - if (!psqtOnly) - std::memcpy(accumulator.accumulation[Perspective], biases, - HalfDimensions * sizeof(BiasType)); - - for (std::size_t k = 0; k < PSQTBuckets; ++k) - accumulator.psqtAccumulation[Perspective][k] = 0; - - for (const auto index : active) - { - if (!psqtOnly) - { - const IndexType offset = HalfDimensions * index; - for (IndexType j = 0; j < HalfDimensions; ++j) - accumulator.accumulation[Perspective][j] += weights[offset + j]; - } - - for (std::size_t k = 0; k < PSQTBuckets; ++k) - accumulator.psqtAccumulation[Perspective][k] += - psqtWeights[index * PSQTBuckets + k]; - } -#endif - } - - template - void hint_common_access_for_perspective(const Position& pos, bool psqtOnly) const { - - // Works like update_accumulator, but performs less work. - // Updates ONLY the accumulator for pos. - - // Look for a usable accumulator of an earlier position. We keep track - // of the estimated gain in terms of features to be added/subtracted. - // Fast early exit. - if ((pos.state()->*accPtr).computed[Perspective] - || (psqtOnly && (pos.state()->*accPtr).computedPSQT[Perspective])) - return; - - auto [oldest_st, _] = try_find_computed_accumulator(pos, psqtOnly); - - if ((oldest_st->*accPtr).computed[Perspective] - || (psqtOnly && (oldest_st->*accPtr).computedPSQT[Perspective])) - { - // Only update current position accumulator to minimize work. - StateInfo* states_to_update[2] = {pos.state(), nullptr}; - update_accumulator_incremental(pos, oldest_st, states_to_update, - psqtOnly); - } - else - update_accumulator_refresh(pos, psqtOnly); - } - - template - void update_accumulator(const Position& pos, bool psqtOnly) const { - - auto [oldest_st, next] = try_find_computed_accumulator(pos, psqtOnly); - - if ((oldest_st->*accPtr).computed[Perspective] - || (psqtOnly && (oldest_st->*accPtr).computedPSQT[Perspective])) - { - if (next == nullptr) - return; - - // Now update the accumulators listed in states_to_update[], where the last element is a sentinel. - // Currently we update 2 accumulators. - // 1. for the current position - // 2. the next accumulator after the computed one - // The heuristic may change in the future. - StateInfo* states_to_update[3] = {next, next == pos.state() ? nullptr : pos.state(), - nullptr}; - - update_accumulator_incremental(pos, oldest_st, states_to_update, - psqtOnly); - } - else - update_accumulator_refresh(pos, psqtOnly); - } - - alignas(CacheLineSize) BiasType biases[HalfDimensions]; - alignas(CacheLineSize) WeightType weights[HalfDimensions * InputDimensions]; - alignas(CacheLineSize) PSQTWeightType psqtWeights[InputDimensions * PSQTBuckets]; + alignas(CacheLineSize) std::array biases; + alignas(CacheLineSize) std::array weights; + alignas(CacheLineSize) + std::array threatWeights; + alignas(CacheLineSize) std::array psqtWeights; + alignas(CacheLineSize) + std::array threatPsqtWeights; }; } // namespace Stockfish::Eval::NNUE + +template +struct std::hash> { + std::size_t + operator()(const Stockfish::Eval::NNUE::FeatureTransformer& ft) + const noexcept { + return ft.get_content_hash(); + } +}; + #endif // #ifndef NNUE_FEATURE_TRANSFORMER_H_INCLUDED diff --git a/src/nnue/nnue_misc.cpp b/src/nnue/nnue_misc.cpp index 725d90d27..66a6764a3 100644 --- a/src/nnue/nnue_misc.cpp +++ b/src/nnue/nnue_misc.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -28,8 +28,8 @@ #include #include #include +#include -#include "../evaluate.h" #include "../position.h" #include "../types.h" #include "../uci.h" @@ -42,15 +42,6 @@ namespace Stockfish::Eval::NNUE { constexpr std::string_view PieceToChar(" PNBRQK pnbrqk"); -void hint_common_parent_position(const Position& pos, const Networks& networks) { - - int simpleEvalAbs = std::abs(simple_eval(pos, pos.side_to_move())); - if (simpleEvalAbs > Eval::SmallNetThreshold) - networks.small.hint_common_access(pos, simpleEvalAbs > Eval::PsqtOnlyThreshold); - else - networks.big.hint_common_access(pos, false); -} - namespace { // Converts a Value into (centi)pawns and writes it in a buffer. // The buffer must have capacity for at least 5 chars. @@ -58,7 +49,7 @@ void format_cp_compact(Value v, char* buffer, const Position& pos) { buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' '); - int cp = std::abs(UCI::to_cp(v, pos)); + int cp = std::abs(UCIEngine::to_cp(v, pos)); if (cp >= 10000) { buffer[1] = '0' + cp / 10000; @@ -92,7 +83,7 @@ void format_cp_compact(Value v, char* buffer, const Position& pos) { // Converts a Value into pawns, always keeping two decimals void format_cp_aligned_dot(Value v, std::stringstream& stream, const Position& pos) { - const double pawns = std::abs(0.01 * UCI::to_cp(v, pos)); + const double pawns = std::abs(0.01 * UCIEngine::to_cp(v, pos)); stream << (v < 0 ? '-' : v > 0 ? '+' @@ -104,7 +95,8 @@ void format_cp_aligned_dot(Value v, std::stringstream& stream, const Position& p // Returns a string with the value of each piece on a board, // and a table for (PSQT, Layers) values bucket by bucket. -std::string trace(Position& pos, const Eval::NNUE::Networks& networks) { +std::string +trace(Position& pos, const Eval::NNUE::Networks& networks, Eval::NNUE::AccumulatorCaches& caches) { std::stringstream ss; @@ -124,14 +116,17 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) { board[y][x] = board[y][x + 8] = board[y + 3][x + 8] = board[y + 3][x] = '+'; if (pc != NO_PIECE) board[y + 1][x + 4] = PieceToChar[pc]; - if (value != VALUE_NONE) + if (is_valid(value)) format_cp_compact(value, &board[y + 2][x + 2], pos); }; + auto accumulators = std::make_unique(); + // We estimate the value of each piece by doing a differential evaluation from // the current base eval, simulating the removal of the piece from its square. - Value base = networks.big.evaluate(pos); - base = pos.side_to_move() == WHITE ? base : -base; + auto [psqt, positional] = networks.big.evaluate(pos, *accumulators, caches.big); + Value base = psqt + positional; + base = pos.side_to_move() == WHITE ? base : -base; for (File f = FILE_A; f <= FILE_H; ++f) for (Rank r = RANK_1; r <= RANK_8; ++r) @@ -142,21 +137,15 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) { if (pc != NO_PIECE && type_of(pc) != KING) { - auto st = pos.state(); - pos.remove_piece(sq); - st->accumulatorBig.computed[WHITE] = st->accumulatorBig.computed[BLACK] = - st->accumulatorBig.computedPSQT[WHITE] = st->accumulatorBig.computedPSQT[BLACK] = - false; - Value eval = networks.big.evaluate(pos); - eval = pos.side_to_move() == WHITE ? eval : -eval; - v = base - eval; + accumulators->reset(); + std::tie(psqt, positional) = networks.big.evaluate(pos, *accumulators, caches.big); + Value eval = psqt + positional; + eval = pos.side_to_move() == WHITE ? eval : -eval; + v = base - eval; pos.put_piece(pc, sq); - st->accumulatorBig.computed[WHITE] = st->accumulatorBig.computed[BLACK] = - st->accumulatorBig.computedPSQT[WHITE] = st->accumulatorBig.computedPSQT[BLACK] = - false; } writeSquare(f, r, pc, v); @@ -167,7 +156,8 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) { ss << board[row] << '\n'; ss << '\n'; - auto t = networks.big.trace_evaluate(pos); + accumulators->reset(); + auto t = networks.big.trace_evaluate(pos, *accumulators, caches.big); ss << " NNUE network contributions " << (pos.side_to_move() == WHITE ? "(White to move)" : "(Black to move)") << std::endl @@ -178,16 +168,16 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) { for (std::size_t bucket = 0; bucket < LayerStacks; ++bucket) { - ss << "| " << bucket << " "; - ss << " | "; + ss << "| " << bucket << " " // + << " | "; format_cp_aligned_dot(t.psqt[bucket], ss, pos); - ss << " " + ss << " " // << " | "; format_cp_aligned_dot(t.positional[bucket], ss, pos); - ss << " " + ss << " " // << " | "; format_cp_aligned_dot(t.psqt[bucket] + t.positional[bucket], ss, pos); - ss << " " + ss << " " // << " |"; if (bucket == t.correctBucket) ss << " <-- this bucket is used"; diff --git a/src/nnue/nnue_misc.h b/src/nnue/nnue_misc.h index 5eab02184..ecece5589 100644 --- a/src/nnue/nnue_misc.h +++ b/src/nnue/nnue_misc.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -20,8 +20,10 @@ #define NNUE_MISC_H_INCLUDED #include +#include #include +#include "../misc.h" #include "../types.h" #include "nnue_architecture.h" @@ -31,17 +33,17 @@ class Position; namespace Eval::NNUE { +// EvalFile uses fixed string types because it's part of the network structure which must be trivial. struct EvalFile { // Default net name, will use one of the EvalFileDefaultName* macros defined // in evaluate.h - std::string defaultName; + FixedString<256> defaultName; // Selected net name, either via uci option or default - std::string current; + FixedString<256> current; // Net description extracted from the net file - std::string netDescription; + FixedString<256> netDescription; }; - struct NnueEvalTrace { static_assert(LayerStacks == PSQTBuckets); @@ -50,14 +52,23 @@ struct NnueEvalTrace { std::size_t correctBucket; }; - struct Networks; +struct AccumulatorCaches; - -std::string trace(Position& pos, const Networks& networks); -void hint_common_parent_position(const Position& pos, const Networks& networks); +std::string trace(Position& pos, const Networks& networks, AccumulatorCaches& caches); } // namespace Stockfish::Eval::NNUE } // namespace Stockfish +template<> +struct std::hash { + std::size_t operator()(const Stockfish::Eval::NNUE::EvalFile& evalFile) const noexcept { + std::size_t h = 0; + Stockfish::hash_combine(h, evalFile.defaultName); + Stockfish::hash_combine(h, evalFile.current); + Stockfish::hash_combine(h, evalFile.netDescription); + return h; + } +}; + #endif // #ifndef NNUE_MISC_H_INCLUDED diff --git a/src/nnue/simd.h b/src/nnue/simd.h new file mode 100644 index 000000000..25891163e --- /dev/null +++ b/src/nnue/simd.h @@ -0,0 +1,434 @@ +/* + 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 . +*/ + +#ifndef NNUE_SIMD_H_INCLUDED +#define NNUE_SIMD_H_INCLUDED + +#if defined(USE_AVX2) + #include + +#elif defined(USE_SSE41) + #include + +#elif defined(USE_SSSE3) + #include + +#elif defined(USE_SSE2) + #include + +#elif defined(USE_NEON) + #include +#endif + +#include "../types.h" +#include "nnue_common.h" + +namespace Stockfish::Eval::NNUE::SIMD { + +// If vector instructions are enabled, we update and refresh the +// accumulator tile by tile such that each tile fits in the CPU's +// vector registers. +#define VECTOR + +#ifdef USE_AVX512 +using vec_t = __m512i; +using vec_i8_t = __m256i; +using vec128_t = __m128i; +using psqt_vec_t = __m256i; +using vec_uint_t = __m512i; + #define vec_load(a) _mm512_load_si512(a) + #define vec_store(a, b) _mm512_store_si512(a, b) + #define vec_convert_8_16(a) _mm512_cvtepi8_epi16(a) + #define vec_add_16(a, b) _mm512_add_epi16(a, b) + #define vec_sub_16(a, b) _mm512_sub_epi16(a, b) + #define vec_mulhi_16(a, b) _mm512_mulhi_epi16(a, b) + #define vec_zero() _mm512_setzero_epi32() + #define vec_set_16(a) _mm512_set1_epi16(a) + #define vec_max_16(a, b) _mm512_max_epi16(a, b) + #define vec_min_16(a, b) _mm512_min_epi16(a, b) + #define vec_slli_16(a, b) _mm512_slli_epi16(a, b) + // Inverse permuted at load time + #define vec_packus_16(a, b) _mm512_packus_epi16(a, b) + #define vec_load_psqt(a) _mm256_load_si256(a) + #define vec_store_psqt(a, b) _mm256_store_si256(a, b) + #define vec_add_psqt_32(a, b) _mm256_add_epi32(a, b) + #define vec_sub_psqt_32(a, b) _mm256_sub_epi32(a, b) + #define vec_zero_psqt() _mm256_setzero_si256() + + #ifdef USE_SSSE3 + #define vec_nnz(a) _mm512_cmpgt_epi32_mask(a, _mm512_setzero_si512()) + #endif + + #define vec128_zero _mm_setzero_si128() + #define vec128_set_16(a) _mm_set1_epi16(a) + #define vec128_load(a) _mm_load_si128(a) + #define vec128_storeu(a, b) _mm_storeu_si128(a, b) + #define vec128_add(a, b) _mm_add_epi16(a, b) + #define NumRegistersSIMD 16 + #define MaxChunkSize 64 + +#elif USE_AVX2 +using vec_t = __m256i; +using vec_i8_t = __m128i; +using vec128_t = __m128i; +using psqt_vec_t = __m256i; +using vec_uint_t = __m256i; + #define vec_load(a) _mm256_load_si256(a) + #define vec_store(a, b) _mm256_store_si256(a, b) + #define vec_convert_8_16(a) _mm256_cvtepi8_epi16(a) + #define vec_add_16(a, b) _mm256_add_epi16(a, b) + #define vec_sub_16(a, b) _mm256_sub_epi16(a, b) + #define vec_mulhi_16(a, b) _mm256_mulhi_epi16(a, b) + #define vec_zero() _mm256_setzero_si256() + #define vec_set_16(a) _mm256_set1_epi16(a) + #define vec_max_16(a, b) _mm256_max_epi16(a, b) + #define vec_min_16(a, b) _mm256_min_epi16(a, b) + #define vec_slli_16(a, b) _mm256_slli_epi16(a, b) + // Inverse permuted at load time + #define vec_packus_16(a, b) _mm256_packus_epi16(a, b) + #define vec_load_psqt(a) _mm256_load_si256(a) + #define vec_store_psqt(a, b) _mm256_store_si256(a, b) + #define vec_add_psqt_32(a, b) _mm256_add_epi32(a, b) + #define vec_sub_psqt_32(a, b) _mm256_sub_epi32(a, b) + #define vec_zero_psqt() _mm256_setzero_si256() + + #ifdef USE_SSSE3 + #if defined(USE_VNNI) && !defined(USE_AVXVNNI) + #define vec_nnz(a) _mm256_cmpgt_epi32_mask(a, _mm256_setzero_si256()) + #else + #define vec_nnz(a) \ + _mm256_movemask_ps( \ + _mm256_castsi256_ps(_mm256_cmpgt_epi32(a, _mm256_setzero_si256()))) + #endif + #endif + + #define vec128_zero _mm_setzero_si128() + #define vec128_set_16(a) _mm_set1_epi16(a) + #define vec128_load(a) _mm_load_si128(a) + #define vec128_storeu(a, b) _mm_storeu_si128(a, b) + #define vec128_add(a, b) _mm_add_epi16(a, b) + + #define NumRegistersSIMD 12 + #define MaxChunkSize 32 + +#elif USE_SSE2 +using vec_t = __m128i; +using vec_i8_t = std::uint64_t; // for the correct size -- will be loaded into an xmm reg +using vec128_t = __m128i; +using psqt_vec_t = __m128i; +using vec_uint_t = __m128i; + #define vec_load(a) (*(a)) + #define vec_store(a, b) *(a) = (b) + #define vec_add_16(a, b) _mm_add_epi16(a, b) + #define vec_sub_16(a, b) _mm_sub_epi16(a, b) + #define vec_mulhi_16(a, b) _mm_mulhi_epi16(a, b) + #define vec_zero() _mm_setzero_si128() + #define vec_set_16(a) _mm_set1_epi16(a) + #define vec_max_16(a, b) _mm_max_epi16(a, b) + #define vec_min_16(a, b) _mm_min_epi16(a, b) + #define vec_slli_16(a, b) _mm_slli_epi16(a, b) + #define vec_packus_16(a, b) _mm_packus_epi16(a, b) + #define vec_load_psqt(a) (*(a)) + #define vec_store_psqt(a, b) *(a) = (b) + #define vec_add_psqt_32(a, b) _mm_add_epi32(a, b) + #define vec_sub_psqt_32(a, b) _mm_sub_epi32(a, b) + #define vec_zero_psqt() _mm_setzero_si128() + + #ifdef USE_SSSE3 + #define vec_nnz(a) \ + _mm_movemask_ps(_mm_castsi128_ps(_mm_cmpgt_epi32(a, _mm_setzero_si128()))) + #endif + + #ifdef __i386__ +inline __m128i _mm_cvtsi64_si128(int64_t val) { + return _mm_loadl_epi64(reinterpret_cast(&val)); +} + #endif + + #ifdef USE_SSE41 + #define vec_convert_8_16(a) _mm_cvtepi8_epi16(_mm_cvtsi64_si128(static_cast(a))) + #else +// Credit: Yoshie2000 +inline __m128i vec_convert_8_16(uint64_t x) { + __m128i v8 = _mm_cvtsi64_si128(static_cast(x)); + __m128i sign = _mm_cmpgt_epi8(_mm_setzero_si128(), v8); + return _mm_unpacklo_epi8(v8, sign); +} + #endif + + #define vec128_zero _mm_setzero_si128() + #define vec128_set_16(a) _mm_set1_epi16(a) + #define vec128_load(a) _mm_load_si128(a) + #define vec128_storeu(a, b) _mm_storeu_si128(a, b) + #define vec128_add(a, b) _mm_add_epi16(a, b) + + #define NumRegistersSIMD (Is64Bit ? 12 : 6) + #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; + #define vec_load(a) (*(a)) + #define vec_store(a, b) *(a) = (b) + #define vec_add_16(a, b) vaddq_s16(a, b) + #define vec_sub_16(a, b) vsubq_s16(a, b) + #define vec_mulhi_16(a, b) vqdmulhq_s16(a, b) + #define vec_zero() vec_t{0} + #define vec_set_16(a) vdupq_n_s16(a) + #define vec_max_16(a, b) vmaxq_s16(a, b) + #define vec_min_16(a, b) vminq_s16(a, b) + #define vec_slli_16(a, b) vshlq_s16(a, vec_set_16(b)) + #define vec_packus_16(a, b) reinterpret_cast(vcombine_u8(vqmovun_s16(a), vqmovun_s16(b))) + #define vec_load_psqt(a) (*(a)) + #define vec_store_psqt(a, b) *(a) = (b) + #define vec_add_psqt_32(a, b) vaddq_s32(a, b) + #define vec_sub_psqt_32(a, b) vsubq_s32(a, b) + #define vec_zero_psqt() psqt_vec_t{0} + +static constexpr std::uint32_t Mask[4] = {1, 2, 4, 8}; + #define vec_nnz(a) vaddvq_u32(vandq_u32(vtstq_u32(a, a), vld1q_u32(Mask))) + #define vec128_zero vdupq_n_u16(0) + #define vec128_set_16(a) vdupq_n_u16(a) + #define vec128_load(a) vld1q_u16(reinterpret_cast(a)) + #define vec128_storeu(a, b) vst1q_u16(reinterpret_cast(a), b) + #define vec128_add(a, b) vaddq_u16(a, b) + + #define NumRegistersSIMD 16 + #define MaxChunkSize 16 + + #ifndef __aarch64__ +// Single instruction doesn't exist on 32-bit ARM +inline int8x16_t vmovl_high_s8(int8x16_t val) { return vmovl_s8(vget_high_s8(val)); } + #endif + +#else + #undef VECTOR + +#endif + +struct Vec16Wrapper { +#ifdef VECTOR + using type = vec_t; + static type add(const type& lhs, const type& rhs) { return vec_add_16(lhs, rhs); } + static type sub(const type& lhs, const type& rhs) { return vec_sub_16(lhs, rhs); } +#else + using type = BiasType; + static type add(const type& lhs, const type& rhs) { return lhs + rhs; } + static type sub(const type& lhs, const type& rhs) { return lhs - rhs; } +#endif +}; + +struct Vec32Wrapper { +#ifdef VECTOR + using type = psqt_vec_t; + static type add(const type& lhs, const type& rhs) { return vec_add_psqt_32(lhs, rhs); } + static type sub(const type& lhs, const type& rhs) { return vec_sub_psqt_32(lhs, rhs); } +#else + using type = PSQTWeightType; + static type add(const type& lhs, const type& rhs) { return lhs + rhs; } + static type sub(const type& lhs, const type& rhs) { return lhs - rhs; } +#endif +}; + +enum UpdateOperation { + Add, + Sub +}; + +template = true> +typename VecWrapper::type fused(const typename VecWrapper::type& in) { + return in; +} + +template, bool> = true, + std::enable_if_t = true> +typename VecWrapper::type +fused(const typename VecWrapper::type& in, const T& operand, const Ts&... operands) { + switch (update_op) + { + case Add : + return fused(VecWrapper::add(in, operand), operands...); + case Sub : + return fused(VecWrapper::sub(in, operand), operands...); + default : + static_assert(update_op == Add || update_op == Sub, + "Only Add and Sub are currently supported."); + return typename VecWrapper::type(); + } +} + +#if defined(USE_AVX512) + +[[maybe_unused]] static int m512_hadd(__m512i sum, int bias) { + return _mm512_reduce_add_epi32(sum) + bias; +} + +[[maybe_unused]] static void m512_add_dpbusd_epi32(__m512i& acc, __m512i a, __m512i b) { + + #if defined(USE_VNNI) + acc = _mm512_dpbusd_epi32(acc, a, b); + #else + __m512i product0 = _mm512_maddubs_epi16(a, b); + product0 = _mm512_madd_epi16(product0, _mm512_set1_epi16(1)); + acc = _mm512_add_epi32(acc, product0); + #endif +} + +#endif + +#if defined(USE_AVX2) + +[[maybe_unused]] static int m256_hadd(__m256i sum, int bias) { + __m128i sum128 = _mm_add_epi32(_mm256_castsi256_si128(sum), _mm256_extracti128_si256(sum, 1)); + sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_BADC)); + sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_CDAB)); + return _mm_cvtsi128_si32(sum128) + bias; +} + +[[maybe_unused]] static void m256_add_dpbusd_epi32(__m256i& acc, __m256i a, __m256i b) { + + #if defined(USE_VNNI) + acc = _mm256_dpbusd_epi32(acc, a, b); + #else + __m256i product0 = _mm256_maddubs_epi16(a, b); + product0 = _mm256_madd_epi16(product0, _mm256_set1_epi16(1)); + acc = _mm256_add_epi32(acc, product0); + #endif +} + +#endif + +#if defined(USE_SSSE3) + +[[maybe_unused]] static int m128_hadd(__m128i sum, int bias) { + sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0x4E)); //_MM_PERM_BADC + sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0xB1)); //_MM_PERM_CDAB + return _mm_cvtsi128_si32(sum) + bias; +} + +[[maybe_unused]] static void m128_add_dpbusd_epi32(__m128i& acc, __m128i a, __m128i b) { + + __m128i product0 = _mm_maddubs_epi16(a, b); + product0 = _mm_madd_epi16(product0, _mm_set1_epi16(1)); + acc = _mm_add_epi32(acc, product0); +} + +#endif + +#if defined(USE_NEON_DOTPROD) + +[[maybe_unused]] static void +dotprod_m128_add_dpbusd_epi32(int32x4_t& acc, int8x16_t a, int8x16_t b) { + + acc = vdotq_s32(acc, a, b); +} +#endif + +#if defined(USE_NEON) + +[[maybe_unused]] static int neon_m128_reduce_add_epi32(int32x4_t s) { + #if USE_NEON >= 8 + return vaddvq_s32(s); + #else + return s[0] + s[1] + s[2] + s[3]; + #endif +} + +[[maybe_unused]] static int neon_m128_hadd(int32x4_t sum, int bias) { + return neon_m128_reduce_add_epi32(sum) + bias; +} + +#endif + +#if USE_NEON >= 8 +[[maybe_unused]] static void neon_m128_add_dpbusd_epi32(int32x4_t& acc, int8x16_t a, int8x16_t b) { + + int16x8_t product0 = vmull_s8(vget_low_s8(a), vget_low_s8(b)); + int16x8_t product1 = vmull_high_s8(a, b); + int16x8_t sum = vpaddq_s16(product0, product1); + acc = vpadalq_s16(acc, sum); +} +#endif + + +// Compute optimal SIMD register count for feature transformer accumulation. +template +class SIMDTiling { +#ifdef VECTOR + // We use __m* types as template arguments, which causes GCC to emit warnings + // about losing some attribute information. This is irrelevant to us as we + // only take their size, so the following pragma are harmless. + #if defined(__GNUC__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wignored-attributes" + #endif + + template + static constexpr int BestRegisterCount() { + constexpr std::size_t RegisterSize = sizeof(SIMDRegisterType); + constexpr std::size_t LaneSize = sizeof(LaneType); + + static_assert(RegisterSize >= LaneSize); + static_assert(MaxRegisters <= NumRegistersSIMD); + static_assert(MaxRegisters > 0); + static_assert(NumRegistersSIMD > 0); + static_assert(RegisterSize % LaneSize == 0); + static_assert((NumLanes * LaneSize) % RegisterSize == 0); + + const int ideal = (NumLanes * LaneSize) / RegisterSize; + if (ideal <= MaxRegisters) + return ideal; + + // Look for the largest divisor of the ideal register count that is smaller than MaxRegisters + for (int divisor = MaxRegisters; divisor > 1; --divisor) + if (ideal % divisor == 0) + return divisor; + + return 1; + } + + #if defined(__GNUC__) + #pragma GCC diagnostic pop + #endif + + public: + static constexpr int NumRegs = + BestRegisterCount(); + static constexpr int NumPsqtRegs = + BestRegisterCount(); + + static constexpr IndexType TileHeight = NumRegs * sizeof(vec_t) / 2; + static constexpr IndexType PsqtTileHeight = NumPsqtRegs * sizeof(psqt_vec_t) / 4; + + static_assert(HalfDimensions % TileHeight == 0, "TileHeight must divide HalfDimensions"); + static_assert(PSQTBuckets % PsqtTileHeight == 0, "PsqtTileHeight must divide PSQTBuckets"); +#endif +}; +} + +#endif diff --git a/src/numa.h b/src/numa.h new file mode 100644 index 000000000..99169c211 --- /dev/null +++ b/src/numa.h @@ -0,0 +1,1482 @@ +/* + 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 . +*/ + +#ifndef NUMA_H_INCLUDED +#define NUMA_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shm.h" + +// We support linux very well, but we explicitly do NOT support Android, +// because there is no affected systems, not worth maintaining. +#if defined(__linux__) && !defined(__ANDROID__) + #if !defined(_GNU_SOURCE) + #define _GNU_SOURCE + #endif + #include +#elif defined(_WIN64) + + #if _WIN32_WINNT < 0x0601 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0601 // Force to include needed API prototypes + #endif + +// On Windows each processor group can have up to 64 processors. +// https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups +static constexpr size_t WIN_PROCESSOR_GROUP_SIZE = 64; + + #if !defined(NOMINMAX) + #define NOMINMAX + #endif + #include + #if defined small + #undef small + #endif + +// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadselectedcpusetmasks +using SetThreadSelectedCpuSetMasks_t = BOOL (*)(HANDLE, PGROUP_AFFINITY, USHORT); + +// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadselectedcpusetmasks +using GetThreadSelectedCpuSetMasks_t = BOOL (*)(HANDLE, PGROUP_AFFINITY, USHORT, PUSHORT); + +#endif + +#include "misc.h" + +namespace Stockfish { + +using CpuIndex = size_t; +using NumaIndex = size_t; + +inline CpuIndex get_hardware_concurrency() { + CpuIndex concurrency = std::thread::hardware_concurrency(); + + // Get all processors across all processor groups on windows, since + // hardware_concurrency() only returns the number of processors in + // the first group, because only these are available to std::thread. +#ifdef _WIN64 + concurrency = std::max(concurrency, GetActiveProcessorCount(ALL_PROCESSOR_GROUPS)); +#endif + + return concurrency; +} + +inline const CpuIndex SYSTEM_THREADS_NB = std::max(1, get_hardware_concurrency()); + +#if defined(_WIN64) + +struct WindowsAffinity { + std::optional> oldApi; + std::optional> newApi; + + // We also provide diagnostic for when the affinity is set to nullopt + // whether it was due to being indeterminate. If affinity is indeterminate + // it is best to assume it is not set at all, so consistent with the meaning + // of the nullopt affinity. + bool isNewDeterminate = true; + bool isOldDeterminate = true; + + std::optional> get_combined() const { + if (!oldApi.has_value()) + return newApi; + if (!newApi.has_value()) + return oldApi; + + std::set intersect; + std::set_intersection(oldApi->begin(), oldApi->end(), newApi->begin(), newApi->end(), + std::inserter(intersect, intersect.begin())); + return intersect; + } + + // Since Windows 11 and Windows Server 2022 thread affinities can span + // processor groups and can be set as such by a new WinAPI function. However, + // we may need to force using the old API if we detect that the process has + // affinity set by the old API already and we want to override that. Due to the + // limitations of the old API we cannot detect its use reliably. There will be + // cases where we detect not use but it has actually been used and vice versa. + + bool likely_used_old_api() const { return oldApi.has_value() || !isOldDeterminate; } +}; + +inline std::pair> get_process_group_affinity() { + + // GetProcessGroupAffinity requires the GroupArray argument to be + // aligned to 4 bytes instead of just 2. + static constexpr size_t GroupArrayMinimumAlignment = 4; + static_assert(GroupArrayMinimumAlignment >= alignof(USHORT)); + + // The function should succeed the second time, but it may fail if the group + // affinity has changed between GetProcessGroupAffinity calls. In such case + // we consider this a hard error, as we Cannot work with unstable affinities + // anyway. + static constexpr int MAX_TRIES = 2; + USHORT GroupCount = 1; + for (int i = 0; i < MAX_TRIES; ++i) + { + auto GroupArray = std::make_unique( + GroupCount + (GroupArrayMinimumAlignment / alignof(USHORT) - 1)); + + USHORT* GroupArrayAligned = align_ptr_up(GroupArray.get()); + + const BOOL status = + GetProcessGroupAffinity(GetCurrentProcess(), &GroupCount, GroupArrayAligned); + + if (status == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) + { + break; + } + + if (status != 0) + { + return std::make_pair(status, + std::vector(GroupArrayAligned, GroupArrayAligned + GroupCount)); + } + } + + return std::make_pair(0, std::vector()); +} + +// On Windows there are two ways to set affinity, and therefore 2 ways to get it. +// These are not consistent, so we have to check both. In some cases it is actually +// not possible to determine affinity. For example when two different threads have +// affinity on different processor groups, set using SetThreadAffinityMask, we cannot +// retrieve the actual affinities. +// From documentation on GetProcessAffinityMask: +// > If the calling process contains threads in multiple groups, +// > the function returns zero for both affinity masks. +// In such cases we just give up and assume we have affinity for all processors. +// nullopt means no affinity is set, that is, all processors are allowed +inline WindowsAffinity get_process_affinity() { + HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); + auto GetThreadSelectedCpuSetMasks_f = GetThreadSelectedCpuSetMasks_t( + (void (*)()) GetProcAddress(k32, "GetThreadSelectedCpuSetMasks")); + + BOOL status = 0; + + WindowsAffinity affinity; + + if (GetThreadSelectedCpuSetMasks_f != nullptr) + { + USHORT RequiredMaskCount; + status = GetThreadSelectedCpuSetMasks_f(GetCurrentThread(), nullptr, 0, &RequiredMaskCount); + + // We expect ERROR_INSUFFICIENT_BUFFER from GetThreadSelectedCpuSetMasks, + // but other failure is an actual error. + if (status == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) + { + affinity.isNewDeterminate = false; + } + else if (RequiredMaskCount > 0) + { + // If RequiredMaskCount then these affinities were never set, but it's + // not consistent so GetProcessAffinityMask may still return some affinity. + auto groupAffinities = std::make_unique(RequiredMaskCount); + + status = GetThreadSelectedCpuSetMasks_f(GetCurrentThread(), groupAffinities.get(), + RequiredMaskCount, &RequiredMaskCount); + + if (status == 0) + { + affinity.isNewDeterminate = false; + } + else + { + std::set cpus; + + for (USHORT i = 0; i < RequiredMaskCount; ++i) + { + const size_t procGroupIndex = groupAffinities[i].Group; + + for (size_t j = 0; j < WIN_PROCESSOR_GROUP_SIZE; ++j) + { + if (groupAffinities[i].Mask & (KAFFINITY(1) << j)) + cpus.insert(procGroupIndex * WIN_PROCESSOR_GROUP_SIZE + j); + } + } + + affinity.newApi = std::move(cpus); + } + } + } + + // NOTE: There is no way to determine full affinity using the old API if + // individual threads set affinity on different processor groups. + + DWORD_PTR proc, sys; + status = GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys); + + // If proc == 0 then we cannot determine affinity because it spans processor groups. + // On Windows 11 and Server 2022 it will instead + // > If, however, hHandle specifies a handle to the current process, the function + // > always uses the calling thread's primary group (which by default is the same + // > as the process' primary group) in order to set the + // > lpProcessAffinityMask and lpSystemAffinityMask. + // So it will never be indeterminate here. We can only make assumptions later. + if (status == 0 || proc == 0) + { + affinity.isOldDeterminate = false; + return affinity; + } + + // If SetProcessAffinityMask was never called the affinity must span + // all processor groups, but if it was called it must only span one. + + std::vector groupAffinity; // We need to capture this later and capturing + // from structured bindings requires c++20. + + std::tie(status, groupAffinity) = get_process_group_affinity(); + if (status == 0) + { + affinity.isOldDeterminate = false; + return affinity; + } + + if (groupAffinity.size() == 1) + { + // We detect the case when affinity is set to all processors and correctly + // leave affinity.oldApi as nullopt. + if (GetActiveProcessorGroupCount() != 1 || proc != sys) + { + std::set cpus; + + const size_t procGroupIndex = groupAffinity[0]; + + const uint64_t mask = static_cast(proc); + for (size_t j = 0; j < WIN_PROCESSOR_GROUP_SIZE; ++j) + { + if (mask & (KAFFINITY(1) << j)) + cpus.insert(procGroupIndex * WIN_PROCESSOR_GROUP_SIZE + j); + } + + affinity.oldApi = std::move(cpus); + } + } + else + { + // If we got here it means that either SetProcessAffinityMask was never set + // or we're on Windows 11/Server 2022. + + // Since Windows 11 and Windows Server 2022 the behaviour of + // GetProcessAffinityMask changed: + // > If, however, hHandle specifies a handle to the current process, + // > the function always uses the calling thread's primary group + // > (which by default is the same as the process' primary group) + // > in order to set the lpProcessAffinityMask and lpSystemAffinityMask. + // In which case we can actually retrieve the full affinity. + + if (GetThreadSelectedCpuSetMasks_f != nullptr) + { + std::thread th([&]() { + std::set cpus; + bool isAffinityFull = true; + + for (auto procGroupIndex : groupAffinity) + { + const int numActiveProcessors = + GetActiveProcessorCount(static_cast(procGroupIndex)); + + // We have to schedule to two different processors + // and & the affinities we get. Otherwise our processor + // choice could influence the resulting affinity. + // We assume the processor IDs within the group are + // filled sequentially from 0. + uint64_t procCombined = std::numeric_limits::max(); + uint64_t sysCombined = std::numeric_limits::max(); + + for (int i = 0; i < std::min(numActiveProcessors, 2); ++i) + { + GROUP_AFFINITY GroupAffinity; + std::memset(&GroupAffinity, 0, sizeof(GROUP_AFFINITY)); + GroupAffinity.Group = static_cast(procGroupIndex); + + GroupAffinity.Mask = static_cast(1) << i; + + status = + SetThreadGroupAffinity(GetCurrentThread(), &GroupAffinity, nullptr); + if (status == 0) + { + affinity.isOldDeterminate = false; + return; + } + + SwitchToThread(); + + DWORD_PTR proc2, sys2; + status = GetProcessAffinityMask(GetCurrentProcess(), &proc2, &sys2); + if (status == 0) + { + affinity.isOldDeterminate = false; + return; + } + + procCombined &= static_cast(proc2); + sysCombined &= static_cast(sys2); + } + + if (procCombined != sysCombined) + isAffinityFull = false; + + for (size_t j = 0; j < WIN_PROCESSOR_GROUP_SIZE; ++j) + { + if (procCombined & (KAFFINITY(1) << j)) + cpus.insert(procGroupIndex * WIN_PROCESSOR_GROUP_SIZE + j); + } + } + + // We have to detect the case where the affinity was not set, + // or is set to all processors so that we correctly produce as + // std::nullopt result. + if (!isAffinityFull) + { + affinity.oldApi = std::move(cpus); + } + }); + + th.join(); + } + } + + return affinity; +} + +#endif + +#if defined(__linux__) && !defined(__ANDROID__) + +inline std::set get_process_affinity() { + + std::set cpus; + + // For unsupported systems, or in case of a soft error, we may assume + // all processors are available for use. + [[maybe_unused]] auto set_to_all_cpus = [&]() { + for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c) + cpus.insert(c); + }; + + // cpu_set_t by default holds 1024 entries. This may not be enough soon, + // but there is no easy way to determine how many threads there actually + // is. In this case we just choose a reasonable upper bound. + static constexpr CpuIndex MaxNumCpus = 1024 * 64; + + cpu_set_t* mask = CPU_ALLOC(MaxNumCpus); + if (mask == nullptr) + std::exit(EXIT_FAILURE); + + const size_t masksize = CPU_ALLOC_SIZE(MaxNumCpus); + + CPU_ZERO_S(masksize, mask); + + const int status = sched_getaffinity(0, masksize, mask); + + if (status != 0) + { + CPU_FREE(mask); + std::exit(EXIT_FAILURE); + } + + for (CpuIndex c = 0; c < MaxNumCpus; ++c) + if (CPU_ISSET_S(c, masksize, mask)) + cpus.insert(c); + + CPU_FREE(mask); + + return cpus; +} + +#endif + +#if defined(__linux__) && !defined(__ANDROID__) + +inline static const auto STARTUP_PROCESSOR_AFFINITY = get_process_affinity(); + +#elif defined(_WIN64) + +inline static const auto STARTUP_PROCESSOR_AFFINITY = get_process_affinity(); +inline static const auto STARTUP_USE_OLD_AFFINITY_API = + STARTUP_PROCESSOR_AFFINITY.likely_used_old_api(); + +#endif + +// We want to abstract the purpose of storing the numa node index somewhat. +// Whoever is using this does not need to know the specifics of the replication +// machinery to be able to access NUMA replicated memory. +class NumaReplicatedAccessToken { + public: + NumaReplicatedAccessToken() : + n(0) {} + + explicit NumaReplicatedAccessToken(NumaIndex idx) : + n(idx) {} + + NumaIndex get_numa_index() const { return n; } + + private: + NumaIndex n; +}; + +// 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. +// +// We use startup affinities so as not to modify its own behaviour in time. +// +// Since Stockfish doesn't support exceptions all places where an exception +// should be thrown are replaced by std::exit. +class NumaConfig { + public: + NumaConfig() : + highestCpuIndex(0), + customAffinity(false) { + const auto numCpus = SYSTEM_THREADS_NB; + 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) { + NumaConfig cfg = empty(); + +#if defined(__linux__) && !defined(__ANDROID__) + + std::set 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) + + std::optional> allowedCpus; + + if (respectProcessAffinity) + allowedCpus = STARTUP_PROCESSOR_AFFINITY.get_combined(); + + // The affinity cannot be determined in all cases on Windows, + // but we at least guarantee that the number of allowed processors + // is >= number of processors in the affinity mask. In case the user + // is not satisfied they must set the processor numbers explicitly. + auto is_cpu_allowed = [&allowedCpus](CpuIndex c) { + 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; + + const BOOL status = GetNumaProcessorNodeEx(&procnum, &nodeNumber); + const CpuIndex c = static_cast(procGroup) * WIN_PROCESSOR_GROUP_SIZE + + static_cast(number); + if (status != 0 && nodeNumber != std::numeric_limits::max() + && is_cpu_allowed(c)) + { + cfg.add_cpu_to_node(nodeNumber, c); + } + } + } + + // 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 + // still no way to set thread affinity spanning multiple processor groups. + // See https://learn.microsoft.com/en-us/windows/win32/procthread/numa-support + // We also do this is if need to force old API for some reason. + // + // 2024-08-26: It appears that we need to actually always force this behaviour. + // While Windows allows this to work now, such assignments have bad interaction + // with the scheduler - in particular it still prefers scheduling on the thread's + // "primary" node, even if it means scheduling SMT processors first. + // See https://github.com/official-stockfish/Stockfish/issues/5551 + // See https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups + // + // Each process is assigned a primary group at creation, and by default all + // of its threads' primary group is the same. Each thread's ideal processor + // is in the thread's primary group, so threads will preferentially be + // scheduled to processors on their primary group, but they are able to + // be scheduled to processors on any other group. + // + // used to be guarded by if (STARTUP_USE_OLD_AFFINITY_API) + { + NumaConfig splitCfg = empty(); + + NumaIndex splitNodeIndex = 0; + for (const auto& cpus : cfg.nodes) + { + if (cpus.empty()) + continue; + + size_t lastProcGroupIndex = *(cpus.begin()) / WIN_PROCESSOR_GROUP_SIZE; + for (CpuIndex c : cpus) + { + const size_t procGroupIndex = c / WIN_PROCESSOR_GROUP_SIZE; + if (procGroupIndex != lastProcGroupIndex) + { + splitNodeIndex += 1; + lastProcGroupIndex = procGroupIndex; + } + splitCfg.add_cpu_to_node(splitNodeIndex, c); + } + splitNodeIndex += 1; + } + + 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 + + // We have to ensure no empty NUMA nodes persist. + cfg.remove_empty_numa_nodes(); + + // If the user explicitly opts out from respecting the current process affinity + // then it may be inconsistent with the current affinity (obviously), so we + // consider it custom. + if (!respectProcessAffinity) + cfg.customAffinity = true; + + return cfg; + } + + // ':'-separated numa nodes + // ','-separated cpu indices + // supports "first-last" range syntax for cpu indices + // For example "0-15,128-143:16-31,144-159:32-47,160-175:48-63,176-191" + static NumaConfig from_string(const std::string& s) { + NumaConfig cfg = empty(); + + NumaIndex n = 0; + for (auto&& nodeStr : split(s, ":")) + { + auto indices = indices_from_shortened_string(std::string(nodeStr)); + if (!indices.empty()) + { + for (auto idx : indices) + { + if (!cfg.add_cpu_to_node(n, CpuIndex(idx))) + std::exit(EXIT_FAILURE); + } + + n += 1; + } + } + + cfg.customAffinity = true; + + return cfg; + } + + NumaConfig(const NumaConfig&) = delete; + NumaConfig(NumaConfig&&) = default; + NumaConfig& operator=(const NumaConfig&) = delete; + NumaConfig& operator=(NumaConfig&&) = default; + + bool is_cpu_assigned(CpuIndex n) const { return nodeByCpu.count(n) == 1; } + + NumaIndex num_numa_nodes() const { return nodes.size(); } + + CpuIndex num_cpus_in_numa_node(NumaIndex n) const { + assert(n < nodes.size()); + return nodes[n].size(); + } + + CpuIndex num_cpus() const { return nodeByCpu.size(); } + + bool requires_memory_replication() const { return customAffinity || nodes.size() > 1; } + + std::string to_string() const { + std::string str; + + bool isFirstNode = true; + for (auto&& cpus : nodes) + { + if (!isFirstNode) + str += ":"; + + bool isFirstSet = true; + auto rangeStart = cpus.begin(); + for (auto it = cpus.begin(); it != cpus.end(); ++it) + { + auto next = std::next(it); + if (next == cpus.end() || *next != *it + 1) + { + // cpus[i] is at the end of the range (may be of size 1) + if (!isFirstSet) + str += ","; + + const CpuIndex last = *it; + + if (it != rangeStart) + { + const CpuIndex first = *rangeStart; + + str += std::to_string(first); + str += "-"; + str += std::to_string(last); + } + else + str += std::to_string(last); + + rangeStart = next; + isFirstSet = false; + } + } + + isFirstNode = false; + } + + return str; + } + + bool suggests_binding_threads(CpuIndex numThreads) const { + // If we can reasonably determine that the threads cannot be contained + // by the OS within the first NUMA node then we advise distributing + // and binding threads. When the threads are not bound we can only use + // NUMA memory replicated objects from the first node, so when the OS + // has to schedule on other nodes we lose performance. We also suggest + // binding if there's enough threads to distribute among nodes with minimal + // disparity. We try to ignore small nodes, in particular the empty ones. + + // If the affinity set by the user does not match the affinity given by + // the OS then binding is necessary to ensure the threads are running on + // correct processors. + if (customAffinity) + return true; + + // We obviously cannot distribute a single thread, so a single thread + // should never be bound. + if (numThreads <= 1) + return false; + + size_t largestNodeSize = 0; + for (auto&& cpus : nodes) + if (cpus.size() > largestNodeSize) + largestNodeSize = cpus.size(); + + auto is_node_small = [largestNodeSize](const std::set& node) { + static constexpr double SmallNodeThreshold = 0.6; + return static_cast(node.size()) / static_cast(largestNodeSize) + <= SmallNodeThreshold; + }; + + size_t numNotSmallNodes = 0; + for (auto&& cpus : nodes) + if (!is_node_small(cpus)) + numNotSmallNodes += 1; + + return (numThreads > largestNodeSize / 2 || numThreads >= numNotSmallNodes * 4) + && nodes.size() > 1; + } + + std::vector distribute_threads_among_numa_nodes(CpuIndex numThreads) const { + std::vector ns; + + if (nodes.size() == 1) + { + // Special case for when there's no NUMA nodes. This doesn't buy us + // much, but let's keep the default path simple. + ns.resize(numThreads, NumaIndex{0}); + } + else + { + std::vector occupation(nodes.size(), 0); + for (CpuIndex c = 0; c < numThreads; ++c) + { + NumaIndex bestNode{0}; + float bestNodeFill = std::numeric_limits::max(); + for (NumaIndex n = 0; n < nodes.size(); ++n) + { + float fill = + static_cast(occupation[n] + 1) / static_cast(nodes[n].size()); + // NOTE: Do we want to perhaps fill the first available node + // up to 50% first before considering other nodes? + // Probably not, because it would interfere with running + // multiple instances. We basically shouldn't favor any + // particular node. + if (fill < bestNodeFill) + { + bestNode = n; + bestNodeFill = fill; + } + } + ns.emplace_back(bestNode); + occupation[bestNode] += 1; + } + } + + return ns; + } + + NumaReplicatedAccessToken bind_current_thread_to_numa_node(NumaIndex n) const { + if (n >= nodes.size() || nodes[n].size() == 0) + std::exit(EXIT_FAILURE); + +#if defined(__linux__) && !defined(__ANDROID__) + + cpu_set_t* mask = CPU_ALLOC(highestCpuIndex + 1); + if (mask == nullptr) + std::exit(EXIT_FAILURE); + + const size_t masksize = CPU_ALLOC_SIZE(highestCpuIndex + 1); + + CPU_ZERO_S(masksize, mask); + + for (CpuIndex c : nodes[n]) + CPU_SET_S(c, masksize, mask); + + const int status = sched_setaffinity(0, masksize, mask); + + CPU_FREE(mask); + + if (status != 0) + std::exit(EXIT_FAILURE); + + // We yield this thread just to be sure it gets rescheduled. + // This is defensive, allowed because this code is not performance critical. + sched_yield(); + +#elif defined(_WIN64) + + // Requires Windows 11. No good way to set thread affinity spanning + // processor groups before that. + HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); + auto SetThreadSelectedCpuSetMasks_f = SetThreadSelectedCpuSetMasks_t( + (void (*)()) GetProcAddress(k32, "SetThreadSelectedCpuSetMasks")); + + // We ALWAYS set affinity with the new API if available, because + // there's no downsides, and we forcibly keep it consistent with + // the old API should we need to use it. I.e. we always keep this + // as a superset of what we set with SetThreadGroupAffinity. + if (SetThreadSelectedCpuSetMasks_f != nullptr) + { + // Only available on Windows 11 and Windows Server 2022 onwards + const USHORT numProcGroups = USHORT( + ((highestCpuIndex + 1) + WIN_PROCESSOR_GROUP_SIZE - 1) / WIN_PROCESSOR_GROUP_SIZE); + auto groupAffinities = std::make_unique(numProcGroups); + std::memset(groupAffinities.get(), 0, sizeof(GROUP_AFFINITY) * numProcGroups); + for (WORD i = 0; i < numProcGroups; ++i) + groupAffinities[i].Group = i; + + for (CpuIndex c : nodes[n]) + { + const size_t procGroupIndex = c / WIN_PROCESSOR_GROUP_SIZE; + const size_t idxWithinProcGroup = c % WIN_PROCESSOR_GROUP_SIZE; + groupAffinities[procGroupIndex].Mask |= KAFFINITY(1) << idxWithinProcGroup; + } + + HANDLE hThread = GetCurrentThread(); + + const BOOL status = + SetThreadSelectedCpuSetMasks_f(hThread, groupAffinities.get(), numProcGroups); + if (status == 0) + std::exit(EXIT_FAILURE); + + // We yield this thread just to be sure it gets rescheduled. + // This is defensive, allowed because this code is not performance critical. + SwitchToThread(); + } + + // Sometimes we need to force the old API, but do not use it unless necessary. + if (SetThreadSelectedCpuSetMasks_f == nullptr || STARTUP_USE_OLD_AFFINITY_API) + { + // On earlier windows version (since windows 7) we cannot run a single thread + // on multiple processor groups, so we need to restrict the group. + // We assume the group of the first processor listed for this node. + // Processors from outside this group will not be assigned for this thread. + // Normally this won't be an issue because windows used to assign NUMA nodes + // such that they cannot span processor groups. However, since Windows 10 + // Build 20348 the behaviour changed, so there's a small window of versions + // between this and Windows 11 that might exhibit problems with not all + // processors being utilized. + // + // We handle this in NumaConfig::from_system by manually splitting the + // nodes when we detect that there is no function to set affinity spanning + // processor nodes. This is required because otherwise our thread distribution + // code may produce suboptimal results. + // + // See https://learn.microsoft.com/en-us/windows/win32/procthread/numa-support + GROUP_AFFINITY affinity; + std::memset(&affinity, 0, sizeof(GROUP_AFFINITY)); + // We use an ordered set to be sure to get the smallest cpu number here. + const size_t forcedProcGroupIndex = *(nodes[n].begin()) / WIN_PROCESSOR_GROUP_SIZE; + affinity.Group = static_cast(forcedProcGroupIndex); + for (CpuIndex c : nodes[n]) + { + const size_t procGroupIndex = c / WIN_PROCESSOR_GROUP_SIZE; + const size_t idxWithinProcGroup = c % WIN_PROCESSOR_GROUP_SIZE; + // We skip processors that are not in the same processor group. + // If everything was set up correctly this will never be an issue, + // but we have to account for bad NUMA node specification. + if (procGroupIndex != forcedProcGroupIndex) + continue; + + affinity.Mask |= KAFFINITY(1) << idxWithinProcGroup; + } + + HANDLE hThread = GetCurrentThread(); + + const BOOL status = SetThreadGroupAffinity(hThread, &affinity, nullptr); + if (status == 0) + std::exit(EXIT_FAILURE); + + // We yield this thread just to be sure it gets rescheduled. This is + // defensive, allowed because this code is not performance critical. + SwitchToThread(); + } + +#endif + + return NumaReplicatedAccessToken(n); + } + + template + void execute_on_numa_node(NumaIndex n, FuncT&& f) const { + std::thread th([this, &f, n]() { + bind_current_thread_to_numa_node(n); + std::forward(f)(); + }); + + th.join(); + } + + std::vector> nodes; + std::map nodeByCpu; + + private: + CpuIndex highestCpuIndex; + + bool customAffinity; + + static NumaConfig empty() { return NumaConfig(EmptyNodeTag{}); } + + struct EmptyNodeTag {}; + + NumaConfig(EmptyNodeTag) : + highestCpuIndex(0), + customAffinity(false) {} + + void remove_empty_numa_nodes() { + std::vector> newNodes; + for (auto&& cpus : nodes) + if (!cpus.empty()) + newNodes.emplace_back(std::move(cpus)); + nodes = std::move(newNodes); + } + + // Returns true if successful + // Returns false if failed, i.e. when the cpu is already present + // strong guarantee, the structure remains unmodified + bool add_cpu_to_node(NumaIndex n, CpuIndex c) { + if (is_cpu_assigned(c)) + return false; + + while (nodes.size() <= n) + nodes.emplace_back(); + + nodes[n].insert(c); + nodeByCpu[c] = n; + + if (c > highestCpuIndex) + highestCpuIndex = c; + + return true; + } + + // Returns true if successful + // Returns false if failed, i.e. when any of the cpus is already present + // strong guarantee, the structure remains unmodified + bool add_cpu_range_to_node(NumaIndex n, CpuIndex cfirst, CpuIndex clast) { + for (CpuIndex c = cfirst; c <= clast; ++c) + if (is_cpu_assigned(c)) + return false; + + while (nodes.size() <= n) + nodes.emplace_back(); + + for (CpuIndex c = cfirst; c <= clast; ++c) + { + nodes[n].insert(c); + nodeByCpu[c] = n; + } + + if (clast > highestCpuIndex) + highestCpuIndex = clast; + + return true; + } + + static std::vector indices_from_shortened_string(const std::string& s) { + std::vector indices; + + if (s.empty()) + return indices; + + for (const auto& ss : split(s, ",")) + { + if (ss.empty()) + continue; + + auto parts = split(ss, "-"); + if (parts.size() == 1) + { + const CpuIndex c = CpuIndex{str_to_size_t(std::string(parts[0]))}; + indices.emplace_back(c); + } + else if (parts.size() == 2) + { + const CpuIndex cfirst = CpuIndex{str_to_size_t(std::string(parts[0]))}; + const CpuIndex clast = CpuIndex{str_to_size_t(std::string(parts[1]))}; + for (size_t c = cfirst; c <= clast; ++c) + { + indices.emplace_back(c); + } + } + } + + return indices; + } +}; + +class NumaReplicationContext; + +// Instances of this class are tracked by the NumaReplicationContext instance. +// NumaReplicationContext informs all tracked instances when NUMA configuration changes. +class NumaReplicatedBase { + public: + NumaReplicatedBase(NumaReplicationContext& ctx); + + NumaReplicatedBase(const NumaReplicatedBase&) = delete; + NumaReplicatedBase(NumaReplicatedBase&& other) noexcept; + + NumaReplicatedBase& operator=(const NumaReplicatedBase&) = delete; + NumaReplicatedBase& operator=(NumaReplicatedBase&& other) noexcept; + + virtual void on_numa_config_changed() = 0; + virtual ~NumaReplicatedBase(); + + const NumaConfig& get_numa_config() const; + + private: + NumaReplicationContext* context; +}; + +// We force boxing with a unique_ptr. If this becomes an issue due to added +// indirection we may need to add an option for a custom boxing type. When the +// NUMA config changes the value stored at the index 0 is replicated to other nodes. +template +class NumaReplicated: public NumaReplicatedBase { + public: + using ReplicatorFuncType = std::function; + + NumaReplicated(NumaReplicationContext& ctx) : + NumaReplicatedBase(ctx) { + replicate_from(T{}); + } + + NumaReplicated(NumaReplicationContext& ctx, T&& source) : + NumaReplicatedBase(ctx) { + replicate_from(std::move(source)); + } + + NumaReplicated(const NumaReplicated&) = delete; + NumaReplicated(NumaReplicated&& other) noexcept : + NumaReplicatedBase(std::move(other)), + instances(std::exchange(other.instances, {})) {} + + NumaReplicated& operator=(const NumaReplicated&) = delete; + NumaReplicated& operator=(NumaReplicated&& other) noexcept { + NumaReplicatedBase::operator=(*this, std::move(other)); + instances = std::exchange(other.instances, {}); + + return *this; + } + + NumaReplicated& operator=(T&& source) { + replicate_from(std::move(source)); + + return *this; + } + + ~NumaReplicated() override = default; + + const T& operator[](NumaReplicatedAccessToken token) const { + assert(token.get_numa_index() < instances.size()); + return *(instances[token.get_numa_index()]); + } + + const T& operator*() const { return *(instances[0]); } + + const T* operator->() const { return instances[0].get(); } + + template + void modify_and_replicate(FuncT&& f) { + auto source = std::move(instances[0]); + std::forward(f)(*source); + replicate_from(std::move(*source)); + } + + void on_numa_config_changed() override { + // Use the first one as the source. It doesn't matter which one we use, + // because they all must be identical, but the first one is guaranteed to exist. + auto source = std::move(instances[0]); + replicate_from(std::move(*source)); + } + + private: + std::vector> instances; + + void replicate_from(T&& source) { + instances.clear(); + + const NumaConfig& cfg = get_numa_config(); + if (cfg.requires_memory_replication()) + { + for (NumaIndex n = 0; n < cfg.num_numa_nodes(); ++n) + { + cfg.execute_on_numa_node( + n, [this, &source]() { instances.emplace_back(std::make_unique(source)); }); + } + } + else + { + assert(cfg.num_numa_nodes() == 1); + // We take advantage of the fact that replication is not required + // and reuse the source value, avoiding one copy operation. + instances.emplace_back(std::make_unique(std::move(source))); + } + } +}; + +// We force boxing with a unique_ptr. If this becomes an issue due to added +// indirection we may need to add an option for a custom boxing type. +template +class LazyNumaReplicated: public NumaReplicatedBase { + public: + using ReplicatorFuncType = std::function; + + LazyNumaReplicated(NumaReplicationContext& ctx) : + NumaReplicatedBase(ctx) { + prepare_replicate_from(T{}); + } + + LazyNumaReplicated(NumaReplicationContext& ctx, T&& source) : + NumaReplicatedBase(ctx) { + prepare_replicate_from(std::move(source)); + } + + LazyNumaReplicated(const LazyNumaReplicated&) = delete; + LazyNumaReplicated(LazyNumaReplicated&& other) noexcept : + NumaReplicatedBase(std::move(other)), + instances(std::exchange(other.instances, {})) {} + + LazyNumaReplicated& operator=(const LazyNumaReplicated&) = delete; + LazyNumaReplicated& operator=(LazyNumaReplicated&& other) noexcept { + NumaReplicatedBase::operator=(*this, std::move(other)); + instances = std::exchange(other.instances, {}); + + return *this; + } + + LazyNumaReplicated& operator=(T&& source) { + prepare_replicate_from(std::move(source)); + + return *this; + } + + ~LazyNumaReplicated() override = default; + + const T& operator[](NumaReplicatedAccessToken token) const { + assert(token.get_numa_index() < instances.size()); + ensure_present(token.get_numa_index()); + return *(instances[token.get_numa_index()]); + } + + const T& operator*() const { return *(instances[0]); } + + const T* operator->() const { return instances[0].get(); } + + template + void modify_and_replicate(FuncT&& f) { + auto source = std::move(instances[0]); + std::forward(f)(*source); + prepare_replicate_from(std::move(*source)); + } + + void on_numa_config_changed() override { + // Use the first one as the source. It doesn't matter which one we use, + // because they all must be identical, but the first one is guaranteed to exist. + auto source = std::move(instances[0]); + prepare_replicate_from(std::move(*source)); + } + + private: + mutable std::vector> instances; + mutable std::mutex mutex; + + void ensure_present(NumaIndex idx) const { + assert(idx < instances.size()); + + if (instances[idx] != nullptr) + return; + + assert(idx != 0); + + std::unique_lock lock(mutex); + // Check again for races. + if (instances[idx] != nullptr) + return; + + const NumaConfig& cfg = get_numa_config(); + cfg.execute_on_numa_node( + idx, [this, idx]() { instances[idx] = std::make_unique(*instances[0]); }); + } + + void prepare_replicate_from(T&& source) { + instances.clear(); + + const NumaConfig& cfg = get_numa_config(); + if (cfg.requires_memory_replication()) + { + assert(cfg.num_numa_nodes() > 0); + + // We just need to make sure the first instance is there. + // Note that we cannot move here as we need to reallocate the data + // on the correct NUMA node. + cfg.execute_on_numa_node( + 0, [this, &source]() { instances.emplace_back(std::make_unique(source)); }); + + // Prepare others for lazy init. + instances.resize(cfg.num_numa_nodes()); + } + else + { + assert(cfg.num_numa_nodes() == 1); + // We take advantage of the fact that replication is not required + // and reuse the source value, avoiding one copy operation. + instances.emplace_back(std::make_unique(std::move(source))); + } + } +}; + +// Utilizes shared memory. +template +class LazyNumaReplicatedSystemWide: public NumaReplicatedBase { + public: + using ReplicatorFuncType = std::function; + + LazyNumaReplicatedSystemWide(NumaReplicationContext& ctx) : + NumaReplicatedBase(ctx) { + prepare_replicate_from(std::make_unique()); + } + + LazyNumaReplicatedSystemWide(NumaReplicationContext& ctx, std::unique_ptr&& source) : + NumaReplicatedBase(ctx) { + prepare_replicate_from(std::move(source)); + } + + LazyNumaReplicatedSystemWide(const LazyNumaReplicatedSystemWide&) = delete; + LazyNumaReplicatedSystemWide(LazyNumaReplicatedSystemWide&& other) noexcept : + NumaReplicatedBase(std::move(other)), + instances(std::exchange(other.instances, {})) {} + + LazyNumaReplicatedSystemWide& operator=(const LazyNumaReplicatedSystemWide&) = delete; + LazyNumaReplicatedSystemWide& operator=(LazyNumaReplicatedSystemWide&& other) noexcept { + NumaReplicatedBase::operator=(*this, std::move(other)); + instances = std::exchange(other.instances, {}); + + return *this; + } + + LazyNumaReplicatedSystemWide& operator=(std::unique_ptr&& source) { + prepare_replicate_from(std::move(source)); + + return *this; + } + + ~LazyNumaReplicatedSystemWide() override = default; + + const T& operator[](NumaReplicatedAccessToken token) const { + assert(token.get_numa_index() < instances.size()); + ensure_present(token.get_numa_index()); + return *(instances[token.get_numa_index()]); + } + + const T& operator*() const { return *(instances[0]); } + + const T* operator->() const { return &*instances[0]; } + + std::vector>> + get_status_and_errors() const { + std::vector>> + status; + status.reserve(instances.size()); + + for (const auto& instance : instances) + { + status.emplace_back(instance.get_status(), instance.get_error_message()); + } + + return status; + } + + template + void modify_and_replicate(FuncT&& f) { + auto source = std::make_unique(*instances[0]); + std::forward(f)(*source); + prepare_replicate_from(std::move(source)); + } + + void on_numa_config_changed() override { + // Use the first one as the source. It doesn't matter which one we use, + // because they all must be identical, but the first one is guaranteed to exist. + auto source = std::make_unique(*instances[0]); + prepare_replicate_from(std::move(source)); + } + + private: + mutable std::vector> instances; + mutable std::mutex mutex; + + std::size_t get_discriminator(NumaIndex idx) const { + const NumaConfig& cfg = get_numa_config(); + const NumaConfig& cfg_sys = NumaConfig::from_system(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{}(s); + } + + void ensure_present(NumaIndex idx) const { + assert(idx < instances.size()); + + if (instances[idx] != nullptr) + return; + + assert(idx != 0); + + std::unique_lock lock(mutex); + // Check again for races. + if (instances[idx] != nullptr) + return; + + const NumaConfig& cfg = get_numa_config(); + cfg.execute_on_numa_node(idx, [this, idx]() { + instances[idx] = SystemWideSharedConstant(*instances[0], get_discriminator(idx)); + }); + } + + void prepare_replicate_from(std::unique_ptr&& source) { + instances.clear(); + + const NumaConfig& cfg = get_numa_config(); + // We just need to make sure the first instance is there. + // Note that we cannot move here as we need to reallocate the data + // on the correct NUMA node. + // Even in the case of a single NUMA node we have to copy since it's shared memory. + if (cfg.requires_memory_replication()) + { + assert(cfg.num_numa_nodes() > 0); + + cfg.execute_on_numa_node(0, [this, &source]() { + instances.emplace_back(SystemWideSharedConstant(*source, get_discriminator(0))); + }); + + // Prepare others for lazy init. + instances.resize(cfg.num_numa_nodes()); + } + else + { + assert(cfg.num_numa_nodes() == 1); + instances.emplace_back(SystemWideSharedConstant(*source, get_discriminator(0))); + } + } +}; + +class NumaReplicationContext { + public: + NumaReplicationContext(NumaConfig&& cfg) : + config(std::move(cfg)) {} + + NumaReplicationContext(const NumaReplicationContext&) = delete; + NumaReplicationContext(NumaReplicationContext&&) = delete; + + NumaReplicationContext& operator=(const NumaReplicationContext&) = delete; + NumaReplicationContext& operator=(NumaReplicationContext&&) = delete; + + ~NumaReplicationContext() { + // The context must outlive replicated objects + if (!trackedReplicatedObjects.empty()) + std::exit(EXIT_FAILURE); + } + + void attach(NumaReplicatedBase* obj) { + assert(trackedReplicatedObjects.count(obj) == 0); + trackedReplicatedObjects.insert(obj); + } + + void detach(NumaReplicatedBase* obj) { + assert(trackedReplicatedObjects.count(obj) == 1); + trackedReplicatedObjects.erase(obj); + } + + // oldObj may be invalid at this point + void move_attached([[maybe_unused]] NumaReplicatedBase* oldObj, NumaReplicatedBase* newObj) { + assert(trackedReplicatedObjects.count(oldObj) == 1); + assert(trackedReplicatedObjects.count(newObj) == 0); + trackedReplicatedObjects.erase(oldObj); + trackedReplicatedObjects.insert(newObj); + } + + void set_numa_config(NumaConfig&& cfg) { + config = std::move(cfg); + for (auto&& obj : trackedReplicatedObjects) + obj->on_numa_config_changed(); + } + + const NumaConfig& get_numa_config() const { return config; } + + private: + NumaConfig config; + + // std::set uses std::less by default, which is required for pointer comparison + std::set trackedReplicatedObjects; +}; + +inline NumaReplicatedBase::NumaReplicatedBase(NumaReplicationContext& ctx) : + context(&ctx) { + context->attach(this); +} + +inline NumaReplicatedBase::NumaReplicatedBase(NumaReplicatedBase&& other) noexcept : + context(std::exchange(other.context, nullptr)) { + context->move_attached(&other, this); +} + +inline NumaReplicatedBase& NumaReplicatedBase::operator=(NumaReplicatedBase&& other) noexcept { + context = std::exchange(other.context, nullptr); + + context->move_attached(&other, this); + + return *this; +} + +inline NumaReplicatedBase::~NumaReplicatedBase() { + if (context != nullptr) + context->detach(this); +} + +inline const NumaConfig& NumaReplicatedBase::get_numa_config() const { + return context->get_numa_config(); +} + +} // namespace Stockfish + + +#endif // #ifndef NUMA_H_INCLUDED diff --git a/src/perft.h b/src/perft.h index a53179f88..4cb409cf4 100644 --- a/src/perft.h +++ b/src/perft.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -27,7 +27,7 @@ #include "types.h" #include "uci.h" -namespace Stockfish { +namespace Stockfish::Benchmark { // Utility to verify move generation. All the leaf nodes up // to the given depth are generated and counted, and the sum is returned. @@ -35,7 +35,6 @@ template uint64_t perft(Position& pos, Depth depth) { StateInfo st; - ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); uint64_t cnt, nodes = 0; const bool leaf = (depth == 2); @@ -51,20 +50,18 @@ uint64_t perft(Position& pos, Depth depth) { nodes += cnt; pos.undo_move(m); } - if (Root && Cluster::is_root()) - sync_cout << UCI::move(m, pos.is_chess960()) << ": " << cnt << sync_endl; + if (Root && Distributed::is_root()) + sync_cout << UCIEngine::move(m, pos.is_chess960()) << ": " << cnt << sync_endl; } return nodes; } -inline void perft(const std::string& fen, Depth depth, bool isChess960) { - StateListPtr states(new std::deque(1)); - Position p; - p.set(fen, isChess960, &states->back()); +inline uint64_t perft(const std::string& fen, Depth depth, bool isChess960) { + StateInfo st; + Position p; + p.set(fen, isChess960, &st); - uint64_t nodes = perft(p, depth); - if (Cluster::is_root()) - sync_cout << "\nNodes searched: " << nodes << "\n" << sync_endl; + return perft(p, depth); } } diff --git a/src/position.cpp b/src/position.cpp index 2263afe76..d8b02e8ab 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -32,9 +32,9 @@ #include #include "bitboard.h" +#include "history.h" #include "misc.h" #include "movegen.h" -#include "nnue/nnue_common.h" #include "syzygy/tbprobe.h" #include "tt.h" #include "uci.h" @@ -49,14 +49,15 @@ Key psq[PIECE_NB][SQUARE_NB]; Key enpassant[FILE_NB]; Key castling[CASTLING_RIGHT_NB]; Key side, noPawns; + } namespace { constexpr std::string_view PieceToChar(" PNBRQK pnbrqk"); -constexpr Piece Pieces[] = {W_PAWN, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING, - B_PAWN, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING}; +static constexpr Piece Pieces[] = {W_PAWN, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING, + B_PAWN, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING}; } // namespace @@ -78,12 +79,11 @@ std::ostream& operator<<(std::ostream& os, const Position& pos) { << std::setw(16) << pos.key() << std::setfill(' ') << std::dec << "\nCheckers: "; for (Bitboard b = pos.checkers(); b;) - os << UCI::square(pop_lsb(b)) << " "; + os << UCIEngine::square(pop_lsb(b)) << " "; - if (int(Tablebases::MaxCardinality) >= popcount(pos.pieces()) && !pos.can_castle(ANY_CASTLING)) + if (Tablebases::MaxCardinality >= popcount(pos.pieces()) && !pos.can_castle(ANY_CASTLING)) { StateInfo st; - ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); Position p; p.set(pos.fen(), pos.is_chess960(), &st); @@ -119,6 +119,9 @@ void Position::init() { for (Piece pc : Pieces) for (Square s = SQ_A1; s <= SQ_H8; ++s) Zobrist::psq[pc][s] = rng.rand(); + // pawns on these squares will promote + std::fill_n(Zobrist::psq[W_PAWN] + SQ_A8, 8, 0); + std::fill_n(Zobrist::psq[B_PAWN], 8, 0); for (File f = FILE_A; f <= FILE_H; ++f) Zobrist::enpassant[f] = rng.rand(); @@ -199,7 +202,7 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si) { Square sq = SQ_A8; std::istringstream ss(fenStr); - std::memset(this, 0, sizeof(Position)); + std::memset(reinterpret_cast(this), 0, sizeof(Position)); std::memset(si, 0, sizeof(StateInfo)); st = si; @@ -269,7 +272,7 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si) { // a) side to move have a pawn threatening epSquare // b) there is an enemy pawn in front of epSquare // c) there is no piece on epSquare or behind epSquare - enpassant = pawn_attacks_bb(~sideToMove, st->epSquare) & pieces(sideToMove, PAWN) + enpassant = attacks_bb(st->epSquare, ~sideToMove) & pieces(sideToMove, PAWN) && (pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove))) && !(pieces() & (st->epSquare | (st->epSquare + pawn_push(sideToMove)))); } @@ -320,7 +323,7 @@ void Position::set_check_info() const { Square ksq = square(~sideToMove); - st->checkSquares[PAWN] = pawn_attacks_bb(~sideToMove, ksq); + st->checkSquares[PAWN] = attacks_bb(ksq, ~sideToMove); st->checkSquares[KNIGHT] = attacks_bb(ksq); st->checkSquares[BISHOP] = attacks_bb(ksq, pieces()); st->checkSquares[ROOK] = attacks_bb(ksq, pieces()); @@ -334,8 +337,10 @@ void Position::set_check_info() const { // The function is only used when a new position is set up void Position::set_state() const { - st->key = st->materialKey = 0; - st->pawnKey = Zobrist::noPawns; + st->key = 0; + st->minorPieceKey = 0; + st->nonPawnKey[WHITE] = st->nonPawnKey[BLACK] = 0; + st->pawnKey = Zobrist::noPawns; st->nonPawnMaterial[WHITE] = st->nonPawnMaterial[BLACK] = VALUE_ZERO; st->checkersBB = attackers_to(square(sideToMove)) & pieces(~sideToMove); @@ -350,8 +355,18 @@ void Position::set_state() const { if (type_of(pc) == PAWN) st->pawnKey ^= Zobrist::psq[pc][s]; - else if (type_of(pc) != KING) - st->nonPawnMaterial[color_of(pc)] += PieceValue[pc]; + else + { + st->nonPawnKey[color_of(pc)] ^= Zobrist::psq[pc][s]; + + if (type_of(pc) != KING) + { + st->nonPawnMaterial[color_of(pc)] += PieceValue[pc]; + + if (type_of(pc) <= BISHOP) + st->minorPieceKey ^= Zobrist::psq[pc][s]; + } + } } if (st->epSquare != SQ_NONE) @@ -361,10 +376,15 @@ void Position::set_state() const { st->key ^= Zobrist::side; st->key ^= Zobrist::castling[st->castlingRights]; + st->materialKey = compute_material_key(); +} +Key Position::compute_material_key() const { + Key k = 0; for (Piece pc : Pieces) for (int cnt = 0; cnt < pieceCount[pc]; ++cnt) - st->materialKey ^= Zobrist::psq[pc][cnt]; + k ^= Zobrist::psq[pc][8 + cnt]; + return k; } @@ -431,8 +451,8 @@ string Position::fen() const { if (!can_castle(ANY_CASTLING)) ss << '-'; - ss << (ep_square() == SQ_NONE ? " - " : " " + UCI::square(ep_square()) + " ") << st->rule50 - << " " << 1 + (gamePly - (sideToMove == BLACK)) / 2; + ss << (ep_square() == SQ_NONE ? " - " : " " + UCIEngine::square(ep_square()) + " ") + << st->rule50 << " " << 1 + (gamePly - (sideToMove == BLACK)) / 2; return ss.str(); } @@ -472,14 +492,23 @@ void Position::update_slider_blockers(Color c) const { // Slider attacks use the occupied bitboard to indicate occupancy. Bitboard Position::attackers_to(Square s, Bitboard occupied) const { - return (pawn_attacks_bb(BLACK, s) & pieces(WHITE, PAWN)) - | (pawn_attacks_bb(WHITE, s) & pieces(BLACK, PAWN)) - | (attacks_bb(s) & pieces(KNIGHT)) - | (attacks_bb(s, occupied) & pieces(ROOK, QUEEN)) + return (attacks_bb(s, occupied) & pieces(ROOK, QUEEN)) | (attacks_bb(s, occupied) & pieces(BISHOP, QUEEN)) - | (attacks_bb(s) & pieces(KING)); + | (attacks_bb(s, BLACK) & pieces(WHITE, PAWN)) + | (attacks_bb(s, WHITE) & pieces(BLACK, PAWN)) + | (attacks_bb(s) & pieces(KNIGHT)) | (attacks_bb(s) & pieces(KING)); } +bool Position::attackers_to_exist(Square s, Bitboard occupied, Color c) const { + + return ((attacks_bb(s) & pieces(c, ROOK, QUEEN)) + && (attacks_bb(s, occupied) & pieces(c, ROOK, QUEEN))) + || ((attacks_bb(s) & pieces(c, BISHOP, QUEEN)) + && (attacks_bb(s, occupied) & pieces(c, BISHOP, QUEEN))) + || (((attacks_bb(s, ~c) & pieces(PAWN)) | (attacks_bb(s) & pieces(KNIGHT)) + | (attacks_bb(s) & pieces(KING))) + & pieces(c)); +} // Tests whether a pseudo-legal move is legal bool Position::legal(Move m) const { @@ -521,7 +550,7 @@ bool Position::legal(Move m) const { Direction step = to > from ? WEST : EAST; for (Square s = to; s != from; s += step) - if (attackers_to(s) & pieces(~us)) + if (attackers_to_exist(s, pieces(), ~us)) return false; // In case of Chess960, verify if the Rook blocks some checks. @@ -532,11 +561,11 @@ bool Position::legal(Move m) const { // If the moving piece is a king, check whether the destination square is // attacked by the opponent. if (type_of(piece_on(from)) == KING) - return !(attackers_to(to, pieces() ^ from) & pieces(~us)); + return !(attackers_to_exist(to, pieces() ^ from, ~us)); // A non-king move is legal if and only if it is not pinned or it // is moving along the ray towards or away from the king. - return !(blockers_for_king(us) & from) || aligned(from, to, square(us)); + return !(blockers_for_king(us) & from) || line_bb(from, to) & pieces(us, KING); } @@ -575,10 +604,14 @@ bool Position::pseudo_legal(const Move m) const { if ((Rank8BB | Rank1BB) & to) return false; - if (!(pawn_attacks_bb(us, from) & pieces(~us) & to) // Not a capture - && !((from + pawn_push(us) == to) && empty(to)) // Not a single push - && !((from + 2 * pawn_push(us) == to) // Not a double push - && (relative_rank(us, from) == RANK_2) && empty(to) && empty(to - pawn_push(us)))) + // Check if it's a valid capture, single push, or double push + const bool isCapture = bool(attacks_bb(from, us) & pieces(~us) & to); + const bool isSinglePush = (from + pawn_push(us) == to) && empty(to); + const bool isDoublePush = (from + 2 * pawn_push(us) == to) + && (relative_rank(us, from) == RANK_2) && empty(to) + && empty(to - pawn_push(us)); + + if (!(isCapture || isSinglePush || isDoublePush)) return false; } else if (!(attacks_bb(type_of(pc), from, pieces()) & to)) @@ -601,7 +634,7 @@ bool Position::pseudo_legal(const Move m) const { } // In case of king moves under check we have to remove the king so as to catch // invalid moves like b1a1 when opposite queen is on c1. - else if (attackers_to(to, pieces() ^ from) & pieces(~us)) + else if (attackers_to_exist(to, pieces() ^ from, ~us)) return false; } @@ -624,7 +657,7 @@ bool Position::gives_check(Move m) const { // Is there a discovered check? if (blockers_for_king(~sideToMove) & from) - return !aligned(from, to, square(~sideToMove)) || m.type_of() == CASTLING; + return !(line_bb(from, to) & pieces(~sideToMove, KING)) || m.type_of() == CASTLING; switch (m.type_of()) { @@ -632,7 +665,7 @@ bool Position::gives_check(Move m) const { return false; case PROMOTION : - return attacks_bb(m.promotion_type(), to, pieces() ^ from) & square(~sideToMove); + return attacks_bb(m.promotion_type(), to, pieces() ^ from) & pieces(~sideToMove, KING); // En passant capture with check? We have already handled the case of direct // checks and ordinary discovered check, so the only case we need to handle @@ -659,7 +692,15 @@ bool Position::gives_check(Move m) const { // Makes a move, and saves all information necessary // to a StateInfo object. The move is assumed to be legal. Pseudo-legal // moves should be filtered out before this function is called. -void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { +// If a pointer to the TT table is passed, the entry for the new position +// will be prefetched, and likewise for shared history. +void Position::do_move(Move m, + StateInfo& newSt, + bool givesCheck, + DirtyPiece& dp, + DirtyThreats& dts, + const TranspositionTable* tt = nullptr, + const SharedHistories* history = nullptr) { assert(m.is_ok()); assert(&newSt != st); @@ -679,16 +720,6 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { ++st->rule50; ++st->pliesFromNull; - // Used by NNUE - st->accumulatorBig.computed[WHITE] = st->accumulatorBig.computed[BLACK] = - st->accumulatorBig.computedPSQT[WHITE] = st->accumulatorBig.computedPSQT[BLACK] = - st->accumulatorSmall.computed[WHITE] = st->accumulatorSmall.computed[BLACK] = - st->accumulatorSmall.computedPSQT[WHITE] = st->accumulatorSmall.computedPSQT[BLACK] = - false; - - auto& dp = st->dirtyPiece; - dp.dirty_num = 1; - Color us = sideToMove; Color them = ~us; Square from = m.from_sq(); @@ -696,6 +727,16 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { Piece pc = piece_on(from); Piece captured = m.type_of() == EN_PASSANT ? make_piece(them, PAWN) : piece_on(to); + bool checkEP = false; + + dp.pc = pc; + dp.from = from; + dp.to = to; + dp.add_sq = SQ_NONE; + dts.us = us; + dts.prevKsq = square(us); + dts.threatenedSqs = dts.threateningSqs = 0; + assert(color_of(pc) == us); assert(captured == NO_PIECE || color_of(captured) == (m.type_of() != CASTLING ? them : us)); assert(type_of(captured) != KING); @@ -706,13 +747,13 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { assert(captured == make_piece(us, ROOK)); Square rfrom, rto; - do_castling(us, from, to, rfrom, rto); + do_castling(us, from, to, rfrom, rto, &dts, &dp); k ^= Zobrist::psq[captured][rfrom] ^ Zobrist::psq[captured][rto]; + st->nonPawnKey[us] ^= Zobrist::psq[captured][rfrom] ^ Zobrist::psq[captured][rto]; captured = NO_PIECE; } - - if (captured) + else if (captured) { Square capsq = to; @@ -729,28 +770,34 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { assert(relative_rank(us, to) == RANK_6); assert(piece_on(to) == NO_PIECE); assert(piece_on(capsq) == make_piece(them, PAWN)); + + // Update board and piece lists in ep case, normal captures are updated later + remove_piece(capsq, &dts); } st->pawnKey ^= Zobrist::psq[captured][capsq]; } else + { st->nonPawnMaterial[them] -= PieceValue[captured]; + st->nonPawnKey[them] ^= Zobrist::psq[captured][capsq]; - dp.dirty_num = 2; // 1 piece moved, 1 piece captured - dp.piece[1] = captured; - dp.from[1] = capsq; - dp.to[1] = SQ_NONE; + if (type_of(captured) <= BISHOP) + st->minorPieceKey ^= Zobrist::psq[captured][capsq]; + } - // Update board and piece lists - remove_piece(capsq); + dp.remove_pc = captured; + dp.remove_sq = capsq; - // Update material hash key and prefetch access to materialTable k ^= Zobrist::psq[captured][capsq]; - st->materialKey ^= Zobrist::psq[captured][pieceCount[captured]]; + st->materialKey ^= + Zobrist::psq[captured][8 + pieceCount[captured] - (m.type_of() != EN_PASSANT)]; // Reset rule 50 counter st->rule50 = 0; } + else + dp.remove_sq = SQ_NONE; // Update hash key k ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; @@ -773,46 +820,45 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { // Move the piece. The tricky Chess960 castling is handled earlier if (m.type_of() != CASTLING) { - dp.piece[0] = pc; - dp.from[0] = from; - dp.to[0] = to; - - move_piece(from, to); + if (captured && m.type_of() != EN_PASSANT) + { + remove_piece(from, &dts); + swap_piece(to, pc, &dts); + } + else + move_piece(from, to, &dts); } // If the moving piece is a pawn do some special extra work if (type_of(pc) == PAWN) { - // Set en passant square if the moved pawn can be captured - if ((int(to) ^ int(from)) == 16 - && (pawn_attacks_bb(us, to - pawn_push(us)) & pieces(them, PAWN))) - { - st->epSquare = to - pawn_push(us); - k ^= Zobrist::enpassant[file_of(st->epSquare)]; - } + // Check later if the en passant square needs to be set + if ((int(to) ^ int(from)) == 16) + checkEP = true; else if (m.type_of() == PROMOTION) { - Piece promotion = make_piece(us, m.promotion_type()); + Piece promotion = make_piece(us, m.promotion_type()); + PieceType promotionType = type_of(promotion); assert(relative_rank(us, to) == RANK_8); assert(type_of(promotion) >= KNIGHT && type_of(promotion) <= QUEEN); - remove_piece(to); - put_piece(promotion, to); + swap_piece(to, promotion, &dts); - // Promoting pawn to SQ_NONE, promoted piece from SQ_NONE - dp.to[0] = SQ_NONE; - dp.piece[dp.dirty_num] = promotion; - dp.from[dp.dirty_num] = SQ_NONE; - dp.to[dp.dirty_num] = to; - dp.dirty_num++; + dp.add_pc = promotion; + dp.add_sq = to; + dp.to = SQ_NONE; // Update hash keys - k ^= Zobrist::psq[pc][to] ^ Zobrist::psq[promotion][to]; - st->pawnKey ^= Zobrist::psq[pc][to]; - st->materialKey ^= - Zobrist::psq[promotion][pieceCount[promotion] - 1] ^ Zobrist::psq[pc][pieceCount[pc]]; + // Zobrist::psq[pc][to] is zero, so we don't need to clear it + k ^= Zobrist::psq[promotion][to]; + st->materialKey ^= Zobrist::psq[promotion][8 + pieceCount[promotion] - 1] + ^ Zobrist::psq[pc][8 + pieceCount[pc]]; + st->nonPawnKey[us] ^= Zobrist::psq[promotion][to]; + + if (promotionType <= BISHOP) + st->minorPieceKey ^= Zobrist::psq[promotion][to]; // Update material st->nonPawnMaterial[us] += PieceValue[promotion]; @@ -825,12 +871,30 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { st->rule50 = 0; } + else + { + st->nonPawnKey[us] ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; + + if (type_of(pc) <= BISHOP) + st->minorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; + } + + // If en passant is impossible, then k will not change and we can prefetch earlier + if (tt && !checkEP) + prefetch(tt->first_entry(adjust_key50(k))); + + if (history) + { + prefetch(&history->pawn_entry(*this)[pc][to]); + prefetch(&history->pawn_correction_entry(*this)); + prefetch(&history->minor_piece_correction_entry(*this)); + prefetch(&history->nonpawn_correction_entry(*this)); + prefetch(&history->nonpawn_correction_entry(*this)); + } + // Set capture piece st->capturedPiece = captured; - // Update the key with the final value - st->key = k; - // Calculate checkers bitboard (if move gives check) st->checkersBB = givesCheck ? attackers_to(square(them)) & pieces(us) : 0; @@ -839,6 +903,63 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { // Update king attacks used for fast check detection set_check_info(); + // Accurate e.p. info is needed for correct zobrist key generation and 3-fold checking + while (checkEP) + { + auto updateEpSquare = [&] { + st->epSquare = to - pawn_push(us); + k ^= Zobrist::enpassant[file_of(st->epSquare)]; + }; + + Bitboard pawns = attacks_bb(to - pawn_push(us), us) & pieces(them, PAWN); + + // If there are no pawns attacking the ep square, ep is not possible + if (!pawns) + break; + + // If there are checkers other than the to be captured pawn, ep is never legal + if (checkers() & ~square_bb(to)) + break; + + if (more_than_one(pawns)) + { + // If there are two pawns potentially being able to capture and at least one + // is not pinned, ep is legal as there are no horizontal exposed checks + if (!more_than_one(blockers_for_king(them) & pawns)) + { + updateEpSquare(); + break; + } + + // If there is no pawn on our king's file, and thus both pawns are pinned + // by bishops, ep is not legal as the king square must be in front of the to square. + // And because the ep square and the king are not on a common diagonal, either ep capture + // would expose the king to a check from one of the bishops + if (!(file_bb(square(them)) & pawns)) + break; + + // Otherwise remove the pawn on the king file, as an ep capture by it can never be legal and the + // check below relies on there only being one pawn + pawns &= ~file_bb(square(them)); + } + + Square ksq = square(them); + Square capsq = to; + Bitboard occupied = (pieces() ^ lsb(pawns) ^ capsq) | (to - pawn_push(us)); + + // If our king is not attacked after making the move, ep is legal. + if (!(attacks_bb(ksq, occupied) & pieces(us, QUEEN, ROOK)) + && !(attacks_bb(ksq, occupied) & pieces(us, QUEEN, BISHOP))) + updateEpSquare(); + + break; + } + + // Update the key with the final value + st->key = k; + if (tt) + prefetch(tt->first_entry(key())); + // Calculate the repetition info. It is the ply distance from the previous // occurrence of the same position, negative in the 3-fold case, or zero // if the position was not repeated. @@ -858,7 +979,14 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { } } + dts.ksq = square(us); + assert(pos_is_ok()); + + assert(dp.pc != NO_PIECE); + assert(!(bool(captured) || m.type_of() == CASTLING) ^ (dp.remove_sq != SQ_NONE)); + assert(dp.from != SQ_NONE); + assert(!(dp.add_sq != SQ_NONE) ^ (m.type_of() == PROMOTION || m.type_of() == CASTLING)); } @@ -924,59 +1052,208 @@ void Position::undo_move(Move m) { assert(pos_is_ok()); } +template +inline void add_dirty_threat( + DirtyThreats* const dts, Piece pc, Piece threatened, Square s, Square threatenedSq) { + if (PutPiece) + { + dts->threatenedSqs |= square_bb(threatenedSq); + dts->threateningSqs |= square_bb(s); + } + + dts->list.push_back({pc, threatened, s, threatenedSq, PutPiece}); +} + +#ifdef USE_AVX512ICL +// Given a DirtyThreat template and bit offsets to insert the piece type and square, write the threats +// present at the given bitboard. +template +void write_multiple_dirties(const Position& p, + Bitboard mask, + DirtyThreat dt_template, + DirtyThreats* dts) { + static_assert(sizeof(DirtyThreat) == 4); + + const __m512i board = _mm512_loadu_si512(p.piece_array().data()); + const __m512i AllSquares = _mm512_set_epi8( + 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, + 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, + 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0); + + const int dt_count = popcount(mask); + assert(dt_count <= 16); + + const __m512i template_v = _mm512_set1_epi32(dt_template.raw()); + auto* write = dts->list.make_space(dt_count); + + // Extract the list of squares and upconvert to 32 bits. There are never more than 16 + // incoming threats so this is sufficient. + __m512i threat_squares = _mm512_maskz_compress_epi8(mask, AllSquares); + threat_squares = _mm512_cvtepi8_epi32(_mm512_castsi512_si128(threat_squares)); + + __m512i threat_pieces = + _mm512_maskz_permutexvar_epi8(0x1111111111111111ULL, threat_squares, board); + + // Shift the piece and square into place + threat_squares = _mm512_slli_epi32(threat_squares, SqShift); + threat_pieces = _mm512_slli_epi32(threat_pieces, PcShift); + + const __m512i dirties = + _mm512_ternarylogic_epi32(template_v, threat_squares, threat_pieces, 254 /* A | B | C */); + _mm512_storeu_si512(reinterpret_cast<__m512i*>(write), dirties); +} +#endif + +template +void Position::update_piece_threats(Piece pc, + Square s, + DirtyThreats* const dts, + [[maybe_unused]] Bitboard noRaysContaining) const { + const Bitboard occupied = pieces(); + const Bitboard rookQueens = pieces(ROOK, QUEEN); + const Bitboard bishopQueens = pieces(BISHOP, QUEEN); + const Bitboard knights = pieces(KNIGHT); + const Bitboard kings = pieces(KING); + const Bitboard whitePawns = pieces(WHITE, PAWN); + const Bitboard blackPawns = pieces(BLACK, PAWN); + + const Bitboard rAttacks = attacks_bb(s, occupied); + const Bitboard bAttacks = attacks_bb(s, occupied); + + Bitboard threatened = attacks_bb(pc, s, occupied) & occupied; + Bitboard sliders = (rookQueens & rAttacks) | (bishopQueens & bAttacks); + Bitboard incoming_threats = + (PseudoAttacks[KNIGHT][s] & knights) | (attacks_bb(s, WHITE) & blackPawns) + | (attacks_bb(s, BLACK) & whitePawns) | (PseudoAttacks[KING][s] & kings); + +#ifdef USE_AVX512ICL + if (threatened) + { + if constexpr (PutPiece) + { + dts->threatenedSqs |= threatened; + dts->threateningSqs |= square_bb(s); + } + + DirtyThreat dt_template{pc, NO_PIECE, s, Square(0), PutPiece}; + write_multiple_dirties( + *this, threatened, dt_template, dts); + } + + Bitboard all_attackers = sliders | incoming_threats; + if (!all_attackers) + return; // Square s is threatened iff there's at least one attacker + + if constexpr (PutPiece) + { + dts->threatenedSqs |= square_bb(s); + dts->threateningSqs |= all_attackers; + } + + DirtyThreat dt_template{NO_PIECE, pc, Square(0), s, PutPiece}; + write_multiple_dirties(*this, all_attackers, + dt_template, dts); +#else + while (threatened) + { + Square threatenedSq = pop_lsb(threatened); + Piece threatenedPc = piece_on(threatenedSq); + + assert(threatenedSq != s); + assert(threatenedPc); + + add_dirty_threat(dts, pc, threatenedPc, s, threatenedSq); + } +#endif + + if constexpr (ComputeRay) + { + while (sliders) + { + Square sliderSq = pop_lsb(sliders); + Piece slider = piece_on(sliderSq); + + const Bitboard ray = RayPassBB[sliderSq][s] & ~BetweenBB[sliderSq][s]; + const Bitboard discovered = ray & (rAttacks | bAttacks) & occupied; + + assert(!more_than_one(discovered)); + if (discovered && (RayPassBB[sliderSq][s] & noRaysContaining) != noRaysContaining) + { + const Square threatenedSq = lsb(discovered); + const Piece threatenedPc = piece_on(threatenedSq); + add_dirty_threat(dts, slider, threatenedPc, sliderSq, threatenedSq); + } + +#ifndef USE_AVX512ICL // for ICL, direct threats were processed earlier (all_attackers) + add_dirty_threat(dts, slider, pc, sliderSq, s); +#endif + } + } + else + { + incoming_threats |= sliders; + } + +#ifndef USE_AVX512ICL + while (incoming_threats) + { + Square srcSq = pop_lsb(incoming_threats); + Piece srcPc = piece_on(srcSq); + + assert(srcSq != s); + assert(srcPc != NO_PIECE); + + add_dirty_threat(dts, srcPc, pc, srcSq, s); + } +#endif +} // Helper used to do/undo a castling move. This is a bit // tricky in Chess960 where from/to squares can overlap. template -void Position::do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto) { +void Position::do_castling(Color us, + Square from, + Square& to, + Square& rfrom, + Square& rto, + DirtyThreats* const dts, + DirtyPiece* const dp) { bool kingSide = to > from; rfrom = to; // Castling is encoded as "king captures friendly rook" rto = relative_square(us, kingSide ? SQ_F1 : SQ_D1); to = relative_square(us, kingSide ? SQ_G1 : SQ_C1); + assert(!Do || dp); + if (Do) { - auto& dp = st->dirtyPiece; - dp.piece[0] = make_piece(us, KING); - dp.from[0] = from; - dp.to[0] = to; - dp.piece[1] = make_piece(us, ROOK); - dp.from[1] = rfrom; - dp.to[1] = rto; - dp.dirty_num = 2; + dp->to = to; + dp->remove_pc = dp->add_pc = make_piece(us, ROOK); + dp->remove_sq = rfrom; + dp->add_sq = rto; } // Remove both pieces first since squares could overlap in Chess960 - remove_piece(Do ? from : to); - remove_piece(Do ? rfrom : rto); - board[Do ? from : to] = board[Do ? rfrom : rto] = - NO_PIECE; // remove_piece does not do this for us - put_piece(make_piece(us, KING), Do ? to : from); - put_piece(make_piece(us, ROOK), Do ? rto : rfrom); + remove_piece(Do ? from : to, dts); + remove_piece(Do ? rfrom : rto, dts); + put_piece(make_piece(us, KING), Do ? to : from, dts); + put_piece(make_piece(us, ROOK), Do ? rto : rfrom, dts); } // Used to do a "null move": it flips // the side to move without executing any move on the board. -void Position::do_null_move(StateInfo& newSt, TranspositionTable& tt) { +void Position::do_null_move(StateInfo& newSt, const TranspositionTable& tt) { assert(!checkers()); assert(&newSt != st); - std::memcpy(&newSt, st, offsetof(StateInfo, accumulatorBig)); + std::memcpy(&newSt, st, sizeof(StateInfo)); newSt.previous = st; st = &newSt; - st->dirtyPiece.dirty_num = 0; - st->dirtyPiece.piece[0] = NO_PIECE; // Avoid checks in UpdateAccumulator() - st->accumulatorBig.computed[WHITE] = st->accumulatorBig.computed[BLACK] = - st->accumulatorBig.computedPSQT[WHITE] = st->accumulatorBig.computedPSQT[BLACK] = - st->accumulatorSmall.computed[WHITE] = st->accumulatorSmall.computed[BLACK] = - st->accumulatorSmall.computedPSQT[WHITE] = st->accumulatorSmall.computedPSQT[BLACK] = - false; - if (st->epSquare != SQ_NONE) { st->key ^= Zobrist::enpassant[file_of(st->epSquare)]; @@ -984,7 +1261,6 @@ void Position::do_null_move(StateInfo& newSt, TranspositionTable& tt) { } st->key ^= Zobrist::side; - ++st->rule50; prefetch(tt.first_entry(key())); st->pliesFromNull = 0; @@ -1009,26 +1285,6 @@ void Position::undo_null_move() { } -// Computes the new hash key after the given move. Needed -// for speculative prefetch. It doesn't recognize special moves like castling, -// en passant and promotions. -Key Position::key_after(Move m) const { - - Square from = m.from_sq(); - Square to = m.to_sq(); - Piece pc = piece_on(from); - Piece captured = piece_on(to); - Key k = st->key ^ Zobrist::side; - - if (captured) - k ^= Zobrist::psq[captured][to]; - - k ^= Zobrist::psq[pc][to] ^ Zobrist::psq[pc][from]; - - return (captured || type_of(pc) == PAWN) ? k : adjust_key50(k); -} - - // Tests if the SEE (Static Exchange Evaluation) // value of move is greater or equal to the given threshold. We'll use an // algorithm similar to alpha-beta pruning with a null window. @@ -1042,6 +1298,8 @@ bool Position::see_ge(Move m, int threshold) const { Square from = m.from_sq(), to = m.to_sq(); + assert(piece_on(from) != NO_PIECE); + int swap = PieceValue[piece_on(to)] - threshold; if (swap < 0) return false; @@ -1116,8 +1374,9 @@ bool Position::see_ge(Move m, int threshold) const { else if ((bb = stmAttackers & pieces(QUEEN))) { - if ((swap = QueenValue - swap) < res) - break; + swap = QueenValue - swap; + // implies that the previous recapture was done by a higher rated piece than a Queen (King is excluded) + assert(swap >= res); occupied ^= least_significant_square_bb(bb); attackers |= (attacks_bb(to, occupied) & pieces(BISHOP, QUEEN)) @@ -1140,11 +1399,12 @@ bool Position::is_draw(int ply) const { if (st->rule50 > 99 && (!checkers() || MoveList(*this).size())) return true; - // Return a draw score if a position repeats once earlier but strictly - // after the root, or repeats twice before or at the root. - return st->repetition && st->repetition < ply; + return is_repetition(ply); } +// Return a draw score if a position repeats once earlier but strictly +// after the root, or repeats twice before or at the root. +bool Position::is_repetition(int ply) const { return st->repetition && st->repetition < ply; } // Tests whether there has been at least one repetition // of positions since the last capture or pawn move. @@ -1163,9 +1423,9 @@ bool Position::has_repeated() const { } -// Tests if the position has a move which draws by repetition, -// or an earlier position has a move that directly reaches the current position. -bool Position::has_game_cycle(int ply) const { +// Tests if the position has a move which draws by repetition. +// This function accurately matches the outcome of is_draw() over all legal moves. +bool Position::upcoming_repetition(int ply) const { int j; @@ -1176,10 +1436,16 @@ bool Position::has_game_cycle(int ply) const { Key originalKey = st->key; StateInfo* stp = st->previous; + Key other = originalKey ^ stp->key ^ Zobrist::side; for (int i = 3; i <= end; i += 2) { - stp = stp->previous->previous; + stp = stp->previous; + other ^= stp->key ^ stp->previous->key ^ Zobrist::side; + stp = stp->previous; + + if (other != 0) + continue; Key moveKey = originalKey ^ stp->key; if ((j = H1(moveKey), cuckoo[j] == moveKey) || (j = H2(moveKey), cuckoo[j] == moveKey)) @@ -1195,12 +1461,6 @@ bool Position::has_game_cycle(int ply) const { // For nodes before or at the root, check that the move is a // repetition rather than a move to the current position. - // In the cuckoo table, both moves Rc1c5 and Rc5c1 are stored in - // the same location, so we have to select which square to check. - if (color_of(piece_on(empty(s1) ? s2 : s1)) != side_to_move()) - continue; - - // For repetitions before or at the root, require one more if (stp->repetition) return true; } @@ -1244,6 +1504,9 @@ void Position::flip() { } +bool Position::material_key_is_ok() const { return compute_material_key() == st->materialKey; } + + // Performs some consistency checks for the position object // and raise an assert if something wrong is detected. // This is meant to be helpful when debugging. @@ -1260,7 +1523,7 @@ bool Position::pos_is_ok() const { return true; if (pieceCount[W_KING] != 1 || pieceCount[B_KING] != 1 - || attackers_to(square(~sideToMove)) & pieces(sideToMove)) + || attackers_to_exist(square(~sideToMove), pieces(), sideToMove)) assert(0 && "pos_is_ok: Kings"); if ((pieces(PAWN) & (Rank1BB | Rank8BB)) || pieceCount[W_PAWN] > 8 || pieceCount[B_PAWN] > 8) @@ -1278,7 +1541,7 @@ bool Position::pos_is_ok() const { for (Piece pc : Pieces) if (pieceCount[pc] != popcount(pieces(color_of(pc), type_of(pc))) - || pieceCount[pc] != std::count(board, board + SQUARE_NB, pc)) + || pieceCount[pc] != std::count(board.begin(), board.end(), pc)) assert(0 && "pos_is_ok: Pieces"); for (Color c : {WHITE, BLACK}) @@ -1293,6 +1556,8 @@ bool Position::pos_is_ok() const { assert(0 && "pos_is_ok: Castling"); } + assert(material_key_is_ok() && "pos_is_ok: materialKey"); + return true; } diff --git a/src/position.h b/src/position.h index 154ed6529..a136c0729 100644 --- a/src/position.h +++ b/src/position.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -19,20 +19,21 @@ #ifndef POSITION_H_INCLUDED #define POSITION_H_INCLUDED +#include #include #include #include #include +#include #include #include "bitboard.h" -#include "nnue/nnue_accumulator.h" -#include "nnue/nnue_architecture.h" #include "types.h" namespace Stockfish { class TranspositionTable; +struct SharedHistories; // StateInfo struct stores information needed to restore a Position object to // its previous state when we retract a move. Whenever a move is made on the @@ -43,6 +44,8 @@ struct StateInfo { // Copied when making a move Key materialKey; Key pawnKey; + Key minorPieceKey; + Key nonPawnKey[COLOR_NB]; Value nonPawnMaterial[COLOR_NB]; int castlingRights; int rule50; @@ -58,11 +61,6 @@ struct StateInfo { Bitboard checkSquares[PIECE_TYPE_NB]; Piece capturedPiece; int repetition; - - // Used by NNUE - Eval::NNUE::Accumulator accumulatorBig; - Eval::NNUE::Accumulator accumulatorSmall; - DirtyPiece dirtyPiece; }; @@ -72,7 +70,6 @@ struct StateInfo { // elements are not invalidated upon list resizing. using StateListPtr = std::unique_ptr>; - // Position class stores information regarding the board representation as // pieces, side to move, hash keys, castling info, etc. Important methods are // do_move() and undo_move(), used by the search to update node info when @@ -91,15 +88,16 @@ class Position { std::string fen() const; // Position representation - Bitboard pieces(PieceType pt = ALL_PIECES) const; + Bitboard pieces() const; // All pieces template - Bitboard pieces(PieceType pt, PieceTypes... pts) const; + Bitboard pieces(PieceTypes... pts) const; Bitboard pieces(Color c) const; template - Bitboard pieces(Color c, PieceTypes... pts) const; - Piece piece_on(Square s) const; - Square ep_square() const; - bool empty(Square s) const; + Bitboard pieces(Color c, PieceTypes... pts) const; + Piece piece_on(Square s) const; + const std::array& piece_array() const; + Square ep_square() const; + bool empty(Square s) const; template int count(Color c) const; template @@ -108,10 +106,9 @@ class Position { Square square(Color c) const; // Castling - CastlingRights castling_rights(Color c) const; - bool can_castle(CastlingRights cr) const; - bool castling_impeded(CastlingRights cr) const; - Square castling_rook_square(CastlingRights cr) const; + bool can_castle(CastlingRights cr) const; + bool castling_impeded(CastlingRights cr) const; + Square castling_rook_square(CastlingRights cr) const; // Checking Bitboard checkers() const; @@ -122,6 +119,7 @@ class Position { // Attacks to/from a given square Bitboard attackers_to(Square s) const; Bitboard attackers_to(Square s, Bitboard occupied) const; + bool attackers_to_exist(Square s, Bitboard occupied, Color c) const; void update_slider_blockers(Color c) const; template Bitboard attacks_by(Color c) const; @@ -136,10 +134,16 @@ class Position { Piece captured_piece() const; // Doing and undoing moves - void do_move(Move m, StateInfo& newSt); - void do_move(Move m, StateInfo& newSt, bool givesCheck); + void do_move(Move m, StateInfo& newSt, const TranspositionTable* tt); + void do_move(Move m, + StateInfo& newSt, + bool givesCheck, + DirtyPiece& dp, + DirtyThreats& dts, + const TranspositionTable* tt, + const SharedHistories* worker); void undo_move(Move m); - void do_null_move(StateInfo& newSt, TranspositionTable& tt); + void do_null_move(StateInfo& newSt, const TranspositionTable& tt); void undo_null_move(); // Static Exchange Evaluation @@ -147,16 +151,18 @@ class Position { // Accessing hash keys Key key() const; - Key key_after(Move m) const; Key material_key() const; Key pawn_key() const; + Key minor_piece_key() const; + Key non_pawn_key(Color c) const; // Other properties of the position Color side_to_move() const; int game_ply() const; bool is_chess960() const; bool is_draw(int ply) const; - bool has_game_cycle(int ply) const; + bool is_repetition(int ply) const; + bool upcoming_repetition(int ply) const; bool has_repeated() const; int rule50_count() const; Value non_pawn_material(Color c) const; @@ -164,39 +170,54 @@ class Position { // Position consistency check, for debugging bool pos_is_ok() const; + bool material_key_is_ok() const; void flip(); - // Used by NNUE StateInfo* state() const; - void put_piece(Piece pc, Square s); - void remove_piece(Square s); + void put_piece(Piece pc, Square s, DirtyThreats* const dts = nullptr); + void remove_piece(Square s, DirtyThreats* const dts = nullptr); + void swap_piece(Square s, Piece pc, DirtyThreats* const dts = nullptr); private: // Initialization helpers (used while setting up a position) void set_castling_right(Color c, Square rfrom); + Key compute_material_key() const; void set_state() const; void set_check_info() const; // Other helpers - void move_piece(Square from, Square to); + template + void update_piece_threats(Piece pc, + Square s, + DirtyThreats* const dts, + Bitboard noRaysContaining = -1ULL) const; + void move_piece(Square from, Square to, DirtyThreats* const dts = nullptr); template - void do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto); - template - Key adjust_key50(Key k) const; + void do_castling(Color us, + Square from, + Square& to, + Square& rfrom, + Square& rto, + DirtyThreats* const dts = nullptr, + DirtyPiece* const dp = nullptr); + Key adjust_key50(Key k) const; // Data members - Piece board[SQUARE_NB]; - Bitboard byTypeBB[PIECE_TYPE_NB]; - Bitboard byColorBB[COLOR_NB]; - int pieceCount[PIECE_NB]; - int castlingRightsMask[SQUARE_NB]; - Square castlingRookSquare[CASTLING_RIGHT_NB]; - Bitboard castlingPath[CASTLING_RIGHT_NB]; - StateInfo* st; - int gamePly; - Color sideToMove; - bool chess960; + std::array board; + std::array byTypeBB; + std::array byColorBB; + + int pieceCount[PIECE_NB]; + int castlingRightsMask[SQUARE_NB]; + Square castlingRookSquare[CASTLING_RIGHT_NB]; + Bitboard castlingPath[CASTLING_RIGHT_NB]; + StateInfo* st; + int gamePly; + Color sideToMove; + bool chess960; + DirtyPiece scratch_dp; + DirtyThreats scratch_dts; }; std::ostream& operator<<(std::ostream& os, const Position& pos); @@ -208,15 +229,17 @@ inline Piece Position::piece_on(Square s) const { return board[s]; } +inline const std::array& Position::piece_array() const { return board; } + inline bool Position::empty(Square s) const { return piece_on(s) == NO_PIECE; } inline Piece Position::moved_piece(Move m) const { return piece_on(m.from_sq()); } -inline Bitboard Position::pieces(PieceType pt) const { return byTypeBB[pt]; } +inline Bitboard Position::pieces() const { return byTypeBB[ALL_PIECES]; } template -inline Bitboard Position::pieces(PieceType pt, PieceTypes... pts) const { - return pieces(pt) | pieces(pts...); +inline Bitboard Position::pieces(PieceTypes... pts) const { + return (byTypeBB[pts] | ...); } inline Bitboard Position::pieces(Color c) const { return byColorBB[c]; } @@ -246,10 +269,6 @@ inline Square Position::ep_square() const { return st->epSquare; } inline bool Position::can_castle(CastlingRights cr) const { return st->castlingRights & cr; } -inline CastlingRights Position::castling_rights(Color c) const { - return c & CastlingRights(st->castlingRights); -} - inline bool Position::castling_impeded(CastlingRights cr) const { assert(cr == WHITE_OO || cr == WHITE_OOO || cr == BLACK_OO || cr == BLACK_OOO); return pieces() & castlingPath[cr]; @@ -286,17 +305,20 @@ inline Bitboard Position::pinners(Color c) const { return st->pinners[c]; } inline Bitboard Position::check_squares(PieceType pt) const { return st->checkSquares[pt]; } -inline Key Position::key() const { return adjust_key50(st->key); } +inline Key Position::key() const { return adjust_key50(st->key); } -template inline Key Position::adjust_key50(Key k) const { - return st->rule50 < 14 - AfterMove ? k : k ^ make_key((st->rule50 - (14 - AfterMove)) / 8); + return st->rule50 < 14 ? k : k ^ make_key((st->rule50 - 14) / 8); } inline Key Position::pawn_key() const { return st->pawnKey; } inline Key Position::material_key() const { return st->materialKey; } +inline Key Position::minor_piece_key() const { return st->minorPieceKey; } + +inline Key Position::non_pawn_key(Color c) const { return st->nonPawnKey[c]; } + inline Value Position::non_pawn_material(Color c) const { return st->nonPawnMaterial[c]; } inline Value Position::non_pawn_material() const { @@ -315,8 +337,8 @@ inline bool Position::capture(Move m) const { } // Returns true if a move is generated from the capture stage, having also -// queen promotions covered, i.e. consistency with the capture stage move generation -// is needed to avoid the generation of duplicate moves. +// queen promotions covered, i.e. consistency with the capture stage move +// generation is needed to avoid the generation of duplicate moves. inline bool Position::capture_stage(Move m) const { assert(m.is_ok()); return capture(m) || m.promotion_type() == QUEEN; @@ -324,18 +346,23 @@ inline bool Position::capture_stage(Move m) const { inline Piece Position::captured_piece() const { return st->capturedPiece; } -inline void Position::put_piece(Piece pc, Square s) { - +inline void Position::put_piece(Piece pc, Square s, DirtyThreats* const dts) { board[s] = pc; byTypeBB[ALL_PIECES] |= byTypeBB[type_of(pc)] |= s; byColorBB[color_of(pc)] |= s; pieceCount[pc]++; pieceCount[make_piece(color_of(pc), ALL_PIECES)]++; + + if (dts) + update_piece_threats(pc, s, dts); } -inline void Position::remove_piece(Square s) { - +inline void Position::remove_piece(Square s, DirtyThreats* const dts) { Piece pc = board[s]; + + if (dts) + update_piece_threats(pc, s, dts); + byTypeBB[ALL_PIECES] ^= s; byTypeBB[type_of(pc)] ^= s; byColorBB[color_of(pc)] ^= s; @@ -344,18 +371,41 @@ inline void Position::remove_piece(Square s) { pieceCount[make_piece(color_of(pc), ALL_PIECES)]--; } -inline void Position::move_piece(Square from, Square to) { - +inline void Position::move_piece(Square from, Square to, DirtyThreats* const dts) { Piece pc = board[from]; Bitboard fromTo = from | to; + + if (dts) + update_piece_threats(pc, from, dts, fromTo); + byTypeBB[ALL_PIECES] ^= fromTo; byTypeBB[type_of(pc)] ^= fromTo; byColorBB[color_of(pc)] ^= fromTo; board[from] = NO_PIECE; board[to] = pc; + + if (dts) + update_piece_threats(pc, to, dts, fromTo); } -inline void Position::do_move(Move m, StateInfo& newSt) { do_move(m, newSt, gives_check(m)); } +inline void Position::swap_piece(Square s, Piece pc, DirtyThreats* const dts) { + Piece old = board[s]; + + remove_piece(s); + + if (dts) + update_piece_threats(old, s, dts); + + put_piece(pc, s); + + if (dts) + update_piece_threats(pc, s, dts); +} + +inline void Position::do_move(Move m, StateInfo& newSt, const TranspositionTable* tt = nullptr) { + new (&scratch_dts) DirtyThreats; + do_move(m, newSt, gives_check(m), scratch_dp, scratch_dts, tt, nullptr); +} inline StateInfo* Position::state() const { return st; } diff --git a/src/score.cpp b/src/score.cpp new file mode 100644 index 000000000..ea62577b9 --- /dev/null +++ b/src/score.cpp @@ -0,0 +1,48 @@ +/* + 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 . +*/ + +#include "score.h" + +#include +#include +#include + +#include "uci.h" + +namespace Stockfish { + +Score::Score(Value v, const Position& pos) { + assert(-VALUE_INFINITE < v && v < VALUE_INFINITE); + + if (!is_decisive(v)) + { + score = InternalUnits{UCIEngine::to_cp(v, pos)}; + } + else if (std::abs(v) <= VALUE_TB) + { + auto distance = VALUE_TB - std::abs(v); + score = (v > 0) ? Tablebase{distance, true} : Tablebase{-distance, false}; + } + else + { + auto distance = VALUE_MATE - std::abs(v); + score = (v > 0) ? Mate{distance} : Mate{-distance}; + } +} + +} \ No newline at end of file diff --git a/src/score.h b/src/score.h new file mode 100644 index 000000000..cf89d3cdd --- /dev/null +++ b/src/score.h @@ -0,0 +1,70 @@ +/* + 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 . +*/ + +#ifndef SCORE_H_INCLUDED +#define SCORE_H_INCLUDED + +#include +#include + +#include "types.h" + +namespace Stockfish { + +class Position; + +class Score { + public: + struct Mate { + int plies; + }; + + struct Tablebase { + int plies; + bool win; + }; + + struct InternalUnits { + int value; + }; + + Score() = default; + Score(Value v, const Position& pos); + + template + bool is() const { + return std::holds_alternative(score); + } + + template + T get() const { + return std::get(score); + } + + template + decltype(auto) visit(F&& f) const { + return std::visit(std::forward(f), score); + } + + private: + std::variant score; +}; + +} + +#endif // #ifndef SCORE_H_INCLUDED diff --git a/src/search.cpp b/src/search.cpp index 186ee634a..bc5c3c3a5 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -22,26 +22,32 @@ #include #include #include +#include #include #include #include #include #include -#include +#include +#include +#include #include +#include "bitboard.h" #include "cluster.h" #include "evaluate.h" +#include "history.h" #include "misc.h" #include "movegen.h" #include "movepick.h" -#include "nnue/nnue_common.h" -#include "nnue/nnue_misc.h" +#include "nnue/network.h" +#include "nnue/nnue_accumulator.h" #include "position.h" #include "syzygy/tbprobe.h" #include "thread.h" #include "timeman.h" #include "tt.h" +#include "types.h" #include "uci.h" #include "ucioption.h" @@ -49,102 +55,136 @@ namespace Stockfish { namespace TB = Tablebases; -using Eval::evaluate; +void syzygy_extend_pv(const OptionsMap& options, + const Search::LimitsType& limits, + Stockfish::Position& pos, + Stockfish::Search::RootMove& rootMove, + Value& v); + using namespace Search; namespace { -static constexpr double EvalLevel[10] = {1.043, 1.017, 0.952, 1.009, 0.971, - 1.002, 0.992, 0.947, 1.046, 1.001}; +constexpr int SEARCHEDLIST_CAPACITY = 32; +constexpr int mainHistoryDefault = 68; +using SearchedList = ValueList; -// Futility margin -Value futility_margin(Depth d, bool noTtCutNode, bool improving, bool oppWorsening) { - Value futilityMult = 118 - 44 * noTtCutNode; - Value improvingDeduction = 53 * improving * futilityMult / 32; - Value worseningDeduction = (309 + 47 * improving) * oppWorsening * futilityMult / 1024; +// (*Scalers): +// The values with Scaler asterisks have proven non-linear scaling. +// They are optimized to time controls of 180 + 1.8 and longer, +// so changing them or adding conditions that are similar requires +// tests at these types of time controls. - return futilityMult * d - improvingDeduction - worseningDeduction; +// (*Scaler) All tuned parameters at time controls shorter than +// optimized for require verifications at longer time controls + +int correction_value(const Worker& w, const Position& pos, const Stack* const ss) { + const Color us = pos.side_to_move(); + const auto m = (ss - 1)->currentMove; + const auto& shared = w.sharedHistory; + const int pcv = shared.pawn_correction_entry(pos).at(us).pawn; + const int micv = shared.minor_piece_correction_entry(pos).at(us).minor; + const int wnpcv = shared.nonpawn_correction_entry(pos).at(us).nonPawnWhite; + const int bnpcv = shared.nonpawn_correction_entry(pos).at(us).nonPawnBlack; + const int cntcv = + m.is_ok() ? (*(ss - 2)->continuationCorrectionHistory)[pos.piece_on(m.to_sq())][m.to_sq()] + + (*(ss - 4)->continuationCorrectionHistory)[pos.piece_on(m.to_sq())][m.to_sq()] + : 8; + + return 10347 * pcv + 8821 * micv + 11665 * (wnpcv + bnpcv) + 7841 * cntcv; } -constexpr int futility_move_count(bool improving, Depth depth) { - return improving ? (3 + depth * depth) : (3 + depth * depth) / 2; +// Add correctionHistory value to raw staticEval and guarantee evaluation +// does not hit the tablebase range. +Value to_corrected_static_eval(const Value v, const int cv) { + return std::clamp(v + cv / 131072, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); } -// Add correctionHistory value to raw staticEval and guarantee evaluation does not hit the tablebase range -Value to_corrected_static_eval(Value v, const Worker& w, const Position& pos) { - auto cv = w.correctionHistory[pos.side_to_move()][pawn_structure_index(pos)]; - v += cv * std::abs(cv) / 11175; - return std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); +void update_correction_history(const Position& pos, + Stack* const ss, + Search::Worker& workerThread, + const int bonus) { + const Move m = (ss - 1)->currentMove; + const Color us = pos.side_to_move(); + + constexpr int nonPawnWeight = 178; + auto& shared = workerThread.sharedHistory; + + shared.pawn_correction_entry(pos).at(us).pawn << bonus; + shared.minor_piece_correction_entry(pos).at(us).minor << bonus * 156 / 128; + shared.nonpawn_correction_entry(pos).at(us).nonPawnWhite << bonus * nonPawnWeight / 128; + shared.nonpawn_correction_entry(pos).at(us).nonPawnBlack << bonus * nonPawnWeight / 128; + + if (m.is_ok()) + { + const Square to = m.to_sq(); + const Piece pc = pos.piece_on(m.to_sq()); + (*(ss - 2)->continuationCorrectionHistory)[pc][to] << bonus * 127 / 128; + (*(ss - 4)->continuationCorrectionHistory)[pc][to] << bonus * 59 / 128; + } } -// History and stats update bonus, based on depth -int stat_bonus(Depth d) { return std::clamp(245 * d - 320, 0, 1296); } - -// History and stats update malus, based on depth -int stat_malus(Depth d) { return (d < 4 ? 554 * d - 303 : 1203); } - // Add a small random component to draw evaluations to avoid 3-fold blindness Value value_draw(size_t nodes) { return VALUE_DRAW - 1 + Value(nodes & 0x2); } - -// Skill structure is used to implement strength limit. If we have a UCI_Elo, -// we convert it to an appropriate skill level, anchored to the Stash engine. -// This method is based on a fit of the Elo results for games played between -// Stockfish at various skill levels and various versions of the Stash engine. -// Skill 0 .. 19 now covers CCRL Blitz Elo from 1320 to 3190, approximately -// Reference: https://github.com/vondele/Stockfish/commit/a08b8d4e9711c2 -struct Skill { - Skill(int skill_level, int uci_elo) { - if (uci_elo) - { - double e = double(uci_elo - 1320) / (3190 - 1320); - level = std::clamp((((37.2473 * e - 40.8525) * e + 22.2943) * e - 0.311438), 0.0, 19.0); - } - else - level = double(skill_level); - } - bool enabled() const { return level < 20.0; } - bool time_to_pick(Depth depth) const { return depth == 1 + int(level); } - Move pick_best(const RootMoves&, size_t multiPV); - - double level; - Move best = Move::none(); -}; - Value value_to_tt(Value v, int ply); Value value_from_tt(Value v, int ply, int r50c); void update_pv(Move* pv, Move move, const Move* childPv); void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus); -void update_quiet_stats( +void update_quiet_histories( const Position& pos, Stack* ss, Search::Worker& workerThread, Move move, int bonus); void update_all_stats(const Position& pos, Stack* ss, Search::Worker& workerThread, Move bestMove, - Value bestValue, - Value beta, Square prevSq, - Move* quietsSearched, - int quietCount, - Move* capturesSearched, - int captureCount, - Depth depth); + SearchedList& quietsSearched, + SearchedList& capturesSearched, + Depth depth, + Move TTMove, + int moveCount); + +bool is_shuffling(Move move, Stack* const ss, const Position& pos) { + if (pos.capture_stage(move) || pos.rule50_count() < 10) + return false; + if (pos.state()->pliesFromNull <= 6 || ss->ply < 20) + return false; + return move.from_sq() == (ss - 2)->currentMove.to_sq() + && (ss - 2)->currentMove.from_sq() == (ss - 4)->currentMove.to_sq(); +} } // namespace Search::Worker::Worker(SharedState& sharedState, std::unique_ptr sm, - size_t thread_id) : + size_t threadId, + size_t numaThreadId, + size_t numaTotalThreads, + NumaReplicatedAccessToken token) : // Unpack the SharedState struct into member variables - thread_idx(thread_id), + sharedHistory(sharedState.sharedHistories.at(token.get_numa_index())), + threadIdx(threadId), + numaThreadIdx(numaThreadId), + numaTotal(numaTotalThreads), + numaAccessToken(token), manager(std::move(sm)), options(sharedState.options), threads(sharedState.threads), tt(sharedState.tt), - networks(sharedState.networks) { + networks(sharedState.networks), + refreshTable(networks[token]) { clear(); } +void Search::Worker::ensure_network_replicated() { + // Access once to force lazy initialization. + // We do this because we want to avoid initialization during search. + (void) (networks[numaAccessToken]); +} + void Search::Worker::start_searching() { + + accumulatorStack.reset(); + // Non-main threads go directly to iterative_deepening() if (!is_mainthread()) { @@ -152,16 +192,16 @@ void Search::Worker::start_searching() { return; } - main_manager()->tm.init(limits, rootPos.side_to_move(), rootPos.game_ply(), options); + main_manager()->tm.init(limits, rootPos.side_to_move(), rootPos.game_ply(), options, + main_manager()->originalTimeAdjust); tt.new_search(); if (rootMoves.empty()) { rootMoves.emplace_back(Move::none()); - if (Cluster::is_root()) - sync_cout << "info depth 0 score " - << UCI::to_score(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos) - << sync_endl; + if (Distributed::is_root()) + main_manager()->updates.onUpdateNoMoves( + {0, {rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos}}); } else { @@ -176,15 +216,15 @@ void Search::Worker::start_searching() { // until the GUI sends one of those commands. while (!threads.stop && (main_manager()->ponder || limits.infinite)) { - Cluster::signals_poll(threads); + Distributed::signals_poll(threads); } // 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). + // "ponderhit" just reset threads.ponder) threads.stop = true; // Signal and synchronize all other ranks - Cluster::signals_sync(threads); + Distributed::signals_sync(threads); // Wait until all threads have finished threads.wait_for_search_finished(); @@ -192,8 +232,8 @@ void Search::Worker::start_searching() { // 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(limits.inc[rootPos.side_to_move()] - - Cluster::nodes_searched(threads)); + main_manager()->tm.advance_nodes_time(Distributed::nodes_searched(threads) + - limits.inc[rootPos.side_to_move()]); Worker* bestThread = this; Skill skill = @@ -203,12 +243,21 @@ void Search::Worker::start_searching() { && rootMoves[0].pv[0] != Move::none()) bestThread = threads.get_best_thread()->worker.get(); - // Prepare PVLine and ponder move - std::string PVLine = main_manager()->pv(*bestThread, threads, tt, bestThread->completedDepth); - 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> 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); + Move bestMove = bestThread->rootMoves[0].pv[0]; Move ponderMove = Move::none(); if (bestThread->rootMoves[0].pv.size() > 1 @@ -216,26 +265,33 @@ void Search::Worker::start_searching() { ponderMove = bestThread->rootMoves[0].pv[1]; // Exchange info as needed - Cluster::MoveInfo mi{bestMove.raw(), ponderMove.raw(), bestThread->completedDepth, - bestThread->rootMoves[0].score, Cluster::rank()}; - Cluster::pick_moves(mi, PVLine); + 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(mi.score); - if (Cluster::is_root()) + if (Distributed::is_root()) { // Send again PV info if we have a new best thread/rank if (bestThread != this || mi.rank != 0) - sync_cout << PVLine << sync_endl; + { + for (const auto& serializedInfoOne : serializedInfo) + { + Search::InfoFull info = Search::InfoFull::unserialize(serializedInfoOne); + main_manager()->updates.onUpdateFull(info); + } + } bestMove = static_cast(mi.move); ponderMove = static_cast(mi.ponder); + std::string ponder; if (ponderMove != Move::none()) - sync_cout << "bestmove " << UCI::move(bestMove, rootPos.is_chess960()) << " ponder " - << UCI::move(ponderMove, rootPos.is_chess960()) << sync_endl; - else - sync_cout << "bestmove " << UCI::move(bestMove, rootPos.is_chess960()) << sync_endl; + 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); } } @@ -260,15 +316,16 @@ void Search::Worker::iterative_deepening() { // Allocate stack with extra size to allow access from (ss - 7) to (ss + 2): // (ss - 7) is needed for update_continuation_histories(ss - 1) which accesses (ss - 6), - // (ss + 2) is needed for initialization of cutOffCnt and killers. + // (ss + 2) is needed for initialization of cutOffCnt. Stack stack[MAX_PLY + 10] = {}; Stack* ss = stack + 7; for (int i = 7; i > 0; --i) { (ss - i)->continuationHistory = - &this->continuationHistory[0][0][NO_PIECE][0]; // Use as a sentinel - (ss - i)->staticEval = VALUE_NONE; + &continuationHistory[0][0][NO_PIECE][0]; // Use as a sentinel + (ss - i)->continuationCorrectionHistory = &continuationCorrectionHistory[NO_PIECE][0]; + (ss - i)->staticEval = VALUE_NONE; } for (int i = 0; i <= MAX_PLY + 2; ++i) @@ -296,9 +353,16 @@ void Search::Worker::iterative_deepening() { int searchAgainCounter = 0; + lowPlyHistory.fill(97); + + for (Color c : {WHITE, BLACK}) + for (int i = 0; i < UINT_16_HISTORY_SIZE; i++) + mainHistory[c][i] = + (mainHistory[c][i] - mainHistoryDefault) * 3 / 4 + mainHistoryDefault; + // Iterative deepening loop until requested to stop or the target depth is reached while (++rootDepth < MAX_PLY && !threads.stop - && !(limits.depth && mainThread && Cluster::is_root() && rootDepth > limits.depth)) + && !(limits.depth && mainThread && Distributed::is_root() && rootDepth > limits.depth)) { // Age out PV variability metric if (mainThread) @@ -316,7 +380,7 @@ void Search::Worker::iterative_deepening() { searchAgainCounter++; // MultiPV loop. We perform a full root search for each PV line - for (pvIdx = 0; pvIdx < multiPV && !threads.stop; ++pvIdx) + for (pvIdx = 0; pvIdx < multiPV; ++pvIdx) { if (pvIdx == pvLast) { @@ -330,13 +394,13 @@ void Search::Worker::iterative_deepening() { selDepth = 0; // Reset aspiration window starting size + delta = 5 + threadIdx % 8 + std::abs(rootMoves[pvIdx].meanSquaredScore) / 9000; Value avg = rootMoves[pvIdx].averageScore; - delta = 10 + avg * avg / 12493; alpha = std::max(avg - delta, -VALUE_INFINITE); beta = std::min(avg + delta, VALUE_INFINITE); - // Adjust optimism based on root move's averageScore (~4 Elo) - optimism[us] = 132 * avg / (std::abs(avg) + 89); + // Adjust optimism based on root move's averageScore + optimism[us] = 142 * avg / (std::abs(avg) + 91); optimism[~us] = -optimism[us]; // Start with a small aspiration window and, in the case of a fail @@ -345,10 +409,11 @@ void Search::Worker::iterative_deepening() { int failedHighCnt = 0; while (true) { - // Adjust the effective depth searched, but ensure at least one effective increment - // for every four searchAgain steps (see issue #2717). + // Adjust the effective depth searched, but ensure at least one + // effective increment for every four searchAgain steps (see issue #2717). Depth adjustedDepth = std::max(1, rootDepth - failedHighCnt - 3 * (searchAgainCounter + 1) / 4); + rootDelta = beta - alpha; bestValue = search(rootPos, ss, alpha, beta, adjustedDepth, false); // Bring the best move to the front. It is critical that sorting @@ -365,22 +430,21 @@ void Search::Worker::iterative_deepening() { if (threads.stop) break; - // When failing high/low give some update (without cluttering - // the UI) before a re-search. - if (Cluster::is_root() && mainThread && multiPV == 1 - && (bestValue <= alpha || bestValue >= beta) - && mainThread->tm.elapsed(Cluster::nodes_searched(threads)) > 3000) + // 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) { - sync_cout << main_manager()->pv(*this, threads, tt, rootDepth) << sync_endl; - Cluster::cluster_info(threads, rootDepth, - mainThread->tm.elapsed(Cluster::nodes_searched(threads))); + 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. + // In case of failing low/high increase aspiration window and re-search, + // otherwise exit the loop. if (bestValue <= alpha) { - beta = (alpha + beta) / 2; + beta = alpha; alpha = std::max(bestValue - delta, -VALUE_INFINITE); failedHighCnt = 0; @@ -389,7 +453,8 @@ void Search::Worker::iterative_deepening() { } else if (bestValue >= beta) { - beta = std::min(bestValue + delta, VALUE_INFINITE); + alpha = std::max(beta - delta, alpha); + beta = std::min(bestValue + delta, VALUE_INFINITE); ++failedHighCnt; } else @@ -403,19 +468,21 @@ 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 (Cluster::is_root() && mainThread - && (threads.stop || pvIdx + 1 == multiPV - || mainThread->tm.elapsed(Cluster::nodes_searched(threads)) > 3000) - // 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 if we would have - // had time to fully search other root-moves. Thus we suppress this output and - // below pick a proven score/PV for this thread (from the previous iteration). - && !(threads.abortedSearch && rootMoves[0].uciScore <= VALUE_TB_LOSS_IN_MAX_PLY)) + if (Distributed::is_root() && 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 + // if we would have had time to fully search other root-moves. Thus + // 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))) { - sync_cout << main_manager()->pv(*this, threads, tt, rootDepth) << sync_endl; - Cluster::cluster_info(threads, rootDepth, - mainThread->tm.elapsed(Cluster::nodes_searched(threads)) + 1); + main_manager()->pv(*this, threads, tt, rootDepth); + Distributed::cluster_info(threads, rootDepth, elapsed() + 1); } + + if (threads.stop) + break; } if (!threads.stop) @@ -424,7 +491,7 @@ void Search::Worker::iterative_deepening() { // We make sure not to pick an unproven mated-in score, // in case this thread prematurely stopped search (aborted-search). if (threads.abortedSearch && rootMoves[0].score != -VALUE_INFINITE - && rootMoves[0].score <= VALUE_TB_LOSS_IN_MAX_PLY) + && is_loss(rootMoves[0].score)) { // Bring the last best move to the front for best thread selection. Utility::move_to_front(rootMoves, [&lastBestPV = std::as_const(lastBestPV)]( @@ -456,7 +523,7 @@ void Search::Worker::iterative_deepening() { skill.pick_best(rootMoves, multiPV); // Use part of the gained time from a previous stable move for the current move - for (Thread* th : threads) + for (auto&& th : threads) { totBestMoveChanges += th->worker->bestMoveChanges; th->worker->bestMoveChanges = 0; @@ -465,33 +532,37 @@ void Search::Worker::iterative_deepening() { // Do we have time for the next iteration? Can we stop searching now? if (limits.use_time_management() && !threads.stop && !mainThread->stopOnPonderhit) { - int nodesEffort = rootMoves[0].effort * 100 / std::max(size_t(1), size_t(nodes)); + uint64_t nodesEffort = + rootMoves[0].effort * 100000 / std::max(size_t(1), size_t(nodes)); - double fallingEval = (1067 + 223 * (mainThread->bestPreviousAverageScore - bestValue) - + 97 * (mainThread->iterValue[iterIdx] - bestValue)) - / 10000.0; - fallingEval = std::clamp(fallingEval, 0.580, 1.667); + double fallingEval = (11.85 + 2.24 * (mainThread->bestPreviousAverageScore - bestValue) + + 0.93 * (mainThread->iterValue[iterIdx] - bestValue)) + / 100.0; + fallingEval = std::clamp(fallingEval, 0.57, 1.70); // If the bestMove is stable over several iterations, reduce time accordingly - timeReduction = lastBestMoveDepth + 8 < completedDepth ? 1.495 : 0.687; - double reduction = (1.48 + mainThread->previousTimeReduction) / (2.17 * timeReduction); - double bestMoveInstability = 1 + 1.88 * totBestMoveChanges / threads.size(); - int el = std::clamp((bestValue + 750) / 150, 0, 9); + double k = 0.51; + double center = lastBestMoveDepth + 12.15; + + timeReduction = 0.66 + 0.85 / (0.98 + std::exp(-k * (completedDepth - center))); + + double reduction = (1.43 + mainThread->previousTimeReduction) / (2.28 * timeReduction); + + double bestMoveInstability = 1.02 + 2.14 * totBestMoveChanges / threads.size(); + + double highBestMoveEffort = nodesEffort >= 93340 ? 0.76 : 1.0; double totalTime = mainThread->tm.optimum() * fallingEval * reduction - * bestMoveInstability * EvalLevel[el]; + * bestMoveInstability * highBestMoveEffort; // Cap used time in case of a single legal move for a better viewer experience if (rootMoves.size() == 1) - totalTime = std::min(500.0, totalTime); + totalTime = std::min(502.0, totalTime); - if (completedDepth >= 10 && nodesEffort >= 97 - && mainThread->tm.elapsed(Cluster::nodes_searched(threads)) > totalTime * 0.739 - && !mainThread->ponder) - threads.stop = true; + auto elapsedTime = elapsed(); - // Stop the search if we have exceeded the totalTime - if (mainThread->tm.elapsed(Cluster::nodes_searched(threads)) > totalTime) + // Stop the search if we have exceeded the totalTime or maximum + if (elapsedTime > std::min(totalTime, double(mainThread->tm.maximum()))) { // If we are allowed to ponder do not stop the search now but // keep pondering until the GUI sends "ponderhit" or "stop". @@ -501,9 +572,7 @@ void Search::Worker::iterative_deepening() { threads.stop = true; } else - threads.increaseDepth = - mainThread->ponder - || mainThread->tm.elapsed(Cluster::nodes_searched(threads)) <= totalTime * 0.506; + threads.increaseDepth = mainThread->ponder || elapsedTime <= totalTime * 0.50; } mainThread->iterValue[iterIdx] = bestValue; @@ -522,41 +591,93 @@ void Search::Worker::iterative_deepening() { skill.best ? skill.best : skill.pick_best(rootMoves, multiPV))); } + +void Search::Worker::do_move(Position& pos, const Move move, StateInfo& st, Stack* const ss) { + do_move(pos, move, st, pos.gives_check(move), ss); +} + +void Search::Worker::do_move( + Position& pos, const Move move, StateInfo& st, const bool givesCheck, Stack* const ss) { + bool capture = pos.capture_stage(move); + // Preferable over fetch_add to avoid locking instructions + nodes.store(nodes.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); + + auto [dirtyPiece, dirtyThreats] = accumulatorStack.push(); + pos.do_move(move, st, givesCheck, dirtyPiece, dirtyThreats, &tt, &sharedHistory); + + if (ss != nullptr) + { + ss->currentMove = move; + ss->continuationHistory = + &continuationHistory[ss->inCheck][capture][dirtyPiece.pc][move.to_sq()]; + ss->continuationCorrectionHistory = + &continuationCorrectionHistory[dirtyPiece.pc][move.to_sq()]; + } +} + +void Search::Worker::do_null_move(Position& pos, StateInfo& st, Stack* const ss) { + pos.do_null_move(st, tt); + ss->currentMove = Move::null(); + ss->continuationHistory = &continuationHistory[0][0][NO_PIECE][0]; + ss->continuationCorrectionHistory = &continuationCorrectionHistory[NO_PIECE][0]; +} + +void Search::Worker::undo_move(Position& pos, const Move move) { + pos.undo_move(move); + accumulatorStack.pop(); +} + +void Search::Worker::undo_null_move(Position& pos) { pos.undo_null_move(); } + + +// Reset histories, usually before a new game void Search::Worker::clear() { - counterMoves.fill(Move::none()); - mainHistory.fill(0); - captureHistory.fill(0); - pawnHistory.fill(0); - correctionHistory.fill(0); + mainHistory.fill(mainHistoryDefault); + captureHistory.fill(-689); + + // Each thread is responsible for clearing their part of shared history + sharedHistory.correctionHistory.clear_range(0, numaThreadIdx, numaTotal); + sharedHistory.pawnHistory.clear_range(-1238, numaThreadIdx, numaTotal); + + ttMoveHistory = 0; + + for (auto& to : continuationCorrectionHistory) + for (auto& h : to) + h.fill(8); for (bool inCheck : {false, true}) for (StatsType c : {NoCaptures, Captures}) for (auto& to : continuationHistory[inCheck][c]) for (auto& h : to) - h->fill(-67); + h.fill(-529); for (size_t i = 1; i < reductions.size(); ++i) - reductions[i] = int((19.80 + std::log(size_t(options["Threads"])) / 2) * std::log(i)); + reductions[i] = int(2747 / 128.0 * std::log(i)); + + refreshTable.clear(networks[numaAccessToken]); } -// Main search function for both PV and non-PV nodes. +// Main search function for both PV and non-PV nodes template Value Search::Worker::search( Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode) { constexpr bool PvNode = nodeType != NonPV; constexpr bool rootNode = nodeType == Root; + const bool allNode = !(PvNode || cutNode); // Dive into quiescence search when the depth reaches zero if (depth <= 0) - return qsearch < PvNode ? PV : NonPV > (pos, ss, alpha, beta); + return qsearch(pos, ss, alpha, beta); - // Check if we have an upcoming move that draws by repetition, or - // if the opponent had an alternative move earlier to this position. - if (!rootNode && alpha < VALUE_DRAW && pos.has_game_cycle(ss->ply)) + // Limit the depth if extensions made it too large + depth = std::min(depth, MAX_PLY - 1); + + // Check if we have an upcoming move that draws by repetition + if (!rootNode && alpha < VALUE_DRAW && pos.upcoming_repetition(ss->ply)) { - alpha = value_draw(this->nodes); + alpha = value_draw(nodes); if (alpha >= beta) return alpha; } @@ -566,45 +687,43 @@ Value Search::Worker::search( assert(0 < depth && depth < MAX_PLY); assert(!(PvNode && cutNode)); - Move pv[MAX_PLY + 1], capturesSearched[32], quietsSearched[32]; + Move pv[MAX_PLY + 1]; StateInfo st; - ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); - TTEntry* tte; - Key posKey; - Move ttMove, move, excludedMove, bestMove; - Depth extension, newDepth; - Value bestValue, value, ttValue, eval, maxValue, probCutBeta; - bool givesCheck, improving, priorCapture, opponentWorsening; - bool capture, moveCountPruning, ttCapture; - Piece movedPiece; - int moveCount, captureCount, quietCount; + Key posKey; + Move move, excludedMove, bestMove; + Depth extension, newDepth; + Value bestValue, value, eval, maxValue, probCutBeta; + bool givesCheck, improving, priorCapture, opponentWorsening; + bool capture, ttCapture; + int priorReduction; + Piece movedPiece; + + SearchedList capturesSearched; + SearchedList quietsSearched; // Step 1. Initialize node - Worker* thisThread = this; - ss->inCheck = pos.checkers(); - priorCapture = pos.captured_piece(); - Color us = pos.side_to_move(); - moveCount = captureCount = quietCount = ss->moveCount = 0; - bestValue = -VALUE_INFINITE; - maxValue = VALUE_INFINITE; + ss->inCheck = pos.checkers(); + priorCapture = pos.captured_piece(); + Color us = pos.side_to_move(); + ss->moveCount = 0; + bestValue = -VALUE_INFINITE; + maxValue = VALUE_INFINITE; // Check for the available remaining time if (is_mainthread()) - main_manager()->check_time(*thisThread); + main_manager()->check_time(*this); // Used to send selDepth info to GUI (selDepth counts from 1, ply from 0) - if (PvNode && thisThread->selDepth < ss->ply + 1) - thisThread->selDepth = ss->ply + 1; + if (PvNode && selDepth < ss->ply + 1) + selDepth = ss->ply + 1; if (!rootNode) { // Step 2. Check for aborted search and immediate draw if (threads.stop.load(std::memory_order_relaxed) || pos.is_draw(ss->ply) || ss->ply >= MAX_PLY) - return (ss->ply >= MAX_PLY && !ss->inCheck) - ? evaluate(networks, pos, thisThread->optimism[us]) - : value_draw(thisThread->nodes); + return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos) : value_draw(nodes); // Step 3. Mate distance pruning. Even if we mate at the next move our score // would be at best mate_in(ss->ply + 1), but if alpha is already bigger because @@ -617,58 +736,115 @@ Value Search::Worker::search( if (alpha >= beta) return alpha; } - else - thisThread->rootDelta = beta - alpha; assert(0 <= ss->ply && ss->ply < MAX_PLY); - (ss + 1)->excludedMove = bestMove = Move::none(); - (ss + 2)->killers[0] = (ss + 2)->killers[1] = Move::none(); - (ss + 2)->cutoffCnt = 0; - ss->multipleExtensions = (ss - 1)->multipleExtensions; - Square prevSq = ((ss - 1)->currentMove).is_ok() ? ((ss - 1)->currentMove).to_sq() : SQ_NONE; - ss->statScore = 0; + Square prevSq = ((ss - 1)->currentMove).is_ok() ? ((ss - 1)->currentMove).to_sq() : SQ_NONE; + bestMove = Move::none(); + priorReduction = (ss - 1)->reduction; + (ss - 1)->reduction = 0; + ss->statScore = 0; + (ss + 2)->cutoffCnt = 0; - // Step 4. Transposition table lookup. - excludedMove = ss->excludedMove; - posKey = pos.key(); - tte = tt.probe(posKey, ss->ttHit); - ttValue = ss->ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; - ttMove = rootNode ? thisThread->rootMoves[thisThread->pvIdx].pv[0] - : ss->ttHit ? tte->move() - : Move::none(); - ttCapture = ttMove && pos.capture_stage(ttMove); + // Step 4. Transposition table lookup + excludedMove = ss->excludedMove; + posKey = pos.key(); + auto [ttHit, ttData, ttWriter] = tt.probe(posKey); + // Need further processing of the saved data + ss->ttHit = ttHit; + ttData.move = rootNode ? rootMoves[pvIdx].pv[0] : ttHit ? ttData.move : Move::none(); + ttData.value = ttHit ? value_from_tt(ttData.value, ss->ply, pos.rule50_count()) : VALUE_NONE; + ss->ttPv = excludedMove ? ss->ttPv : PvNode || (ttHit && ttData.is_pv); + ttCapture = ttData.move && pos.capture_stage(ttData.move); - // At this point, if excluded, skip straight to step 6, static eval. However, - // to save indentation, we list the condition in all code between here and there. - if (!excludedMove) - ss->ttPv = PvNode || (ss->ttHit && tte->is_pv()); + // Step 6. Static evaluation of the position + Value unadjustedStaticEval = VALUE_NONE; + const auto correctionValue = correction_value(*this, pos, ss); + // Skip early pruning when in check + if (ss->inCheck) + ss->staticEval = eval = (ss - 2)->staticEval; + else if (excludedMove) + unadjustedStaticEval = eval = ss->staticEval; + else if (ss->ttHit) + { + // Never assume anything about values stored in TT + unadjustedStaticEval = ttData.eval; + if (!is_valid(unadjustedStaticEval)) + unadjustedStaticEval = evaluate(pos); + + ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, correctionValue); + + // ttValue can be used as a better position evaluation + if (is_valid(ttData.value) + && (ttData.bound & (ttData.value > eval ? BOUND_LOWER : BOUND_UPPER))) + eval = ttData.value; + } + else + { + unadjustedStaticEval = evaluate(pos); + 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()); + } + + // Set up the improving flag, which is true if current static evaluation is + // bigger than the previous static evaluation at our turn (if we were in + // check at our previous move we go back until we weren't in check) and is + // false otherwise. The improving flag is used in various pruning heuristics. + // Similarly, opponentWorsening is true if our static evaluation is better + // for us than at the last ply. + improving = ss->staticEval > (ss - 2)->staticEval; + opponentWorsening = ss->staticEval > -(ss - 1)->staticEval; + + // Hindsight adjustment of reductions based on static evaluation difference. + if (priorReduction >= 3 && !opponentWorsening) + depth++; + if (priorReduction >= 2 && depth >= 2 && ss->staticEval + (ss - 1)->staticEval > 173) + depth--; // At non-PV nodes we check for an early TT cutoff - if (!PvNode && !excludedMove && tte->depth() > depth - && ttValue != VALUE_NONE // Possible in case of TT access race or if !ttHit - && (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER))) + if (!PvNode && !excludedMove && ttData.depth > depth - (ttData.value <= beta) + && is_valid(ttData.value) // Can happen when !ttHit or when access race in probe() + && (ttData.bound & (ttData.value >= beta ? BOUND_LOWER : BOUND_UPPER)) + && (cutNode == (ttData.value >= beta) || depth > 5)) { - // If ttMove is quiet, update move sorting heuristics on TT hit (~2 Elo) - if (ttMove && ttValue >= beta) + // If ttMove is quiet, update move sorting heuristics on TT hit + if (ttData.move && ttData.value >= beta) { - // Bonus for a quiet ttMove that fails high (~2 Elo) + // Bonus for a quiet ttMove that fails high if (!ttCapture) - update_quiet_stats(pos, ss, *this, ttMove, stat_bonus(depth)); + update_quiet_histories(pos, ss, *this, ttData.move, + std::min(132 * depth - 72, 985)); - // Extra penalty for early quiet moves of - // the previous ply (~1 Elo on STC, ~2 Elo on LTC) - if (prevSq != SQ_NONE && (ss - 1)->moveCount <= 2 && !priorCapture) - update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, - -stat_malus(depth + 1)); + // Extra penalty for early quiet moves of the previous ply + if (prevSq != SQ_NONE && (ss - 1)->moveCount < 4 && !priorCapture) + update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -2060); } // Partial workaround for the graph history interaction problem // For high rule50 counts don't produce transposition table cutoffs. - if (pos.rule50_count() < 90) - return ttValue >= beta && std::abs(ttValue) < VALUE_TB_WIN_IN_MAX_PLY - ? (ttValue * 3 + beta) / 4 - : ttValue; + if (pos.rule50_count() < 96) + { + if (depth >= 8 && ttData.move && pos.pseudo_legal(ttData.move) && pos.legal(ttData.move) + && !is_decisive(ttData.value)) + { + pos.do_move(ttData.move, st); + Key nextPosKey = pos.key(); + auto [ttHitNext, ttDataNext, ttWriterNext] = tt.probe(nextPosKey); + pos.undo_move(ttData.move); + + // Check that the ttValue after the tt move would also trigger a cutoff + if (!is_valid(ttDataNext.value)) + return ttData.value; + + if ((ttData.value >= beta) == (-ttDataNext.value >= beta)) + return ttData.value; + } + else + return ttData.value; + } } // Step 5. Tablebases probe @@ -689,13 +865,14 @@ Value Search::Worker::search( if (err != TB::ProbeState::FAIL) { - thisThread->tbHits.fetch_add(1, std::memory_order_relaxed); + // Preferable over fetch_add to avoid locking instructions + tbHits.store(tbHits.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); int drawScore = tbConfig.useRule50 ? 1 : 0; Value tbValue = VALUE_TB - ss->ply; - // use the range VALUE_TB to VALUE_TB_WIN_IN_MAX_PLY to score + // Use the range VALUE_TB to VALUE_TB_WIN_IN_MAX_PLY to score value = wdl < -drawScore ? -tbValue : wdl > drawScore ? tbValue : VALUE_DRAW + 2 * wdl * drawScore; @@ -706,9 +883,9 @@ Value Search::Worker::search( if (b == BOUND_EXACT || (b == BOUND_LOWER ? value >= beta : value <= alpha)) { - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(value, ss->ply), - ss->ttPv, b, std::min(MAX_PLY - 1, depth + 6), Move::none(), - VALUE_NONE, tt.generation()); + 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()); return value; } @@ -724,228 +901,157 @@ Value Search::Worker::search( } } - // Step 6. Static evaluation of the position - Value unadjustedStaticEval = VALUE_NONE; if (ss->inCheck) - { - // Skip early pruning when in check - ss->staticEval = eval = VALUE_NONE; - improving = false; goto moves_loop; - } - else if (excludedMove) - { - // Providing the hint that this node's accumulator will be used often - // brings significant Elo gain (~13 Elo). - Eval::NNUE::hint_common_parent_position(pos, networks); - unadjustedStaticEval = eval = ss->staticEval; - } - else if (ss->ttHit) - { - // Never assume anything about values stored in TT - unadjustedStaticEval = tte->eval(); - if (unadjustedStaticEval == VALUE_NONE) - unadjustedStaticEval = evaluate(networks, pos, thisThread->optimism[us]); - else if (PvNode) - Eval::NNUE::hint_common_parent_position(pos, networks); - ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, *thisThread, pos); - - // ttValue can be used as a better position evaluation (~7 Elo) - if (ttValue != VALUE_NONE && (tte->bound() & (ttValue > eval ? BOUND_LOWER : BOUND_UPPER))) - eval = ttValue; - } - else - { - unadjustedStaticEval = evaluate(networks, pos, thisThread->optimism[us]); - ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, *thisThread, pos); - - // Static evaluation is saved as it was before adjustment by correction history - Cluster::save(tt, threads, thisThread, tte, posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, - DEPTH_NONE, Move::none(), unadjustedStaticEval, tt.generation()); - } - - // Use static evaluation difference to improve quiet move ordering (~9 Elo) + // Use static evaluation difference to improve quiet move ordering if (((ss - 1)->currentMove).is_ok() && !(ss - 1)->inCheck && !priorCapture) { - int bonus = std::clamp(-13 * int((ss - 1)->staticEval + ss->staticEval), -1578, 1291); - bonus = bonus > 0 ? 2 * bonus : bonus / 2; - thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << bonus; - if (type_of(pos.piece_on(prevSq)) != PAWN && ((ss - 1)->currentMove).type_of() != PROMOTION) - thisThread->pawnHistory[pawn_structure_index(pos)][pos.piece_on(prevSq)][prevSq] - << bonus / 2; + int evalDiff = std::clamp(-int((ss - 1)->staticEval + ss->staticEval), -209, 167) + 59; + mainHistory[~us][((ss - 1)->currentMove).raw()] << evalDiff * 9; + if (!ttHit && type_of(pos.piece_on(prevSq)) != PAWN + && ((ss - 1)->currentMove).type_of() != PROMOTION) + sharedHistory.pawn_entry(pos)[pos.piece_on(prevSq)][prevSq] << evalDiff * 13; } - // Set up the improving flag, which is true if current static evaluation is - // bigger than the previous static evaluation at our turn (if we were in - // check at our previous move we look at static evaluation at move prior to it - // and if we were in check at move prior to it flag is set to true) and is - // false otherwise. The improving flag is used in various pruning heuristics. - improving = (ss - 2)->staticEval != VALUE_NONE - ? ss->staticEval > (ss - 2)->staticEval - : (ss - 4)->staticEval != VALUE_NONE && ss->staticEval > (ss - 4)->staticEval; - opponentWorsening = ss->staticEval + (ss - 1)->staticEval > 2; + // Step 7. Razoring + // If eval is really low, skip search entirely and return the qsearch value. + // For PvNodes, we must have a guard against mates being returned. + if (!PvNode && eval < alpha - 485 - 281 * depth * depth) + return qsearch(pos, ss, alpha, beta); - // Step 7. Razoring (~1 Elo) - // If eval is really low check with qsearch if it can exceed alpha, if it can't, - // return a fail low. - // Adjust razor margin according to cutoffCnt. (~1 Elo) - if (eval < alpha - 488 - (289 - 142 * ((ss + 1)->cutoffCnt > 3)) * depth * depth) - { - value = qsearch(pos, ss, alpha - 1, alpha); - if (value < alpha) - return value; - } - - // Step 8. Futility pruning: child node (~40 Elo) + // Step 8. Futility pruning: child node // The depth condition is important for mate finding. - if (!ss->ttPv && depth < 12 - && eval - futility_margin(depth, cutNode && !ss->ttHit, improving, opponentWorsening) - - (ss - 1)->statScore / 267 - >= beta - && eval >= beta && eval < VALUE_TB_WIN_IN_MAX_PLY && (!ttMove || ttCapture)) - return beta > VALUE_TB_LOSS_IN_MAX_PLY ? (eval + beta) / 2 : eval; - - // Step 9. Null move search with verification search (~35 Elo) - if (!PvNode && (ss - 1)->currentMove != Move::null() && (ss - 1)->statScore < 16878 - && eval >= beta && ss->staticEval >= beta - 20 * depth + 314 && !excludedMove - && pos.non_pawn_material(us) && ss->ply >= thisThread->nmpMinPly - && beta > VALUE_TB_LOSS_IN_MAX_PLY) { - assert(eval - beta >= 0); + auto futility_margin = [&](Depth d) { + Value futilityMult = 76 - 23 * !ss->ttHit; - // Null move dynamic reduction based on depth and eval - Depth R = std::min(int(eval - beta) / 144, 6) + depth / 3 + 4; + return futilityMult * d + - (2474 * improving + 331 * opponentWorsening) * futilityMult / 1024 // + + std::abs(correctionValue) / 174665; + }; - ss->currentMove = Move::null(); - ss->continuationHistory = &thisThread->continuationHistory[0][0][NO_PIECE][0]; + if (!ss->ttPv && depth < 14 && eval - futility_margin(depth) >= beta && eval >= beta + && (!ttData.move || ttCapture) && !is_loss(beta) && !is_win(eval)) + return (2 * beta + eval) / 3; + } - pos.do_null_move(st, tt); + // Step 9. Null move search with verification search + if (cutNode && ss->staticEval >= beta - 18 * depth + 350 && !excludedMove + && pos.non_pawn_material(us) && ss->ply >= nmpMinPly && !is_loss(beta)) + { + assert((ss - 1)->currentMove != Move::null()); - Value nullValue = -search(pos, ss + 1, -beta, -beta + 1, depth - R, !cutNode); + // Null move dynamic reduction based on depth + Depth R = 7 + depth / 3; + do_null_move(pos, st, ss); - pos.undo_null_move(); + Value nullValue = -search(pos, ss + 1, -beta, -beta + 1, depth - R, false); + + undo_null_move(pos); // Do not return unproven mate or TB scores - if (nullValue >= beta && nullValue < VALUE_TB_WIN_IN_MAX_PLY) + if (nullValue >= beta && !is_win(nullValue)) { - if (thisThread->nmpMinPly || depth < 16) + if (nmpMinPly || depth < 16) return nullValue; - assert(!thisThread->nmpMinPly); // Recursive verification is not allowed + assert(!nmpMinPly); // Recursive verification is not allowed // Do verification search at high depths, with null move pruning disabled // until ply exceeds nmpMinPly. - thisThread->nmpMinPly = ss->ply + 3 * (depth - R) / 4; + nmpMinPly = ss->ply + 3 * (depth - R) / 4; Value v = search(pos, ss, beta - 1, beta, depth - R, false); - thisThread->nmpMinPly = 0; + nmpMinPly = 0; if (v >= beta) return nullValue; } } - // Step 10. Internal iterative reductions (~9 Elo) - // For PV nodes without a ttMove, we decrease depth by 3. - if (PvNode && !ttMove) - depth -= 3; + improving |= ss->staticEval >= beta; - // Use qsearch if depth <= 0. - if (depth <= 0) - return qsearch(pos, ss, alpha, beta); + // Step 10. Internal iterative reductions + // At sufficient depth, reduce depth for PV/Cut nodes without a TTMove. + // (*Scaler) Making IIR more aggressive scales poorly. + if (!allNode && depth >= 6 && !ttData.move && priorReduction <= 3) + depth--; - // For cutNodes without a ttMove, we decrease depth by 2 if depth is high enough. - if (cutNode && depth >= 8 && !ttMove) - depth -= 2; - - // Step 11. ProbCut (~10 Elo) - // If we have a good enough capture (or queen promotion) and a reduced search returns a value - // much above beta, we can (almost) safely prune the previous move. - probCutBeta = beta + 170 - 64 * improving; - if ( - !PvNode && depth > 3 - && std::abs(beta) < VALUE_TB_WIN_IN_MAX_PLY - // If value from transposition table is lower than probCutBeta, don't attempt probCut - // there and in further interactions with transposition table cutoff depth is set to depth - 3 - // because probCut search has depth set to depth - 4 but we also do a move before it - // So effective depth is equal to depth - 3 - && !(tte->depth() >= depth - 3 && ttValue != VALUE_NONE && ttValue < probCutBeta)) + // Step 11. ProbCut + // If we have a good enough capture (or queen promotion) and a reduced search + // returns a value much above beta, we can (almost) safely prune the previous move. + probCutBeta = beta + 235 - 63 * improving; + if (depth >= 3 + && !is_decisive(beta) + // If value from transposition table is lower than probCutBeta, don't attempt + // probCut there + && !(is_valid(ttData.value) && ttData.value < probCutBeta)) { assert(probCutBeta < VALUE_INFINITE && probCutBeta > beta); - MovePicker mp(pos, ttMove, probCutBeta - ss->staticEval, &thisThread->captureHistory); + MovePicker mp(pos, ttData.move, probCutBeta - ss->staticEval, &captureHistory); + Depth probCutDepth = std::clamp(depth - 5 - (ss->staticEval - beta) / 315, 0, depth); while ((move = mp.next_move()) != Move::none()) - if (move != excludedMove && pos.legal(move)) + { + assert(move.is_ok()); + + if (move == excludedMove || !pos.legal(move)) + continue; + + assert(pos.capture_stage(move)); + + do_move(pos, move, st, ss); + + // Perform a preliminary qsearch to verify that the move holds + value = -qsearch(pos, ss + 1, -probCutBeta, -probCutBeta + 1); + + // If the qsearch held, perform the regular search + if (value >= probCutBeta && probCutDepth > 0) + value = -search(pos, ss + 1, -probCutBeta, -probCutBeta + 1, probCutDepth, + !cutNode); + + undo_move(pos, move); + + if (value >= probCutBeta) { - assert(pos.capture_stage(move)); + // 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()); - // Prefetch the TT entry for the resulting position - prefetch(tt.first_entry(pos.key_after(move))); - - ss->currentMove = move; - ss->continuationHistory = - &this - ->continuationHistory[ss->inCheck][true][pos.moved_piece(move)][move.to_sq()]; - - thisThread->nodes.fetch_add(1, std::memory_order_relaxed); - pos.do_move(move, st); - - // Perform a preliminary qsearch to verify that the move holds - value = -qsearch(pos, ss + 1, -probCutBeta, -probCutBeta + 1); - - // If the qsearch held, perform the regular search - if (value >= probCutBeta) - value = -search(pos, ss + 1, -probCutBeta, -probCutBeta + 1, depth - 4, - !cutNode); - - pos.undo_move(move); - - if (value >= probCutBeta) - { - // Save ProbCut data into transposition table - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(value, ss->ply), - ss->ttPv, BOUND_LOWER, depth - 3, move, unadjustedStaticEval, - tt.generation()); - return std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY ? value - (probCutBeta - beta) - : value; - } + if (!is_decisive(value)) + return value - (probCutBeta - beta); } - - Eval::NNUE::hint_common_parent_position(pos, networks); + } } moves_loop: // When in check, search starts here - // Step 12. A small Probcut idea, when we are in check (~4 Elo) - probCutBeta = beta + 409; - if (ss->inCheck && !PvNode && ttCapture && (tte->bound() & BOUND_LOWER) - && tte->depth() >= depth - 4 && ttValue >= probCutBeta - && std::abs(ttValue) < VALUE_TB_WIN_IN_MAX_PLY && std::abs(beta) < VALUE_TB_WIN_IN_MAX_PLY) + // Step 12. A small Probcut idea + probCutBeta = beta + 418; + if ((ttData.bound & BOUND_LOWER) && ttData.depth >= depth - 4 && ttData.value >= probCutBeta + && !is_decisive(beta) && is_valid(ttData.value) && !is_decisive(ttData.value)) return probCutBeta; - const PieceToHistory* contHist[] = {(ss - 1)->continuationHistory, - (ss - 2)->continuationHistory, - (ss - 3)->continuationHistory, - (ss - 4)->continuationHistory, - nullptr, - (ss - 6)->continuationHistory}; + const PieceToHistory* contHist[] = { + (ss - 1)->continuationHistory, (ss - 2)->continuationHistory, (ss - 3)->continuationHistory, + (ss - 4)->continuationHistory, (ss - 5)->continuationHistory, (ss - 6)->continuationHistory}; - Move countermove = - prevSq != SQ_NONE ? thisThread->counterMoves[pos.piece_on(prevSq)][prevSq] : Move::none(); - MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &thisThread->captureHistory, - contHist, &thisThread->pawnHistory, countermove, ss->killers); + MovePicker mp(pos, ttData.move, depth, &mainHistory, &lowPlyHistory, &captureHistory, contHist, + &sharedHistory, ss->ply); - value = bestValue; - moveCountPruning = false; + value = bestValue; + + int moveCount = 0; // Step 13. Loop through all pseudo-legal moves until no moves remain // or a beta cutoff occurs. - while ((move = mp.next_move(moveCountPruning)) != Move::none()) + while ((move = mp.next_move()) != Move::none()) { assert(move.is_ok()); @@ -959,18 +1065,16 @@ moves_loop: // When in check, search starts here // At root obey the "searchmoves" option and skip moves not listed in Root // Move List. In MultiPV mode we also skip PV moves that have been already // searched and those of lower "TB rank" if we are in a TB root position. - if (rootNode - && !std::count(thisThread->rootMoves.begin() + thisThread->pvIdx, - thisThread->rootMoves.begin() + thisThread->pvLast, move)) + if (rootNode && !std::count(rootMoves.begin() + pvIdx, rootMoves.begin() + pvLast, move)) continue; ss->moveCount = ++moveCount; - if (rootNode && Cluster::is_root() && is_mainthread() - && main_manager()->tm.elapsed(Cluster::nodes_searched(threads)) > 3000) - sync_cout << "info depth " << depth << " currmove " - << UCI::move(move, pos.is_chess960()) << " currmovenumber " - << moveCount + thisThread->pvIdx << sync_endl; + if (rootNode && Distributed::is_root() && is_mainthread() && nodes > 10000000) + { + main_manager()->updates.onIter( + {depth, UCIEngine::move(move, pos.is_chess960()), moveCount + pvIdx}); + } if (PvNode) (ss + 1)->pv = nullptr; @@ -986,242 +1090,236 @@ moves_loop: // When in check, search starts here Depth r = reduction(improving, depth, moveCount, delta); - // Step 14. Pruning at shallow depth (~120 Elo). + // Increase reduction for ttPv nodes (*Scaler) + // Larger values scale well + if (ss->ttPv) + r += 946; + + // Step 14. Pruning at shallow depths. // Depth conditions are important for mate finding. - if (!rootNode && pos.non_pawn_material(us) && bestValue > VALUE_TB_LOSS_IN_MAX_PLY) + if (!rootNode && pos.non_pawn_material(us) && !is_loss(bestValue)) { - // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold (~8 Elo) - if (!moveCountPruning) - moveCountPruning = moveCount >= futility_move_count(improving, depth); + // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold + if (moveCount >= (3 + depth * depth) / (2 - improving)) + mp.skip_quiet_moves(); // Reduced depth of the next LMR search - int lmrDepth = newDepth - r; + int lmrDepth = newDepth - r / 1024; if (capture || givesCheck) { - // Futility pruning for captures (~2 Elo) - if (!givesCheck && lmrDepth < 7 && !ss->inCheck) + Piece capturedPiece = pos.piece_on(move.to_sq()); + int captHist = captureHistory[movedPiece][move.to_sq()][type_of(capturedPiece)]; + + // Futility pruning for captures + if (!givesCheck && lmrDepth < 7) { - Piece capturedPiece = pos.piece_on(move.to_sq()); - int futilityEval = - ss->staticEval + 297 + 284 * lmrDepth + PieceValue[capturedPiece] - + thisThread->captureHistory[movedPiece][move.to_sq()][type_of(capturedPiece)] - / 7; - if (futilityEval < alpha) + Value futilityValue = ss->staticEval + 232 + 217 * lmrDepth + + PieceValue[capturedPiece] + 131 * captHist / 1024; + + if (futilityValue <= alpha) continue; } - // SEE based pruning for captures and checks (~11 Elo) - if (!pos.see_ge(move, -203 * depth)) + // SEE based pruning for captures and checks + // Avoid pruning sacrifices of our last piece for stalemate + int margin = std::max(166 * depth + captHist / 29, 0); + if ((alpha >= VALUE_DRAW || pos.non_pawn_material(us) != PieceValue[movedPiece]) + && !pos.see_ge(move, -margin)) continue; } else { - int history = - (*contHist[0])[movedPiece][move.to_sq()] - + (*contHist[1])[movedPiece][move.to_sq()] - + (*contHist[3])[movedPiece][move.to_sq()] - + thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][move.to_sq()]; + int history = (*contHist[0])[movedPiece][move.to_sq()] + + (*contHist[1])[movedPiece][move.to_sq()] + + sharedHistory.pawn_entry(pos)[movedPiece][move.to_sq()]; - // Continuation history based pruning (~2 Elo) - if (lmrDepth < 6 && history < -4040 * depth) + // Continuation history based pruning + if (history < -4083 * depth) continue; - history += 2 * thisThread->mainHistory[us][move.from_to()]; + history += 69 * mainHistory[us][move.raw()] / 32; - lmrDepth += history / 5637; + // (*Scaler): Generally, lower divisors scales well + lmrDepth += history / 3208; - Value futilityValue = - ss->staticEval + (bestValue < ss->staticEval - 59 ? 141 : 58) + 125 * lmrDepth; + Value futilityValue = ss->staticEval + 42 + 161 * !bestMove + 127 * lmrDepth + + 85 * (ss->staticEval > alpha); - // Futility pruning: parent node (~13 Elo) - if (!ss->inCheck && lmrDepth < 15 && futilityValue <= alpha) + // Futility pruning: parent node + // (*Scaler): Generally, more frequent futility pruning + // scales well + if (!ss->inCheck && lmrDepth < 13 && futilityValue <= alpha) { - if (bestValue <= futilityValue && std::abs(bestValue) < VALUE_TB_WIN_IN_MAX_PLY - && futilityValue < VALUE_TB_WIN_IN_MAX_PLY) - bestValue = (bestValue + futilityValue * 3) / 4; + if (bestValue <= futilityValue && !is_decisive(bestValue) + && !is_win(futilityValue)) + bestValue = futilityValue; continue; } lmrDepth = std::max(lmrDepth, 0); - // Prune moves with negative SEE (~4 Elo) - if (!pos.see_ge(move, -27 * lmrDepth * lmrDepth)) + // Prune moves with negative SEE + if (!pos.see_ge(move, -25 * lmrDepth * lmrDepth)) continue; } } - // Step 15. Extensions (~100 Elo) - // We take care to not overdo to avoid search getting stuck. - if (ss->ply < thisThread->rootDepth * 2) + // Step 15. Extensions + // Singular extension search. If all moves but one + // fail low on a search of (alpha-s, beta-s), and just one fails high on + // (alpha, beta), then that move is singular and should be extended. To + // verify this we do a reduced search on the position excluding the ttMove + // and if the result is lower than ttValue minus a margin, then we will + // extend the ttMove. Recursive singular search is avoided. + + // (*Scaler) Generally, higher singularBeta (i.e closer to ttValue) + // and lower extension margins scale well. + if (!rootNode && move == ttData.move && !excludedMove && depth >= 6 + ss->ttPv + && is_valid(ttData.value) && !is_decisive(ttData.value) && (ttData.bound & BOUND_LOWER) + && ttData.depth >= depth - 3 && !is_shuffling(move, ss, pos)) { - // Singular extension search (~94 Elo). If all moves but one fail low on a - // search of (alpha-s, beta-s), and just one fails high on (alpha, beta), - // then that move is singular and should be extended. To verify this we do - // a reduced search on the position excluding the ttMove and if the result - // is lower than ttValue minus a margin, then we will extend the ttMove. + Value singularBeta = ttData.value - (53 + 75 * (ss->ttPv && !PvNode)) * depth / 60; + Depth singularDepth = newDepth / 2; - // Note: the depth margin and singularBeta margin are known for having non-linear - // scaling. Their values are optimized to time controls of 180+1.8 and longer - // so changing them requires tests at these types of time controls. - // Recursive singular search is avoided. - if (!rootNode && move == ttMove && !excludedMove - && depth >= 4 - (thisThread->completedDepth > 30) + ss->ttPv - && std::abs(ttValue) < VALUE_TB_WIN_IN_MAX_PLY && (tte->bound() & BOUND_LOWER) - && tte->depth() >= depth - 3) + ss->excludedMove = move; + value = search(pos, ss, singularBeta - 1, singularBeta, singularDepth, cutNode); + ss->excludedMove = Move::none(); + + if (value < singularBeta) { - Value singularBeta = ttValue - (58 + 58 * (ss->ttPv && !PvNode)) * depth / 64; - Depth singularDepth = newDepth / 2; + int corrValAdj = std::abs(correctionValue) / 230673; + int doubleMargin = -4 + 199 * PvNode - 201 * !ttCapture - corrValAdj + - 897 * ttMoveHistory / 127649 - (ss->ply > rootDepth) * 42; + int tripleMargin = 73 + 302 * PvNode - 248 * !ttCapture + 90 * ss->ttPv - corrValAdj + - (ss->ply * 2 > rootDepth * 3) * 50; - ss->excludedMove = move; - value = - search(pos, ss, singularBeta - 1, singularBeta, singularDepth, cutNode); - ss->excludedMove = Move::none(); + extension = + 1 + (value < singularBeta - doubleMargin) + (value < singularBeta - tripleMargin); - if (value < singularBeta) - { - extension = 1; - - // We make sure to limit the extensions in some way to avoid a search explosion - if (!PvNode && ss->multipleExtensions <= 16) - { - extension = 2 + (value < singularBeta - 22 && !ttCapture); - depth += depth < 14; - } - if (PvNode && !ttCapture && ss->multipleExtensions <= 5 - && value < singularBeta - 37) - extension = 2; - } - - // Multi-cut pruning - // Our ttMove is assumed to fail high based on the bound of the TT entry, - // and if after excluding the ttMove with a reduced search we fail high over the original beta, - // we assume this expected cut-node is not singular (multiple moves fail high), - // and we can prune the whole subtree by returning a softbound. - else if (singularBeta >= beta) - return singularBeta; - - // Negative extensions - // If other moves failed high over (ttValue - margin) without the ttMove on a reduced search, - // but we cannot do multi-cut because (ttValue - margin) is lower than the original beta, - // we do not know if the ttMove is singular or can do a multi-cut, - // so we reduce the ttMove in favor of other moves based on some conditions: - - // If the ttMove is assumed to fail high over current beta (~7 Elo) - else if (ttValue >= beta) - extension = -3; - - // If we are on a cutNode but the ttMove is not assumed to fail high over current beta (~1 Elo) - else if (cutNode) - extension = -2; - - // If the ttMove is assumed to fail low over the value of the reduced search (~1 Elo) - else if (ttValue <= value) - extension = -1; + depth++; } - // Recapture extensions (~0 Elo on STC, ~1 Elo on LTC) - else if (PvNode && move == ttMove && move.to_sq() == prevSq - && thisThread->captureHistory[movedPiece][move.to_sq()] - [type_of(pos.piece_on(move.to_sq()))] - > 4026) - extension = 1; + // Multi-cut pruning + // Our ttMove is assumed to fail high based on the bound of the TT entry, + // and if after excluding the ttMove with a reduced search we fail high + // over the original beta, we assume this expected cut-node is not + // singular (multiple moves fail high), and we can prune the whole + // subtree by returning a softbound. + else if (value >= beta && !is_decisive(value)) + { + ttMoveHistory << std::max(-400 - 100 * depth, -4000); + return value; + } + + // Negative extensions + // If other moves failed high over (ttValue - margin) without the + // ttMove on a reduced search, but we cannot do multi-cut because + // (ttValue - margin) is lower than the original beta, we do not know + // if the ttMove is singular or can do a multi-cut, so we reduce the + // ttMove in favor of other moves based on some conditions: + + // If the ttMove is assumed to fail high over current beta + else if (ttData.value >= beta) + extension = -3; + + // If we are on a cutNode but the ttMove is not assumed to fail high + // over current beta + else if (cutNode) + extension = -2; } + // Step 16. Make the move + do_move(pos, move, st, givesCheck, ss); + // Add extension to new depth newDepth += extension; - ss->multipleExtensions = (ss - 1)->multipleExtensions + (extension >= 2); - - // Speculative prefetch as early as possible - prefetch(tt.first_entry(pos.key_after(move))); - - // Update the current move (this must be done after singular extension search) - ss->currentMove = move; - ss->continuationHistory = - &thisThread->continuationHistory[ss->inCheck][capture][movedPiece][move.to_sq()]; - uint64_t nodeCount = rootNode ? uint64_t(nodes) : 0; - // Step 16. Make the move - thisThread->nodes.fetch_add(1, std::memory_order_relaxed); - pos.do_move(move, st, givesCheck); - - // Decrease reduction if position is or has been on the PV (~7 Elo) + // Decrease reduction for PvNodes (*Scaler) if (ss->ttPv) - r -= 1 + (ttValue > alpha) + (tte->depth() >= depth); + r -= 2719 + PvNode * 983 + (ttData.value > alpha) * 922 + + (ttData.depth >= depth) * (934 + cutNode * 1011); - // Increase reduction for cut nodes (~4 Elo) + r += 714; // Base reduction offset to compensate for other tweaks + r -= moveCount * 73; + r -= std::abs(correctionValue) / 30370; + + // Increase reduction for cut nodes if (cutNode) - r += 2 - (tte->depth() >= depth && ss->ttPv); + r += 3372 + 997 * !ttData.move; - // Increase reduction if ttMove is a capture (~3 Elo) + // Increase reduction if ttMove is a capture if (ttCapture) - r++; + r += 1119; - // Decrease reduction for PvNodes (~0 Elo on STC, ~2 Elo on LTC) - if (PvNode) - r--; + // 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; - // Increase reduction if next ply has a lot of fail high (~5 Elo) - if ((ss + 1)->cutoffCnt > 3) - r++; + // For first picked move (ttMove) reduce reduction + if (move == ttData.move) + r -= 2151; - // Set reduction to 0 for first picked move (ttMove) (~2 Elo) - // Nullifies all previous reduction adjustments to ttMove and leaves only history to do them - else if (move == ttMove) - r = 0; + if (capture) + ss->statScore = 868 * int(PieceValue[pos.captured_piece()]) / 128 + + captureHistory[movedPiece][move.to_sq()][type_of(pos.captured_piece())]; + else + ss->statScore = 2 * mainHistory[us][move.raw()] + + (*contHist[0])[movedPiece][move.to_sq()] + + (*contHist[1])[movedPiece][move.to_sq()]; - ss->statScore = 2 * thisThread->mainHistory[us][move.from_to()] - + (*contHist[0])[movedPiece][move.to_sq()] - + (*contHist[1])[movedPiece][move.to_sq()] - + (*contHist[3])[movedPiece][move.to_sq()] - 4723; + // Decrease/increase reduction for moves with a good/bad history + r -= ss->statScore * 850 / 8192; - // Decrease/increase reduction for moves with a good/bad history (~8 Elo) - r -= ss->statScore / 13659; + // Scale up reductions for expected ALL nodes + if (allNode) + r += r / (depth + 1); - // Step 17. Late moves reduction / extension (LMR, ~117 Elo) - if (depth >= 2 && moveCount > 1 + rootNode) + // Step 17. Late moves reduction / extension (LMR) + if (depth >= 2 && moveCount > 1) { // In general we want to cap the LMR depth search at newDepth, but when // reduction is negative, we allow this move a limited search extension - // beyond the first move depth. This may lead to hidden multiple extensions. + // beyond the first move depth. // To prevent problems when the max value is less than the min value, // std::clamp has been replaced by a more robust implementation. - Depth d = std::max(1, std::min(newDepth - r, newDepth + 1)); + Depth d = std::max(1, std::min(newDepth - r / 1024, newDepth + 2)) + PvNode; - value = -search(pos, ss + 1, -(alpha + 1), -alpha, d, true); + ss->reduction = newDepth - d; + value = -search(pos, ss + 1, -(alpha + 1), -alpha, d, true); + ss->reduction = 0; // Do a full-depth search when reduced LMR search fails high - if (value > alpha && d < newDepth) + // (*Scaler) Shallower searches here don't scale well + if (value > alpha) { - // Adjust full-depth search based on LMR results - if the result - // was good enough search deeper, if it was bad enough search shallower. - const bool doDeeperSearch = value > (bestValue + 47 + 2 * newDepth); // (~1 Elo) - const bool doShallowerSearch = value < bestValue + newDepth; // (~2 Elo) + // Adjust full-depth search based on LMR results - if the result was + // good enough search deeper, if it was bad enough search shallower. + const bool doDeeperSearch = d < newDepth && value > bestValue + 50; + const bool doShallowerSearch = value < bestValue + 9; newDepth += doDeeperSearch - doShallowerSearch; if (newDepth > d) value = -search(pos, ss + 1, -(alpha + 1), -alpha, newDepth, !cutNode); - // Post LMR continuation history updates (~1 Elo) - int bonus = value <= alpha ? -stat_malus(newDepth) - : value >= beta ? stat_bonus(newDepth) - : 0; - - update_continuation_histories(ss, movedPiece, move.to_sq(), bonus); + // Post LMR continuation history updates + update_continuation_histories(ss, movedPiece, move.to_sq(), 1365); } } // Step 18. Full-depth search when LMR is skipped else if (!PvNode || moveCount > 1) { - // Increase reduction if ttMove is not present (~6 Elo) - if (!ttMove) - r += 2; + // Increase reduction if ttMove is not present + if (!ttData.move) + r += 1140; - // Note that if expected reduction is high, we reduce search depth by 1 here (~9 Elo) - value = -search(pos, ss + 1, -(alpha + 1), -alpha, newDepth - (r > 3), !cutNode); + // Note that if expected reduction is high, we reduce search depth here + value = -search(pos, ss + 1, -(alpha + 1), -alpha, + newDepth - (r > 3957) - (r > 5654 && newDepth > 2), !cutNode); } // For PV nodes only, do a full PV search on the first move or after a fail high, @@ -1231,36 +1329,46 @@ moves_loop: // When in check, search starts here (ss + 1)->pv = pv; (ss + 1)->pv[0] = Move::none(); + // Extend move from transposition table if we are about to dive into qsearch. + // decisive score handling improves mate finding and retrograde analysis. + if (move == ttData.move + && ((is_valid(ttData.value) && is_decisive(ttData.value) && ttData.depth > 0) + || ttData.depth > 1)) + newDepth = std::max(newDepth, 1); + value = -search(pos, ss + 1, -beta, -alpha, newDepth, false); } // Step 19. Undo move - pos.undo_move(move); + undo_move(pos, move); assert(value > -VALUE_INFINITE && value < VALUE_INFINITE); // Step 20. Check for a new best move // Finished searching the move. If a stop occurred, the return value of - // the search cannot be trusted, and we return immediately without - // updating best move, PV and TT. + // the search cannot be trusted, and we return immediately without updating + // best move, principal variation nor transposition table. if (threads.stop.load(std::memory_order_relaxed)) return VALUE_ZERO; if (rootNode) { - RootMove& rm = - *std::find(thisThread->rootMoves.begin(), thisThread->rootMoves.end(), move); + RootMove& rm = *std::find(rootMoves.begin(), rootMoves.end(), move); rm.effort += nodes - nodeCount; rm.averageScore = - rm.averageScore != -VALUE_INFINITE ? (2 * value + rm.averageScore) / 3 : value; + rm.averageScore != -VALUE_INFINITE ? (value + rm.averageScore) / 2 : value; + + rm.meanSquaredScore = rm.meanSquaredScore != -VALUE_INFINITE * VALUE_INFINITE + ? (value * std::abs(value) + rm.meanSquaredScore) / 2 + : value * std::abs(value); // PV move or new best move? if (moveCount == 1 || value > alpha) { rm.score = rm.uciScore = value; - rm.selDepth = thisThread->selDepth; + rm.selDepth = selDepth; rm.scoreLowerbound = rm.scoreUpperbound = false; if (value >= beta) @@ -1284,8 +1392,8 @@ moves_loop: // When in check, search starts here // We record how often the best move has been changed in each iteration. // This information is used for time management. In MultiPV mode, // we must take care to only do this for the first PV line. - if (moveCount > 1 && !thisThread->pvIdx) - ++thisThread->bestMoveChanges; + if (moveCount > 1 && !pvIdx) + ++bestMoveChanges; } else // All other moves but the PV, are set to the lowest value: this @@ -1294,11 +1402,16 @@ moves_loop: // When in check, search starts here rm.score = -VALUE_INFINITE; } - if (value > bestValue) + // In case we have an alternative move equal in eval to the current bestmove, + // promote it to bestmove by pretending it just exceeds alpha (but not beta). + int inc = (value == bestValue && ss->ply + 2 >= rootDepth && (int(nodes) & 14) == 0 + && !is_win(std::abs(value) + 1)); + + if (value + inc > bestValue) { bestValue = value; - if (value > alpha) + if (value + inc > alpha) { bestMove = move; @@ -1307,30 +1420,29 @@ moves_loop: // When in check, search starts here if (value >= beta) { - ss->cutoffCnt += 1 + !ttMove; + // (*Scaler) Infrequent and small updates scale well + ss->cutoffCnt += (extension < 2) || PvNode; assert(value >= beta); // Fail high break; } - else - { - // Reduce other moves if we have found at least one score improvement (~2 Elo) - if (depth > 2 && depth < 12 && beta < 14206 && value > -12077) - depth -= 2; - assert(depth > 0); - alpha = value; // Update alpha! Always alpha < beta - } + // Reduce other moves if we have found at least one score improvement + if (depth > 2 && depth < 14 && !is_decisive(value)) + depth -= 2; + + assert(depth > 0); + alpha = value; // Update alpha! Always alpha < beta } } // If the move is worse than some previously searched move, // remember it, to update its stats later. - if (move != bestMove && moveCount <= 32) + if (move != bestMove && moveCount <= SEARCHEDLIST_CAPACITY) { if (capture) - capturesSearched[captureCount++] = move; + capturesSearched.push_back(move); else - quietsSearched[quietCount++] = move; + quietsSearched.push_back(move); } } @@ -1341,57 +1453,82 @@ moves_loop: // When in check, search starts here assert(moveCount || !ss->inCheck || excludedMove || !MoveList(pos).size()); - // Adjust best value for fail high cases at non-pv nodes - if (!PvNode && bestValue >= beta && std::abs(bestValue) < VALUE_TB_WIN_IN_MAX_PLY - && std::abs(beta) < VALUE_TB_WIN_IN_MAX_PLY && std::abs(alpha) < VALUE_TB_WIN_IN_MAX_PLY) - bestValue = (bestValue * (depth + 2) + beta) / (depth + 3); + // Adjust best value for fail high cases + if (bestValue >= beta && !is_decisive(bestValue) && !is_decisive(alpha)) + bestValue = (bestValue * depth + beta) / (depth + 1); if (!moveCount) bestValue = excludedMove ? alpha : ss->inCheck ? mated_in(ss->ply) : VALUE_DRAW; - // If there is a move that produces search value greater than alpha we update the stats of searched moves + // If there is a move that produces search value greater than alpha, + // we update the stats of searched moves. else if (bestMove) - update_all_stats(pos, ss, *this, bestMove, bestValue, beta, prevSq, quietsSearched, - quietCount, capturesSearched, captureCount, depth); + { + update_all_stats(pos, ss, *this, bestMove, prevSq, quietsSearched, capturesSearched, depth, + ttData.move, moveCount); + if (!PvNode) + ttMoveHistory << (bestMove == ttData.move ? 809 : -865); + } - // Bonus for prior countermove that caused the fail low + // Bonus for prior quiet countermove that caused the fail low else if (!priorCapture && prevSq != SQ_NONE) { - int bonus = (depth > 5) + (PvNode || cutNode) + ((ss - 1)->statScore < -14963) - + ((ss - 1)->moveCount > 11) - + (!ss->inCheck && bestValue <= ss->staticEval - 150); + int bonusScale = -215; + bonusScale -= (ss - 1)->statScore / 100; + bonusScale += std::min(56 * depth, 489); + bonusScale += 184 * ((ss - 1)->moveCount > 8); + bonusScale += 147 * (!ss->inCheck && bestValue <= ss->staticEval - 107); + bonusScale += 156 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 65); + + bonusScale = std::max(bonusScale, 0); + + const int scaledBonus = std::min(141 * depth - 87, 1351) * bonusScale; + update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, - stat_bonus(depth) * bonus); - thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] - << stat_bonus(depth) * bonus / 2; + scaledBonus * 406 / 32768); + + 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; + } + + // Bonus for prior capture countermove that caused the fail low + else if (priorCapture && prevSq != SQ_NONE) + { + Piece capturedPiece = pos.captured_piece(); + assert(capturedPiece != NO_PIECE); + captureHistory[pos.piece_on(prevSq)][prevSq][type_of(capturedPiece)] << 1012; } if (PvNode) bestValue = std::min(bestValue, maxValue); // If no good move is found and the previous position was ttPv, then the previous - // opponent move is probably good and the new position is added to the search tree. (~7 Elo) + // opponent move is probably good and the new position is added to the search tree. if (bestValue <= alpha) - ss->ttPv = ss->ttPv || ((ss - 1)->ttPv && depth > 3); + ss->ttPv = ss->ttPv || (ss - 1)->ttPv; - // Write gathered information in transposition table - // Static evaluation is saved as it was before correction history - if (!excludedMove && !(rootNode && thisThread->pvIdx)) - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(bestValue, ss->ply), - ss->ttPv, - bestValue >= beta ? BOUND_LOWER - : PvNode && bestMove ? BOUND_EXACT - : BOUND_UPPER, - depth, bestMove, unadjustedStaticEval, tt.generation()); + // 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()); - // Adjust correction history - if (!ss->inCheck && (!bestMove || !pos.capture(bestMove)) - && !(bestValue >= beta && bestValue <= ss->staticEval) - && !(!bestMove && bestValue >= ss->staticEval)) + // Adjust correction history if the best move is not a capture + // and the error direction matches whether we are above/below bounds. + if (!ss->inCheck && !(bestMove && pos.capture(bestMove)) + && (bestValue > ss->staticEval) == bool(bestMove)) { - auto bonus = std::clamp(int(bestValue - ss->staticEval) * depth / 8, + auto bonus = std::clamp(int(bestValue - ss->staticEval) * depth / (bestMove ? 10 : 8), -CORRECTION_HISTORY_LIMIT / 4, CORRECTION_HISTORY_LIMIT / 4); - thisThread->correctionHistory[us][pawn_structure_index(pos)] << bonus; + update_correction_history(pos, ss, *this, bonus); } assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); @@ -1400,40 +1537,37 @@ moves_loop: // When in check, search starts here } -// Quiescence search function, which is called by the main search -// function with zero depth, or recursively with further decreasing depth per call. -// (~155 Elo) +// Quiescence search function, which is called by the main search function with +// depth zero, or recursively with further decreasing depth. With depth <= 0, we +// "should" be using static eval only, but tactical moves may confuse the static eval. +// To fight this horizon effect, we implement this qsearch of tactical moves. +// See https://www.chessprogramming.org/Horizon_Effect +// and https://www.chessprogramming.org/Quiescence_Search template -Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { +Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) { static_assert(nodeType != Root); constexpr bool PvNode = nodeType == PV; assert(alpha >= -VALUE_INFINITE && alpha < beta && beta <= VALUE_INFINITE); assert(PvNode || (alpha == beta - 1)); - assert(depth <= 0); - // Check if we have an upcoming move that draws by repetition, or if - // the opponent had an alternative move earlier to this position. (~1 Elo) - if (alpha < VALUE_DRAW && pos.has_game_cycle(ss->ply)) + // Check if we have an upcoming move that draws by repetition + if (alpha < VALUE_DRAW && pos.upcoming_repetition(ss->ply)) { - alpha = value_draw(this->nodes); + alpha = value_draw(nodes); if (alpha >= beta) return alpha; } Move pv[MAX_PLY + 1]; StateInfo st; - ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); - TTEntry* tte; - Key posKey; - Move ttMove, move, bestMove; - Depth ttDepth; - Value bestValue, value, ttValue, futilityValue, futilityBase; - bool pvHit, givesCheck, capture; - int moveCount; - Color us = pos.side_to_move(); + Key posKey; + Move move, bestMove; + Value bestValue, value, futilityBase; + bool pvHit, givesCheck, capture; + int moveCount; // Step 1. Initialize node if (PvNode) @@ -1442,38 +1576,34 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, ss->pv[0] = Move::none(); } - Worker* thisThread = this; - bestMove = Move::none(); - ss->inCheck = pos.checkers(); - moveCount = 0; + bestMove = Move::none(); + ss->inCheck = pos.checkers(); + moveCount = 0; // Used to send selDepth info to GUI (selDepth counts from 1, ply from 0) - if (PvNode && thisThread->selDepth < ss->ply + 1) - thisThread->selDepth = ss->ply + 1; + if (PvNode && selDepth < ss->ply + 1) + selDepth = ss->ply + 1; // Step 2. Check for an immediate draw or maximum ply reached if (pos.is_draw(ss->ply) || ss->ply >= MAX_PLY) - return (ss->ply >= MAX_PLY && !ss->inCheck) - ? evaluate(networks, pos, thisThread->optimism[us]) - : VALUE_DRAW; + return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos) : VALUE_DRAW; assert(0 <= ss->ply && ss->ply < MAX_PLY); - // Decide the replacement and cutoff priority of the qsearch TT entries - ttDepth = ss->inCheck || depth >= DEPTH_QS_CHECKS ? DEPTH_QS_CHECKS : DEPTH_QS_NO_CHECKS; - // Step 3. Transposition table lookup - posKey = pos.key(); - tte = tt.probe(posKey, ss->ttHit); - ttValue = ss->ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; - ttMove = ss->ttHit ? tte->move() : Move::none(); - pvHit = ss->ttHit && tte->is_pv(); + posKey = pos.key(); + auto [ttHit, ttData, ttWriter] = tt.probe(posKey); + // Need further processing of the saved data + ss->ttHit = ttHit; + ttData.move = ttHit ? ttData.move : Move::none(); + ttData.value = ttHit ? value_from_tt(ttData.value, ss->ply, pos.rule50_count()) : VALUE_NONE; + pvHit = ttHit && ttData.is_pv; // At non-PV nodes we check for an early TT cutoff - if (!PvNode && tte->depth() >= ttDepth - && ttValue != VALUE_NONE // Only in case of TT access race or if !ttHit - && (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER))) - return ttValue; + if (!PvNode && ttData.depth >= DEPTH_QS + && is_valid(ttData.value) // Can happen when !ttHit or when access race in probe() + && (ttData.bound & (ttData.value >= beta ? BOUND_LOWER : BOUND_UPPER))) + return ttData.value; // Step 4. Static evaluation of the position Value unadjustedStaticEval = VALUE_NONE; @@ -1481,37 +1611,42 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, bestValue = futilityBase = -VALUE_INFINITE; else { + const auto correctionValue = correction_value(*this, pos, ss); + if (ss->ttHit) { // Never assume anything about values stored in TT - unadjustedStaticEval = tte->eval(); - if (unadjustedStaticEval == VALUE_NONE) - unadjustedStaticEval = evaluate(networks, pos, thisThread->optimism[us]); - ss->staticEval = bestValue = - to_corrected_static_eval(unadjustedStaticEval, *thisThread, pos); + unadjustedStaticEval = ttData.eval; - // ttValue can be used as a better position evaluation (~13 Elo) - if (ttValue != VALUE_NONE - && (tte->bound() & (ttValue > bestValue ? BOUND_LOWER : BOUND_UPPER))) - bestValue = ttValue; + if (!is_valid(unadjustedStaticEval)) + unadjustedStaticEval = evaluate(pos); + + ss->staticEval = bestValue = + to_corrected_static_eval(unadjustedStaticEval, correctionValue); + + // ttValue can be used as a better position evaluation + if (is_valid(ttData.value) && !is_decisive(ttData.value) + && (ttData.bound & (ttData.value > bestValue ? BOUND_LOWER : BOUND_UPPER))) + bestValue = ttData.value; } else { - // In case of null move search, use previous static eval with a different sign - unadjustedStaticEval = (ss - 1)->currentMove != Move::null() - ? evaluate(networks, pos, thisThread->optimism[us]) - : -(ss - 1)->staticEval; + unadjustedStaticEval = evaluate(pos); ss->staticEval = bestValue = - to_corrected_static_eval(unadjustedStaticEval, *thisThread, pos); + to_corrected_static_eval(unadjustedStaticEval, correctionValue); } // Stand pat. Return immediately if static value is at least beta if (bestValue >= beta) { + if (!is_decisive(bestValue)) + bestValue = (bestValue + beta) / 2; + if (!ss->ttHit) - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(bestValue, ss->ply), - false, BOUND_LOWER, DEPTH_NONE, Move::none(), unadjustedStaticEval, - tt.generation()); + Distributed::save(tt, threads, this, ttWriter, posKey, + value_to_tt(bestValue, ss->ply), false, BOUND_LOWER, + DEPTH_UNSEARCHED, Move::none(), unadjustedStaticEval, + tt.generation()); return bestValue; } @@ -1519,29 +1654,25 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, if (bestValue > alpha) alpha = bestValue; - futilityBase = ss->staticEval + 226; + futilityBase = ss->staticEval + 351; } - const PieceToHistory* contHist[] = {(ss - 1)->continuationHistory, - (ss - 2)->continuationHistory}; + const PieceToHistory* contHist[] = {(ss - 1)->continuationHistory}; - // Initialize a MovePicker object for the current position, and prepare - // to search the moves. Because the depth is <= 0 here, only captures, - // queen promotions, and other checks (only if depth >= DEPTH_QS_CHECKS) - // will be generated. - Square prevSq = ((ss - 1)->currentMove).is_ok() ? ((ss - 1)->currentMove).to_sq() : SQ_NONE; - MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &thisThread->captureHistory, - contHist, &thisThread->pawnHistory); + Square prevSq = ((ss - 1)->currentMove).is_ok() ? ((ss - 1)->currentMove).to_sq() : SQ_NONE; - int quietCheckEvasions = 0; + // Initialize a MovePicker object for the current position, and prepare to search + // the moves. We presently use two stages of move generator in quiescence search: + // captures, or evasions only when in check. + MovePicker mp(pos, ttData.move, DEPTH_QS, &mainHistory, &lowPlyHistory, &captureHistory, + contHist, &sharedHistory, ss->ply); - // Step 5. Loop through all pseudo-legal moves until no moves remain - // or a beta cutoff occurs. + // Step 5. Loop through all pseudo-legal moves until no moves remain or a beta + // cutoff occurs. while ((move = mp.next_move()) != Move::none()) { assert(move.is_ok()); - // Check for legality if (!pos.legal(move)) continue; @@ -1551,74 +1682,48 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, moveCount++; // Step 6. Pruning - if (bestValue > VALUE_TB_LOSS_IN_MAX_PLY && pos.non_pawn_material(us)) + if (!is_loss(bestValue)) { - // Futility pruning and moveCount pruning (~10 Elo) - if (!givesCheck && move.to_sq() != prevSq && futilityBase > VALUE_TB_LOSS_IN_MAX_PLY + // Futility pruning and moveCount pruning + if (!givesCheck && move.to_sq() != prevSq && !is_loss(futilityBase) && move.type_of() != PROMOTION) { if (moveCount > 2) continue; - futilityValue = futilityBase + PieceValue[pos.piece_on(move.to_sq())]; + Value futilityValue = futilityBase + PieceValue[pos.piece_on(move.to_sq())]; - // If static eval + value of piece we are going to capture is much lower - // than alpha we can prune this move. (~2 Elo) + // If static eval + value of piece we are going to capture is + // much lower than alpha, we can prune this move. if (futilityValue <= alpha) { bestValue = std::max(bestValue, futilityValue); continue; } - // If static eval is much lower than alpha and move is not winning material - // we can prune this move. (~2 Elo) - if (futilityBase <= alpha && !pos.see_ge(move, 1)) + // If static exchange evaluation is low enough + // we can prune this move. + if (!pos.see_ge(move, alpha - futilityBase)) { - bestValue = std::max(bestValue, futilityBase); - continue; - } - - // If static exchange evaluation is much worse than what is needed to not - // fall below alpha we can prune this move. - if (futilityBase > alpha && !pos.see_ge(move, (alpha - futilityBase) * 4)) - { - bestValue = alpha; + bestValue = std::min(alpha, futilityBase); continue; } } - // We prune after the second quiet check evasion move, where being 'in check' is - // implicitly checked through the counter, and being a 'quiet move' apart from - // being a tt move is assumed after an increment because captures are pushed ahead. - if (quietCheckEvasions > 1) - break; - - // Continuation history based pruning (~3 Elo) - if (!capture && (*contHist[0])[pos.moved_piece(move)][move.to_sq()] < 0 - && (*contHist[1])[pos.moved_piece(move)][move.to_sq()] < 0) + // Skip non-captures + if (!capture) continue; - // Do not search moves with bad enough SEE values (~5 Elo) - if (!pos.see_ge(move, -78)) + // Do not search moves with bad enough SEE values + if (!pos.see_ge(move, -80)) continue; } - // Speculative prefetch as early as possible - prefetch(tt.first_entry(pos.key_after(move))); - - // Update the current move - ss->currentMove = move; - ss->continuationHistory = - &thisThread - ->continuationHistory[ss->inCheck][capture][pos.moved_piece(move)][move.to_sq()]; - - quietCheckEvasions += !capture && ss->inCheck; - // Step 7. Make and search the move - thisThread->nodes.fetch_add(1, std::memory_order_relaxed); - pos.do_move(move, st, givesCheck); - value = -qsearch(pos, ss + 1, -beta, -alpha, depth - 1); - pos.undo_move(move); + do_move(pos, move, st, givesCheck, ss); + + value = -qsearch(pos, ss + 1, -beta, -alpha); + undo_move(pos, move); assert(value > -VALUE_INFINITE && value < VALUE_INFINITE); @@ -1643,56 +1748,86 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, } // Step 9. Check for mate - // All legal moves have been searched. A special case: if we're in check - // and no legal moves were found, it is checkmate. + // All legal moves have been searched. A special case: if we are + // in check and no legal moves were found, it is checkmate. if (ss->inCheck && bestValue == -VALUE_INFINITE) { assert(!MoveList(pos).size()); return mated_in(ss->ply); // Plies to mate from the root } - if (std::abs(bestValue) < VALUE_TB_WIN_IN_MAX_PLY && bestValue >= beta) - bestValue = (3 * bestValue + beta) / 4; + if (!is_decisive(bestValue) && bestValue > beta) + bestValue = (bestValue + beta) / 2; - // Save gathered info in transposition table - // Static evaluation is saved as it was before adjustment by correction history - Cluster::save(tt, threads, thisThread, tte, posKey, value_to_tt(bestValue, ss->ply), pvHit, - bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, ttDepth, bestMove, - unadjustedStaticEval, tt.generation()); + Color us = pos.side_to_move(); + if (!ss->inCheck && !moveCount && !pos.non_pawn_material(us) + && type_of(pos.captured_piece()) >= ROOK) + { + if (!((us == WHITE ? shift(pos.pieces(us, PAWN)) + : shift(pos.pieces(us, PAWN))) + & ~pos.pieces())) // no pawn pushes available + { + pos.state()->checkersBB = Rank1BB; // search for legal king-moves only + if (!MoveList(pos).size()) // stalemate + bestValue = VALUE_DRAW; + pos.state()->checkersBB = 0; + } + } + + // 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()); assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); return bestValue; } -Depth Search::Worker::reduction(bool i, Depth d, int mn, int delta) { +Depth Search::Worker::reduction(bool i, Depth d, int mn, int delta) const { int reductionScale = reductions[d] * reductions[mn]; - return (reductionScale + 1107 - delta * 725 / rootDelta) / 1024 + (!i && reductionScale > 956); + return reductionScale - delta * 608 / rootDelta + !i * reductionScale * 238 / 512 + 1182; +} + +// elapsed() returns the time elapsed since the search started. If the +// 'nodestime' option is enabled, it will return the count of nodes searched +// instead. This function is called to check whether the search should be +// stopped based on predefined thresholds like time limits or nodes searched. +// +// elapsed_time() returns the actual time elapsed since the start of the search. +// 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); }); +} + +TimePoint Search::Worker::elapsed_time() const { return main_manager()->tm.elapsed_time(); } + +Value Search::Worker::evaluate(const Position& pos) { + return Eval::evaluate(networks[numaAccessToken], pos, accumulatorStack, refreshTable, + optimism[pos.side_to_move()]); } namespace { -// Adjusts a mate or TB score from "plies to mate from the root" -// to "plies to mate from the current position". Standard scores are unchanged. +// Adjusts a mate or TB score from "plies to mate from the root" to +// "plies to mate from the current position". Standard scores are unchanged. // The function is called before storing a value in the transposition table. -Value value_to_tt(Value v, int ply) { - - assert(v != VALUE_NONE); - return v >= VALUE_TB_WIN_IN_MAX_PLY ? v + ply : v <= VALUE_TB_LOSS_IN_MAX_PLY ? v - ply : v; -} +Value value_to_tt(Value v, int ply) { return is_win(v) ? v + ply : is_loss(v) ? v - ply : v; } -// Inverse of value_to_tt(): it adjusts a mate or TB score -// from the transposition table (which refers to the plies to mate/be mated from -// current position) to "plies to mate/be mated (TB win/loss) from the root". -// However, to avoid potentially false mate or TB scores related to the 50 moves rule -// and the graph history interaction, we return the highest non-TB score instead. +// Inverse of value_to_tt(): it adjusts a mate or TB score from the transposition +// table (which refers to the plies to mate/be mated from current position) to +// "plies to mate/be mated (TB win/loss) from the root". However, to avoid +// potentially false mate or TB scores related to the 50 moves rule and the +// graph history interaction, we return the highest non-TB score instead. Value value_from_tt(Value v, int ply, int r50c) { - if (v == VALUE_NONE) + if (!is_valid(v)) return VALUE_NONE; // handle TB win or better - if (v >= VALUE_TB_WIN_IN_MAX_PLY) + if (is_win(v)) { // Downgrade a potentially false mate score if (v >= VALUE_MATE_IN_MAX_PLY && VALUE_MATE - v > 100 - r50c) @@ -1706,7 +1841,7 @@ Value value_from_tt(Value v, int ply, int r50c) { } // handle TB loss or worse - if (v <= VALUE_TB_LOSS_IN_MAX_PLY) + if (is_loss(v)) { // Downgrade a potentially false mate score. if (v <= VALUE_MATED_IN_MAX_PLY && VALUE_MATE + v > 100 - r50c) @@ -1737,112 +1872,96 @@ void update_all_stats(const Position& pos, Stack* ss, Search::Worker& workerThread, Move bestMove, - Value bestValue, - Value beta, Square prevSq, - Move* quietsSearched, - int quietCount, - Move* capturesSearched, - int captureCount, - Depth depth) { + SearchedList& quietsSearched, + SearchedList& capturesSearched, + Depth depth, + Move ttMove, + int moveCount) { - Color us = pos.side_to_move(); CapturePieceToHistory& captureHistory = workerThread.captureHistory; - Piece moved_piece = pos.moved_piece(bestMove); - PieceType captured; + Piece movedPiece = pos.moved_piece(bestMove); + PieceType capturedPiece; - int quietMoveBonus = stat_bonus(depth + 1); - int quietMoveMalus = stat_malus(depth); + int bonus = + std::min(116 * depth - 81, 1515) + 347 * (bestMove == ttMove) + (ss - 1)->statScore / 32; + int malus = std::min(848 * depth - 207, 2446) - 17 * moveCount; if (!pos.capture_stage(bestMove)) { - int bestMoveBonus = bestValue > beta + 168 ? quietMoveBonus // larger bonus - : stat_bonus(depth); // smaller bonus - - // Increase stats for the best move in case it was a quiet move - update_quiet_stats(pos, ss, workerThread, bestMove, bestMoveBonus); - - int pIndex = pawn_structure_index(pos); - workerThread.pawnHistory[pIndex][moved_piece][bestMove.to_sq()] << quietMoveBonus; + update_quiet_histories(pos, ss, workerThread, bestMove, bonus * 910 / 1024); + int i = 0; // Decrease stats for all non-best quiet moves - for (int i = 0; i < quietCount; ++i) + for (Move move : quietsSearched) { - workerThread - .pawnHistory[pIndex][pos.moved_piece(quietsSearched[i])][quietsSearched[i].to_sq()] - << -quietMoveMalus; - - workerThread.mainHistory[us][quietsSearched[i].from_to()] << -quietMoveMalus; - update_continuation_histories(ss, pos.moved_piece(quietsSearched[i]), - quietsSearched[i].to_sq(), -quietMoveMalus); + i++; + int actualMalus = malus * 1085 / 1024; + if (i > 5) + actualMalus -= actualMalus * (i - 5) / i; + update_quiet_histories(pos, ss, workerThread, move, -actualMalus); } } else { // Increase stats for the best move in case it was a capture move - captured = type_of(pos.piece_on(bestMove.to_sq())); - captureHistory[moved_piece][bestMove.to_sq()][captured] << quietMoveBonus; + capturedPiece = type_of(pos.piece_on(bestMove.to_sq())); + captureHistory[movedPiece][bestMove.to_sq()][capturedPiece] << bonus * 1395 / 1024; } - // Extra penalty for a quiet early move that was not a TT move or - // main killer move in previous ply when it gets refuted. - if (prevSq != SQ_NONE - && ((ss - 1)->moveCount == 1 + (ss - 1)->ttHit - || ((ss - 1)->currentMove == (ss - 1)->killers[0])) - && !pos.captured_piece()) - update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -quietMoveMalus); + // Extra penalty for a quiet early move that was not a TT move in + // previous ply when it gets refuted. + if (prevSq != SQ_NONE && ((ss - 1)->moveCount == 1 + (ss - 1)->ttHit) && !pos.captured_piece()) + update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -malus * 602 / 1024); // Decrease stats for all non-best capture moves - for (int i = 0; i < captureCount; ++i) + for (Move move : capturesSearched) { - moved_piece = pos.moved_piece(capturesSearched[i]); - captured = type_of(pos.piece_on(capturesSearched[i].to_sq())); - captureHistory[moved_piece][capturesSearched[i].to_sq()][captured] << -quietMoveMalus; + movedPiece = pos.moved_piece(move); + capturedPiece = type_of(pos.piece_on(move.to_sq())); + captureHistory[movedPiece][move.to_sq()][capturedPiece] << -malus * 1448 / 1024; } } -// Updates histories of the move pairs formed -// by moves at ply -1, -2, -3, -4, and -6 with current move. +// Updates histories of the move pairs formed by moves +// at ply -1, -2, -3, -4, and -6 with current move. void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus) { + static std::array conthist_bonuses = { + {{1, 1133}, {2, 683}, {3, 312}, {4, 582}, {5, 149}, {6, 474}}}; - for (int i : {1, 2, 3, 4, 6}) + for (const auto [i, weight] : conthist_bonuses) { // Only update the first 2 continuation histories if we are in check if (ss->inCheck && i > 2) break; + if (((ss - i)->currentMove).is_ok()) - (*(ss - i)->continuationHistory)[pc][to] << bonus / (1 + 3 * (i == 3)); + (*(ss - i)->continuationHistory)[pc][to] << (bonus * weight / 1024) + 88 * (i < 2); } } - // Updates move sorting heuristics -void update_quiet_stats( + +void update_quiet_histories( const Position& pos, Stack* ss, Search::Worker& workerThread, Move move, int bonus) { - // Update killers - if (ss->killers[0] != move) - { - ss->killers[1] = ss->killers[0]; - ss->killers[0] = move; - } - Color us = pos.side_to_move(); - workerThread.mainHistory[us][move.from_to()] << bonus; - update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus); + workerThread.mainHistory[us][move.raw()] << bonus; // Untuned to prevent duplicate effort - // Update countermove history - if (((ss - 1)->currentMove).is_ok()) - { - Square prevSq = ((ss - 1)->currentMove).to_sq(); - workerThread.counterMoves[pos.piece_on(prevSq)][prevSq] = move; - } -} + if (ss->ply < LOW_PLY_HISTORY_SIZE) + workerThread.lowPlyHistory[ss->ply][move.raw()] << bonus * 805 / 1024; + + update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus * 896 / 1024); + + workerThread.sharedHistory.pawn_entry(pos)[pos.moved_piece(move)][move.to_sq()] + << bonus * (bonus > 0 ? 905 : 505) / 1024; } -// When playing with strength handicap, choose the best move among a set of RootMoves -// using a statistical rule dependent on 'level'. Idea by Heinz van Saanen. +} + +// When playing with strength handicap, choose the best move among a set of +// RootMoves using a statistical rule dependent on 'level'. Idea by Heinz van Saanen. Move Skill::pick_best(const RootMoves& rootMoves, size_t multiPV) { static PRNG rng(now()); // PRNG sequence should be non-deterministic @@ -1858,8 +1977,8 @@ Move Skill::pick_best(const RootMoves& rootMoves, size_t multiPV) { for (size_t i = 0; i < multiPV; ++i) { // This is our magic formula - int push = (weakness * int(topScore - rootMoves[i].score) - + delta * (rng.rand() % int(weakness))) + int push = int(weakness * int(topScore - rootMoves[i].score) + + delta * (rng.rand() % int(weakness))) / 128; if (rootMoves[i].score + push >= maxScore) @@ -1872,9 +1991,8 @@ Move Skill::pick_best(const RootMoves& rootMoves, size_t multiPV) { return best; } - -// Used to print debug info and, more importantly, -// to detect when we are out of available time and thus stop the search. +// Used to print debug info and, more importantly, to detect +// when we are out of available time and thus stop the search. void SearchManager::check_time(Search::Worker& worker) { if (--callsCnt > 0) return; @@ -1884,8 +2002,9 @@ void SearchManager::check_time(Search::Worker& worker) { static TimePoint lastInfoTime = now(); - TimePoint elapsed = tm.elapsed(Cluster::nodes_searched(worker.threads)); - TimePoint tick = worker.limits.startTime + elapsed; + TimePoint elapsed = + tm.elapsed([&worker]() { return Distributed::nodes_searched(worker.threads); }); + TimePoint tick = worker.limits.startTime + elapsed; if (tick - lastInfoTime >= 1000) { @@ -1894,7 +2013,7 @@ void SearchManager::check_time(Search::Worker& worker) { } // poll on MPI signals - Cluster::signals_poll(worker.threads); + Distributed::signals_poll(worker.threads); // We should not stop pondering until told so by the GUI if (ponder) @@ -1907,23 +2026,160 @@ void SearchManager::check_time(Search::Worker& worker) { && ((worker.limits.use_time_management() && (elapsed > tm.maximum() || stopOnPonderhit)) || (worker.limits.movetime && elapsed >= worker.limits.movetime) || (worker.limits.nodes - && Cluster::nodes_searched(worker.threads) >= worker.limits.nodes))) + && Distributed::nodes_searched(worker.threads) >= worker.limits.nodes))) worker.threads.stop = worker.threads.abortedSearch = true; } -std::string SearchManager::pv(const Search::Worker& worker, - const ThreadPool& threads, - const TranspositionTable& tt, - Depth depth) const { - std::stringstream ss; +// Used to correct and extend PVs for moves that have a TB (but not a mate) score. +// Keeps the search based PV for as long as it is verified to maintain the game +// outcome, truncates afterwards. Finally, extends to mate the PV, providing a +// possible continuation (but not a proven mating line). +void syzygy_extend_pv(const OptionsMap& options, + const Search::LimitsType& limits, + Position& pos, + RootMove& rootMove, + Value& v) { - const auto nodes = Cluster::nodes_searched(threads); - const auto& rootMoves = worker.rootMoves; - const auto& pos = worker.rootPos; - size_t pvIdx = worker.pvIdx; - TimePoint time = tm.elapsed(nodes) + 1; - size_t multiPV = std::min(size_t(worker.options["MultiPV"]), rootMoves.size()); - uint64_t tbHits = Cluster::tb_hits(threads) + (worker.tbConfig.rootInTB ? rootMoves.size() : 0); + auto t_start = std::chrono::steady_clock::now(); + int moveOverhead = int(options["Move Overhead"]); + bool rule50 = bool(options["Syzygy50MoveRule"]); + + // Do not use more than moveOverhead / 2 time, if time management is active + auto time_abort = [&t_start, &moveOverhead, &limits]() -> bool { + auto t_end = std::chrono::steady_clock::now(); + return limits.use_time_management() + && 2 * std::chrono::duration(t_end - t_start).count() + > moveOverhead; + }; + + std::list sts; + + // Step 0, do the rootMove, no correction allowed, as needed for MultiPV in TB. + auto& stRoot = sts.emplace_back(); + pos.do_move(rootMove.pv[0], stRoot); + int ply = 1; + + // Step 1, walk the PV to the last position in TB with correct decisive score + while (size_t(ply) < rootMove.pv.size()) + { + Move& pvMove = rootMove.pv[ply]; + + RootMoves legalMoves; + for (const auto& m : MoveList(pos)) + legalMoves.emplace_back(m); + + Tablebases::Config config = + Tablebases::rank_root_moves(options, pos, legalMoves, false, time_abort); + RootMove& rm = *std::find(legalMoves.begin(), legalMoves.end(), pvMove); + + if (legalMoves[0].tbRank != rm.tbRank) + break; + + ply++; + + auto& st = sts.emplace_back(); + pos.do_move(pvMove, st); + + // Do not allow for repetitions or drawing moves along the PV in TB regime + if (config.rootInTB && ((rule50 && pos.is_draw(ply)) || pos.is_repetition(ply))) + { + pos.undo_move(pvMove); + ply--; + break; + } + + // Full PV shown will thus be validated and end in TB. + // If we cannot validate the full PV in time, we do not show it. + if (config.rootInTB && time_abort()) + break; + } + + // Resize the PV to the correct part + rootMove.pv.resize(ply); + + // Step 2, now extend the PV to mate, as if the user explored syzygy-tables.info + // using top ranked moves (minimal DTZ), which gives optimal mates only for simple + // endgames e.g. KRvK. + while (!(rule50 && pos.is_draw(0))) + { + if (time_abort()) + break; + + RootMoves legalMoves; + for (const auto& m : MoveList(pos)) + { + auto& rm = legalMoves.emplace_back(m); + StateInfo tmpSI; + pos.do_move(m, tmpSI); + // Give a score of each move to break DTZ ties restricting opponent mobility, + // but not giving the opponent a capture. + for (const auto& mOpp : MoveList(pos)) + rm.tbRank -= pos.capture(mOpp) ? 100 : 1; + pos.undo_move(m); + } + + // Mate found + if (legalMoves.size() == 0) + break; + + // Sort moves according to their above assigned rank. + // This will break ties for moves with equal DTZ in rank_root_moves. + std::stable_sort( + legalMoves.begin(), legalMoves.end(), + [](const Search::RootMove& a, const Search::RootMove& b) { return a.tbRank > b.tbRank; }); + + // The winning side tries to minimize DTZ, the losing side maximizes it + Tablebases::Config config = + Tablebases::rank_root_moves(options, pos, legalMoves, true, time_abort); + + // If DTZ is not available we might not find a mate, so we bail out + if (!config.rootInTB || config.cardinality > 0) + break; + + ply++; + + Move& pvMove = legalMoves[0].pv[0]; + rootMove.pv.push_back(pvMove); + auto& st = sts.emplace_back(); + pos.do_move(pvMove, st); + } + + // Finding a draw in this function is an exceptional case, that cannot happen when rule50 is false or + // during engine game play, since we have a winning score, and play correctly + // with TB support. However, it can be that a position is draw due to the 50 move + // rule if it has been been reached on the board with a non-optimal 50 move counter + // (e.g. 8/8/6k1/3B4/3K4/4N3/8/8 w - - 54 106 ) which TB with dtz counter rounding + // cannot always correctly rank. See also + // https://github.com/official-stockfish/Stockfish/issues/5175#issuecomment-2058893495 + // We adjust the score to match the found PV. Note that a TB loss score can be + // displayed if the engine did not find a drawing move yet, but eventually search + // will figure it out (e.g. 1kq5/q2r4/5K2/8/8/8/8/7Q w - - 96 1 ) + if (pos.is_draw(0)) + v = VALUE_DRAW; + + // Undo the PV moves + for (auto it = rootMove.pv.rbegin(); it != rootMove.pv.rend(); ++it) + pos.undo_move(*it); + + // Inform if we couldn't get a full extension in time + if (time_abort()) + sync_cout + << "info string Syzygy based PV extension requires more time, increase Move Overhead as needed." + << sync_endl; +} + +void SearchManager::pv(Search::Worker& worker, + const ThreadPool& threads, + const TranspositionTable& tt, + Depth depth) { + + const auto nodes = Distributed::nodes_searched(threads); + 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); for (size_t i = 0; i < multiPV; ++i) { @@ -1941,29 +2197,47 @@ std::string SearchManager::pv(const Search::Worker& worker, bool tb = worker.tbConfig.rootInTB && std::abs(v) <= VALUE_TB; v = tb ? rootMoves[i].tbScore : v; - if (ss.rdbuf()->in_avail()) // Not at first line - ss << "\n"; + bool isExact = i != pvIdx || tb || !updated; // tablebase- and previous-scores are exact - ss << "info" - << " depth " << d << " seldepth " << rootMoves[i].selDepth << " multipv " << i + 1 - << " score " << UCI::to_score(v, pos); - - if (worker.options["UCI_ShowWDL"]) - ss << UCI::wdl(v, pos); - - if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact - ss << (rootMoves[i].scoreLowerbound - ? " lowerbound" - : (rootMoves[i].scoreUpperbound ? " upperbound" : "")); - - ss << " nodes " << nodes << " nps " << nodes * 1000 / time << " hashfull " << tt.hashfull() - << " tbhits " << tbHits << " time " << time << " pv"; + // Potentially correct and extend the PV, and in exceptional cases v + if (is_decisive(v) && std::abs(v) < VALUE_MATE_IN_MAX_PLY + && ((!rootMoves[i].scoreLowerbound && !rootMoves[i].scoreUpperbound) || isExact)) + syzygy_extend_pv(worker.options, worker.limits, pos, rootMoves[i], v); + std::string pv; for (Move m : rootMoves[i].pv) - ss << " " << UCI::move(m, pos.is_chess960()); - } + pv += UCIEngine::move(m, pos.is_chess960()) + " "; - return ss.str(); + // Remove last whitespace + if (!pv.empty()) + pv.pop_back(); + + auto wdl = worker.options["UCI_ShowWDL"] ? UCIEngine::wdl(v, pos) : ""; + auto bound = rootMoves[i].scoreLowerbound + ? "lowerbound" + : (rootMoves[i].scoreUpperbound ? "upperbound" : ""); + + InfoFull info; + + info.depth = d; + info.selDepth = rootMoves[i].selDepth; + info.multiPV = i + 1; + info.score = {v, pos}; + info.wdl = wdl; + + if (!isExact) + info.bound = bound; + + TimePoint time = std::max(TimePoint(1), tm.elapsed_time()); + info.timeMs = time; + info.nodes = nodes; + info.nps = nodes * 1000 / time; + info.tbHits = tbHits; + info.pv = pv; + info.hashfull = tt.hashfull(); + + updates.onUpdateFull(info); + } } // Called in case we have no ponder move before exiting the search, @@ -1973,27 +2247,93 @@ std::string SearchManager::pv(const Search::Worker& worker, bool RootMove::extract_ponder_from_tt(const TranspositionTable& tt, Position& pos) { StateInfo st; - ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); - - bool ttHit; assert(pv.size() == 1); if (pv[0] == Move::none()) return false; - pos.do_move(pv[0], st); - TTEntry* tte = tt.probe(pos.key(), ttHit); + pos.do_move(pv[0], st, &tt); + auto [ttHit, ttData, ttWriter] = tt.probe(pos.key()); if (ttHit) { - Move m = tte->move(); // Local copy to be SMP safe - if (MoveList(pos).contains(m)) - pv.push_back(m); + if (MoveList(pos).contains(ttData.move)) + pv.push_back(ttData.move); } pos.undo_move(pv[0]); return pv.size() > 1; } +std::vector Search::InfoFull::serialize() const { + std::vector 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& 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 diff --git a/src/search.h b/src/search.h index f966bcbf6..3ceeb7d58 100644 --- a/src/search.h +++ b/src/search.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -19,30 +19,34 @@ #ifndef SEARCH_H_INCLUDED #define SEARCH_H_INCLUDED +#include #include #include #include #include #include +#include +#include #include -#include -#include #include +#include +#include +#include #include "cluster.h" +#include "history.h" #include "misc.h" -#include "movepick.h" +#include "nnue/network.h" +#include "nnue/nnue_accumulator.h" +#include "numa.h" #include "position.h" +#include "score.h" #include "syzygy/tbprobe.h" #include "timeman.h" #include "types.h" namespace Stockfish { -namespace Eval::NNUE { -struct Networks; -} - // Different node types, used as a template parameter enum NodeType { NonPV, @@ -60,20 +64,20 @@ namespace Search { // shallower and deeper in the tree during the search. Each search thread has // its own array of Stack objects, indexed by the current ply. struct Stack { - Move* pv; - PieceToHistory* continuationHistory; - int ply; - Move currentMove; - Move excludedMove; - Move killers[2]; - Value staticEval; - int statScore; - int moveCount; - bool inCheck; - bool ttPv; - bool ttHit; - int multipleExtensions; - int cutoffCnt; + Move* pv; + PieceToHistory* continuationHistory; + CorrectionHistory* continuationCorrectionHistory; + int ply; + Move currentMove; + Move excludedMove; + Value staticEval; + int statScore; + int moveCount; + bool inCheck; + bool ttPv; + bool ttHit; + int cutoffCnt; + int reduction; }; @@ -91,15 +95,16 @@ struct RootMove { return m.score != score ? m.score < score : m.previousScore < previousScore; } - uint64_t effort = 0; - Value score = -VALUE_INFINITE; - Value previousScore = -VALUE_INFINITE; - Value averageScore = -VALUE_INFINITE; - Value uciScore = -VALUE_INFINITE; - bool scoreLowerbound = false; - bool scoreUpperbound = false; - int selDepth = 0; - int tbRank = 0; + uint64_t effort = 0; + Value score = -VALUE_INFINITE; + Value previousScore = -VALUE_INFINITE; + Value averageScore = -VALUE_INFINITE; + Value meanSquaredScore = -VALUE_INFINITE * VALUE_INFINITE; + Value uciScore = -VALUE_INFINITE; + bool scoreLowerbound = false; + bool scoreUpperbound = false; + int selDepth = 0; + int tbRank = 0; Value tbScore; std::vector pv; }; @@ -107,8 +112,7 @@ struct RootMove { using RootMoves = std::vector; -// LimitsType struct stores information sent by GUI about available time to -// search the current move, maximum depth/time, or if we are in analysis mode. +// LimitsType struct stores information sent by the caller about the analysis required. struct LimitsType { // Init explicitly due to broken value-initialization of non POD in MSVC @@ -119,33 +123,37 @@ struct LimitsType { ponderMode = false; } - bool use_time_management() const { return Cluster::is_root() && (time[WHITE] || time[BLACK]); } + bool use_time_management() const { + return Distributed::is_root() && (time[WHITE] || time[BLACK]); + } - std::vector searchmoves; - TimePoint time[COLOR_NB], inc[COLOR_NB], npmsec, movetime, startTime; - int movestogo, depth, mate, perft, infinite; - uint64_t nodes; - bool ponderMode; + std::vector searchmoves; + TimePoint time[COLOR_NB], inc[COLOR_NB], npmsec, movetime, startTime; + int movestogo, depth, mate, perft, infinite; + uint64_t nodes; + bool ponderMode; }; // The UCI stores the uci options, thread pool, and transposition table. // This struct is used to easily forward data to the Search::Worker class. struct SharedState { - SharedState(const OptionsMap& optionsMap, - ThreadPool& threadPool, - TranspositionTable& transpositionTable, - const Eval::NNUE::Networks& nets) : + SharedState(const OptionsMap& optionsMap, + ThreadPool& threadPool, + TranspositionTable& transpositionTable, + std::map& sharedHists, + const LazyNumaReplicatedSystemWide& nets) : options(optionsMap), threads(threadPool), tt(transpositionTable), + sharedHistories(sharedHists), networks(nets) {} - - const OptionsMap& options; - ThreadPool& threads; - TranspositionTable& tt; - const Eval::NNUE::Networks& networks; + const OptionsMap& options; + ThreadPool& threads; + TranspositionTable& tt; + std::map& sharedHistories; + const LazyNumaReplicatedSystemWide& networks; }; class Worker; @@ -158,18 +166,95 @@ class ISearchManager { virtual void check_time(Search::Worker&) = 0; }; +struct InfoShort { + int depth; + Score score; +}; + +struct InfoFull: InfoShort { + int selDepth; + size_t multiPV; + std::string_view wdl; + std::string_view bound; + size_t timeMs; + size_t nodes; + size_t nps; + size_t tbHits; + std::string_view pv; + int hashfull; + + std::vector 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& buf); +}; + +struct InfoIteration { + int depth; + std::string_view currmove; + size_t currmovenumber; +}; + +// Skill structure is used to implement strength limit. If we have a UCI_Elo, +// we convert it to an appropriate skill level, anchored to the Stash engine. +// This method is based on a fit of the Elo results for games played between +// Stockfish at various skill levels and various versions of the Stash engine. +// Skill 0 .. 19 now covers CCRL Blitz Elo from 1320 to 3190, approximately +// Reference: https://github.com/vondele/Stockfish/commit/a08b8d4e9711c2 +struct Skill { + // Lowest and highest Elo ratings used in the skill level calculation + constexpr static int LowestElo = 1320; + constexpr static int HighestElo = 3190; + + Skill(int skill_level, int uci_elo) { + if (uci_elo) + { + double e = double(uci_elo - LowestElo) / (HighestElo - LowestElo); + level = std::clamp((((37.2473 * e - 40.8525) * e + 22.2943) * e - 0.311438), 0.0, 19.0); + } + else + level = double(skill_level); + } + bool enabled() const { return level < 20.0; } + bool time_to_pick(Depth depth) const { return depth == 1 + int(level); } + Move pick_best(const RootMoves&, size_t multiPV); + + double level; + Move best = Move::none(); +}; + // SearchManager manages the search from the main thread. It is responsible for // keeping track of the time, and storing data strictly related to the main thread. class SearchManager: public ISearchManager { public: + using UpdateShort = std::function; + using UpdateFull = std::function; + using UpdateIter = std::function; + using UpdateBestmove = std::function; + + struct UpdateContext { + UpdateShort onUpdateNoMoves; + UpdateFull onUpdateFull; + UpdateIter onIter; + UpdateBestmove onBestmove; + }; + + + SearchManager(UpdateContext& updateContext) : + updates(updateContext) {} + void check_time(Search::Worker& worker) override; - std::string pv(const Search::Worker& worker, - const ThreadPool& threads, - const TranspositionTable& tt, - Depth depth) const; + void pv(Search::Worker& worker, + const ThreadPool& threads, + const TranspositionTable& tt, + Depth depth); Stockfish::TimeManagement tm; + double originalTimeAdjust; int callsCnt; std::atomic_bool ponder; @@ -180,6 +265,8 @@ class SearchManager: public ISearchManager { bool stopOnPonderhit; size_t id; + + UpdateContext& updates; }; class NullSearchManager: public ISearchManager { @@ -187,74 +274,94 @@ class NullSearchManager: public ISearchManager { void check_time(Search::Worker&) override {} }; - // Search::Worker is the class that does the actual search. // It is instantiated once per thread, and it is responsible for keeping track // of the search history, and storing data required for the search. class Worker { public: - Worker(SharedState&, std::unique_ptr, size_t); + Worker(SharedState&, + std::unique_ptr, + size_t, + size_t, + size_t, + NumaReplicatedAccessToken); - // Called at instantiation to initialize Reductions tables - // Reset histories, usually before a new game + // Called at instantiation to initialize reductions tables. + // Reset histories, usually before a new game. void clear(); // Called when the program receives the UCI 'go' command. // It searches from the root position and outputs the "bestmove". void start_searching(); - bool is_mainthread() const { return thread_idx == 0; } + bool is_mainthread() const { return threadIdx == 0; } + + void ensure_network_replicated(); // Public because they need to be updatable by the stats - CounterMoveHistory counterMoves; - ButterflyHistory mainHistory; - CapturePieceToHistory captureHistory; - ContinuationHistory continuationHistory[2][2]; - PawnHistory pawnHistory; - CorrectionHistory correctionHistory; + ButterflyHistory mainHistory; + LowPlyHistory lowPlyHistory; + + CapturePieceToHistory captureHistory; + ContinuationHistory continuationHistory[2][2]; + CorrectionHistory continuationCorrectionHistory; #ifdef USE_MPI struct { - std::mutex mutex; - Cluster::TTCache buffer = {}; + std::mutex mutex; + Distributed::TTCache buffer = {}; } ttCache; #endif std::atomic TTsaves; - friend void Cluster::save(TranspositionTable&, - ThreadPool&, - Search::Worker*, - TTEntry* tte, - Key k, - Value v, - bool PvHit, - Bound b, - Depth d, - Move m, - Value ev, - uint8_t generation8); + 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; private: void iterative_deepening(); - // Main search function for both PV and non-PV nodes + void do_move(Position& pos, const Move move, StateInfo& st, Stack* const ss); + void + do_move(Position& pos, const Move move, StateInfo& st, const bool givesCheck, Stack* const ss); + void do_null_move(Position& pos, StateInfo& st, Stack* const ss); + void undo_move(Position& pos, const Move move); + void undo_null_move(Position& pos); + + // This is the main search function, for both PV and non-PV nodes template Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode); // Quiescence search function, which is called by the main search template - Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth = 0); + Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta); - Depth reduction(bool i, Depth d, int mn, int delta); + Depth reduction(bool i, Depth d, int mn, int delta) const; - // Get a pointer to the search manager, only allowed to be called by the - // main thread. + // Pointer to the search manager, only allowed to be called by the main thread SearchManager* main_manager() const { - assert(thread_idx == 0); + assert(threadIdx == 0); return static_cast(manager.get()); } + TimePoint elapsed() const; + TimePoint elapsed_time() const; + + Value evaluate(const Position&); + LimitsType limits; size_t pvIdx, pvLast; @@ -269,7 +376,8 @@ class Worker { Depth rootDepth, completedDepth; Value rootDelta; - size_t thread_idx; + size_t threadIdx, numaThreadIdx, numaTotal; + NumaReplicatedAccessToken numaAccessToken; // Reductions lookup table initialized at startup std::array reductions; // [depth or moveNumber] @@ -279,15 +387,24 @@ class Worker { Tablebases::Config tbConfig; - const OptionsMap& options; - ThreadPool& threads; - TranspositionTable& tt; - const Eval::NNUE::Networks& networks; + const OptionsMap& options; + ThreadPool& threads; + TranspositionTable& tt; + const LazyNumaReplicatedSystemWide& networks; + + // Used by NNUE + Eval::NNUE::AccumulatorStack accumulatorStack; + Eval::NNUE::AccumulatorCaches refreshTable; friend class Stockfish::ThreadPool; friend class SearchManager; }; +struct ConthistBonus { + int index; + int weight; +}; + } // namespace Search diff --git a/src/shm.h b/src/shm.h new file mode 100644 index 000000000..9bf13f234 --- /dev/null +++ b/src/shm.h @@ -0,0 +1,635 @@ +/* + 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 . +*/ + +#ifndef SHM_H_INCLUDED +#define SHM_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) && !defined(__ANDROID__) + #include "shm_linux.h" +#endif + +#if defined(__ANDROID__) + #include + #define SF_MAX_SEM_NAME_LEN NAME_MAX +#endif + +#include "types.h" + +#include "memory.h" + +#if defined(_WIN32) + + #if _WIN32_WINNT < 0x0601 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0601 // Force to include needed API prototypes + #endif + + #if !defined(NOMINMAX) + #define NOMINMAX + #endif + #include +#else + #include + #include + #include + #include + #include + #include + #include +#endif + + +#if defined(__APPLE__) + #include + #include + +#elif defined(__sun) + #include + +#elif defined(__FreeBSD__) + #include + #include + #include + +#elif defined(__NetBSD__) || defined(__DragonFly__) || defined(__linux__) + #include + #include +#endif + + +namespace Stockfish { + +// argv[0] CANNOT be used because we need to identify the executable. +// argv[0] contains the command used to invoke it, which does not involve the full path. +// Just using a path is not fully resilient either, as the executable could +// have changed if it wasn't locked by the OS. Ideally we would hash the executable +// but it's not really that important at this point. +// If the path is longer than 4095 bytes the hash will be computed from an unspecified +// amount of bytes of the path; in particular it can a hash of an empty string. + +inline std::string getExecutablePathHash() { + char executable_path[4096] = {0}; + std::size_t path_length = 0; + +#if defined(_WIN32) + path_length = GetModuleFileNameA(NULL, executable_path, sizeof(executable_path)); + +#elif defined(__APPLE__) + uint32_t size = sizeof(executable_path); + if (_NSGetExecutablePath(executable_path, &size) == 0) + { + path_length = std::strlen(executable_path); + } + +#elif defined(__sun) // Solaris + const char* path = getexecname(); + if (path) + { + std::strncpy(executable_path, path, sizeof(executable_path) - 1); + path_length = std::strlen(executable_path); + } + +#elif defined(__FreeBSD__) + size_t size = sizeof(executable_path); + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; + if (sysctl(mib, 4, executable_path, &size, NULL, 0) == 0) + { + path_length = std::strlen(executable_path); + } + +#elif defined(__NetBSD__) || defined(__DragonFly__) + ssize_t len = readlink("/proc/curproc/exe", executable_path, sizeof(executable_path) - 1); + if (len >= 0) + { + executable_path[len] = '\0'; + path_length = len; + } + +#elif defined(__linux__) + ssize_t len = readlink("/proc/self/exe", executable_path, sizeof(executable_path) - 1); + if (len >= 0) + { + executable_path[len] = '\0'; + path_length = len; + } + +#endif + + // In case of any error the path will be empty. + return std::string(executable_path, path_length); +} + +enum class SystemWideSharedConstantAllocationStatus { + NoAllocation, + LocalMemory, + SharedMemory +}; + +#if defined(_WIN32) + +inline std::string GetLastErrorAsString(DWORD error) { + //Get the error message ID, if any. + DWORD errorMessageID = error; + if (errorMessageID == 0) + { + return std::string(); //No error message has been recorded + } + + LPSTR messageBuffer = nullptr; + + //Ask Win32 to give us the string version of that message ID. + //The parameters we pass in, tell Win32 to create the buffer that holds the message for us (because we don't yet know how long the message string will be). + size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR) &messageBuffer, 0, NULL); + + //Copy the error message into a std::string. + std::string message(messageBuffer, size); + + //Free the Win32's string's buffer. + LocalFree(messageBuffer); + + return message; +} + +// Utilizes shared memory to store the value. It is deduplicated system-wide (for the single user). +template +class SharedMemoryBackend { + public: + enum class Status { + Success, + LargePageAllocationError, + FileMappingError, + MapViewError, + MutexCreateError, + MutexWaitError, + MutexReleaseError, + NotInitialized + }; + + static constexpr DWORD IS_INITIALIZED_VALUE = 1; + + SharedMemoryBackend() : + status(Status::NotInitialized) {}; + + SharedMemoryBackend(const std::string& shm_name, const T& value) : + status(Status::NotInitialized) { + + initialize(shm_name, value); + } + + bool is_valid() const { return status == Status::Success; } + + std::optional get_error_message() const { + switch (status) + { + case Status::Success : + return std::nullopt; + case Status::LargePageAllocationError : + return "Failed to allocate large page memory"; + case Status::FileMappingError : + return "Failed to create file mapping: " + last_error_message; + case Status::MapViewError : + return "Failed to map view: " + last_error_message; + case Status::MutexCreateError : + return "Failed to create mutex: " + last_error_message; + case Status::MutexWaitError : + return "Failed to wait on mutex: " + last_error_message; + case Status::MutexReleaseError : + return "Failed to release mutex: " + last_error_message; + case Status::NotInitialized : + return "Not initialized"; + default : + return "Unknown error"; + } + } + + void* get() const { return is_valid() ? pMap : nullptr; } + + ~SharedMemoryBackend() { cleanup(); } + + SharedMemoryBackend(const SharedMemoryBackend&) = delete; + SharedMemoryBackend& operator=(const SharedMemoryBackend&) = delete; + + SharedMemoryBackend(SharedMemoryBackend&& other) noexcept : + pMap(other.pMap), + hMapFile(other.hMapFile), + status(other.status), + last_error_message(std::move(other.last_error_message)) { + + other.pMap = nullptr; + other.hMapFile = 0; + other.status = Status::NotInitialized; + } + + SharedMemoryBackend& operator=(SharedMemoryBackend&& other) noexcept { + if (this != &other) + { + cleanup(); + pMap = other.pMap; + hMapFile = other.hMapFile; + status = other.status; + last_error_message = std::move(other.last_error_message); + + other.pMap = nullptr; + other.hMapFile = 0; + other.status = Status::NotInitialized; + } + return *this; + } + + SystemWideSharedConstantAllocationStatus get_status() const { + return status == Status::Success ? SystemWideSharedConstantAllocationStatus::SharedMemory + : SystemWideSharedConstantAllocationStatus::NoAllocation; + } + + private: + void initialize(const std::string& shm_name, const T& value) { + const size_t total_size = sizeof(T) + sizeof(IS_INITIALIZED_VALUE); + + // Try allocating with large pages first. + hMapFile = windows_try_with_large_page_priviliges( + [&](size_t largePageSize) { + const size_t total_size_aligned = + (total_size + largePageSize - 1) / largePageSize * largePageSize; + + #if defined(_WIN64) + DWORD total_size_low = total_size_aligned & 0xFFFFFFFFu; + DWORD total_size_high = total_size_aligned >> 32u; + #else + DWORD total_size_low = total_size_aligned; + DWORD total_size_high = 0; + #endif + + return CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, + PAGE_READWRITE | SEC_COMMIT | SEC_LARGE_PAGES, + total_size_high, total_size_low, shm_name.c_str()); + }, + []() { return (void*) nullptr; }); + + // Fallback to normal allocation if no large pages available. + if (!hMapFile) + { + hMapFile = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, + static_cast(total_size), shm_name.c_str()); + } + + if (!hMapFile) + { + const DWORD err = GetLastError(); + last_error_message = GetLastErrorAsString(err); + status = Status::FileMappingError; + return; + } + + pMap = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, total_size); + if (!pMap) + { + const DWORD err = GetLastError(); + last_error_message = GetLastErrorAsString(err); + status = Status::MapViewError; + cleanup_partial(); + return; + } + + // Use named mutex to ensure only one initializer + std::string mutex_name = shm_name + "$mutex"; + HANDLE hMutex = CreateMutexA(NULL, FALSE, mutex_name.c_str()); + if (!hMutex) + { + const DWORD err = GetLastError(); + last_error_message = GetLastErrorAsString(err); + status = Status::MutexCreateError; + cleanup_partial(); + return; + } + + DWORD wait_result = WaitForSingleObject(hMutex, INFINITE); + if (wait_result != WAIT_OBJECT_0) + { + const DWORD err = GetLastError(); + last_error_message = GetLastErrorAsString(err); + status = Status::MutexWaitError; + CloseHandle(hMutex); + cleanup_partial(); + return; + } + + // Crucially, we place the object first to ensure alignment. + volatile DWORD* is_initialized = + std::launder(reinterpret_cast(reinterpret_cast(pMap) + sizeof(T))); + T* object = std::launder(reinterpret_cast(pMap)); + + if (*is_initialized != IS_INITIALIZED_VALUE) + { + // First time initialization, message for debug purposes + new (object) T{value}; + *is_initialized = IS_INITIALIZED_VALUE; + } + + BOOL release_result = ReleaseMutex(hMutex); + CloseHandle(hMutex); + + if (!release_result) + { + const DWORD err = GetLastError(); + last_error_message = GetLastErrorAsString(err); + status = Status::MutexReleaseError; + cleanup_partial(); + return; + } + + status = Status::Success; + } + + void cleanup_partial() { + if (pMap != nullptr) + { + UnmapViewOfFile(pMap); + pMap = nullptr; + } + if (hMapFile) + { + CloseHandle(hMapFile); + hMapFile = 0; + } + } + + void cleanup() { + if (pMap != nullptr) + { + UnmapViewOfFile(pMap); + pMap = nullptr; + } + if (hMapFile) + { + CloseHandle(hMapFile); + hMapFile = 0; + } + } + + void* pMap = nullptr; + HANDLE hMapFile = 0; + Status status = Status::NotInitialized; + std::string last_error_message; +}; + +#elif !defined(__ANDROID__) + +template +class SharedMemoryBackend { + public: + SharedMemoryBackend() = default; + + SharedMemoryBackend(const std::string& shm_name, const T& value) : + shm1(shm::create_shared(shm_name, value)) {} + + void* get() const { + const T* ptr = &shm1->get(); + return reinterpret_cast(const_cast(ptr)); + } + + bool is_valid() const { return shm1 && shm1->is_open() && shm1->is_initialized(); } + + SystemWideSharedConstantAllocationStatus get_status() const { + return is_valid() ? SystemWideSharedConstantAllocationStatus::SharedMemory + : SystemWideSharedConstantAllocationStatus::NoAllocation; + } + + std::optional get_error_message() const { + if (!shm1) + return "Shared memory not initialized"; + + if (!shm1->is_open()) + return "Shared memory is not open"; + + if (!shm1->is_initialized()) + return "Not initialized"; + + return std::nullopt; + } + + private: + std::optional> shm1; +}; + +#else + +// For systems that don't have shared memory, or support is troublesome. +// The way fallback is done is that we need a dummy backend. + +template +class SharedMemoryBackend { + public: + SharedMemoryBackend() = default; + + SharedMemoryBackend([[maybe_unused]] const std::string& shm_name, + [[maybe_unused]] const T& value) {} + + void* get() const { return nullptr; } + + bool is_valid() const { return false; } + + SystemWideSharedConstantAllocationStatus get_status() const { + return SystemWideSharedConstantAllocationStatus::NoAllocation; + } + + std::optional get_error_message() const { return "Dummy SharedMemoryBackend"; } +}; + +#endif + +template +struct SharedMemoryBackendFallback { + SharedMemoryBackendFallback() = default; + + SharedMemoryBackendFallback(const std::string&, const T& value) : + fallback_object(make_unique_large_page(value)) {} + + void* get() const { return fallback_object.get(); } + + SharedMemoryBackendFallback(const SharedMemoryBackendFallback&) = delete; + SharedMemoryBackendFallback& operator=(const SharedMemoryBackendFallback&) = delete; + + SharedMemoryBackendFallback(SharedMemoryBackendFallback&& other) noexcept : + fallback_object(std::move(other.fallback_object)) {} + + SharedMemoryBackendFallback& operator=(SharedMemoryBackendFallback&& other) noexcept { + fallback_object = std::move(other.fallback_object); + return *this; + } + + SystemWideSharedConstantAllocationStatus get_status() const { + return fallback_object == nullptr ? SystemWideSharedConstantAllocationStatus::NoAllocation + : SystemWideSharedConstantAllocationStatus::LocalMemory; + } + + std::optional get_error_message() const { + if (fallback_object == nullptr) + return "Not initialized"; + + return "Shared memory not supported by the OS. Local allocation fallback."; + } + + private: + LargePagePtr fallback_object; +}; + +// Platform-independent wrapper +template +struct SystemWideSharedConstant { + private: + static std::string createHashString(const std::string& input) { + size_t hash = std::hash{}(input); + + std::stringstream ss; + ss << std::hex << std::setfill('0') << hash; + + return ss.str(); + } + + public: + // We can't run the destructor because it may be in a completely different process. + // The object stored must also be obviously in-line but we can't check for that, other than some basic checks that cover most cases. + static_assert(std::is_trivially_destructible_v); + static_assert(std::is_trivially_move_constructible_v); + static_assert(std::is_trivially_copy_constructible_v); + + SystemWideSharedConstant() = default; + + + // Content is addressed by its hash. An additional discriminator can be added to account for differences + // 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{}(value); + std::size_t executable_hash = std::hash{}(getExecutablePathHash()); + + std::string shm_name = std::string("Local\\sf_") + std::to_string(content_hash) + "$" + + std::to_string(executable_hash) + "$" + + std::to_string(discriminator); + +#if !defined(_WIN32) + // POSIX shared memory names must start with a slash + shm_name = "/sf_" + createHashString(shm_name); + + // hash name and make sure it is not longer than SF_MAX_SEM_NAME_LEN + if (shm_name.size() > SF_MAX_SEM_NAME_LEN) + { + shm_name = shm_name.substr(0, SF_MAX_SEM_NAME_LEN - 1); + } +#endif + + SharedMemoryBackend shm_backend(shm_name, value); + + if (shm_backend.is_valid()) + { + backend = std::move(shm_backend); + } + else + { + backend = SharedMemoryBackendFallback(shm_name, value); + } + } + + SystemWideSharedConstant(const SystemWideSharedConstant&) = delete; + SystemWideSharedConstant& operator=(const SystemWideSharedConstant&) = delete; + + SystemWideSharedConstant(SystemWideSharedConstant&& other) noexcept : + backend(std::move(other.backend)) {} + + SystemWideSharedConstant& operator=(SystemWideSharedConstant&& other) noexcept { + backend = std::move(other.backend); + return *this; + } + + const T& operator*() const { return *std::launder(reinterpret_cast(get_ptr())); } + + bool operator==(std::nullptr_t) const noexcept { return get_ptr() == nullptr; } + + bool operator!=(std::nullptr_t) const noexcept { return get_ptr() != nullptr; } + + SystemWideSharedConstantAllocationStatus get_status() const { + return std::visit( + [](const auto& end) -> SystemWideSharedConstantAllocationStatus { + if constexpr (std::is_same_v, std::monostate>) + { + return SystemWideSharedConstantAllocationStatus::NoAllocation; + } + else + { + return end.get_status(); + } + }, + backend); + } + + std::optional get_error_message() const { + return std::visit( + [](const auto& end) -> std::optional { + if constexpr (std::is_same_v, std::monostate>) + { + return std::nullopt; + } + else + { + return end.get_error_message(); + } + }, + backend); + } + + private: + auto get_ptr() const { + return std::visit( + [](const auto& end) -> void* { + if constexpr (std::is_same_v, std::monostate>) + { + return nullptr; + } + else + { + return end.get(); + } + }, + backend); + } + + std::variant, SharedMemoryBackendFallback> backend; +}; + + +} // namespace Stockfish + +#endif // #ifndef SHM_H_INCLUDED diff --git a/src/shm_linux.h b/src/shm_linux.h new file mode 100644 index 000000000..1e344e93f --- /dev/null +++ b/src/shm_linux.h @@ -0,0 +1,658 @@ +/* + 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 . +*/ + +#ifndef SHM_LINUX_H_INCLUDED +#define SHM_LINUX_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if defined(__NetBSD__) || defined(__DragonFly__) || defined(__linux__) + #include + #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 + + +namespace Stockfish::shm { + +namespace detail { + +struct ShmHeader { + static constexpr uint32_t SHM_MAGIC = 0xAD5F1A12; + pthread_mutex_t mutex; + std::atomic ref_count{0}; + std::atomic initialized{false}; + uint32_t magic = SHM_MAGIC; +}; + +class SharedMemoryBase { + public: + virtual ~SharedMemoryBase() = default; + virtual void close() noexcept = 0; + virtual const std::string& name() const noexcept = 0; +}; + +class SharedMemoryRegistry { + private: + static std::mutex registry_mutex_; + static std::unordered_set active_instances_; + + public: + static void register_instance(SharedMemoryBase* instance) { + std::scoped_lock lock(registry_mutex_); + active_instances_.insert(instance); + } + + static void unregister_instance(SharedMemoryBase* instance) { + std::scoped_lock lock(registry_mutex_); + active_instances_.erase(instance); + } + + static void cleanup_all() noexcept { + std::scoped_lock lock(registry_mutex_); + for (auto* instance : active_instances_) + instance->close(); + active_instances_.clear(); + } +}; + +inline std::mutex SharedMemoryRegistry::registry_mutex_; +inline std::unordered_set 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); + } + + static void register_signal_handlers() noexcept { + std::atexit([]() { SharedMemoryRegistry::cleanup_all(); }); + + constexpr int signals[] = {SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGABRT, SIGFPE, + SIGSEGV, SIGTERM, SIGBUS, SIGSYS, SIGXCPU, SIGXFSZ}; + + struct sigaction sa; + sa.sa_handler = handle_signal; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + for (int sig : signals) + sigaction(sig, &sa, nullptr); + } + + public: + static void ensure_registered() noexcept { + std::call_once(register_once_, register_signal_handlers); + } +}; + +inline std::once_flag CleanupHooks::register_once_; + + +inline int portable_fallocate(int fd, off_t offset, off_t length) { +#ifdef __APPLE__ + fstore_t store = {F_ALLOCATECONTIG, F_PEOFPOSMODE, offset, length, 0}; + int ret = fcntl(fd, F_PREALLOCATE, &store); + if (ret == -1) + { + store.fst_flags = F_ALLOCATEALL; + ret = fcntl(fd, F_PREALLOCATE, &store); + } + if (ret != -1) + ret = ftruncate(fd, offset + length); + return ret; +#else + return posix_fallocate(fd, offset, length); +#endif +} + +} // namespace detail + +template +class SharedMemory: public detail::SharedMemoryBase { + static_assert(std::is_trivially_copyable_v, "T must be trivially copyable"); + static_assert(!std::is_pointer_v, "T cannot be a pointer type"); + + private: + std::string name_; + int fd_ = -1; + void* mapped_ptr_ = nullptr; + T* data_ptr_ = nullptr; + detail::ShmHeader* header_ptr_ = nullptr; + size_t total_size_ = 0; + std::string sentinel_base_; + std::string sentinel_path_; + + static constexpr size_t calculate_total_size() noexcept { + return sizeof(T) + sizeof(detail::ShmHeader); + } + + static std::string make_sentinel_base(const std::string& name) { + uint64_t hash = std::hash{}(name); + char buf[32]; + std::snprintf(buf, sizeof(buf), "sfshm_%016" PRIx64, static_cast(hash)); + return buf; + } + + public: + explicit SharedMemory(const std::string& name) noexcept : + name_(name), + total_size_(calculate_total_size()), + sentinel_base_(make_sentinel_base(name)) {} + + ~SharedMemory() noexcept override { + detail::SharedMemoryRegistry::unregister_instance(this); + close(); + } + + SharedMemory(const SharedMemory&) = delete; + SharedMemory& operator=(const SharedMemory&) = delete; + + SharedMemory(SharedMemory&& other) noexcept : + name_(std::move(other.name_)), + fd_(other.fd_), + mapped_ptr_(other.mapped_ptr_), + data_ptr_(other.data_ptr_), + header_ptr_(other.header_ptr_), + total_size_(other.total_size_), + sentinel_base_(std::move(other.sentinel_base_)), + sentinel_path_(std::move(other.sentinel_path_)) { + + detail::SharedMemoryRegistry::unregister_instance(&other); + detail::SharedMemoryRegistry::register_instance(this); + other.reset(); + } + + SharedMemory& operator=(SharedMemory&& other) noexcept { + if (this != &other) + { + detail::SharedMemoryRegistry::unregister_instance(this); + close(); + + name_ = std::move(other.name_); + fd_ = other.fd_; + mapped_ptr_ = other.mapped_ptr_; + data_ptr_ = other.data_ptr_; + header_ptr_ = other.header_ptr_; + total_size_ = other.total_size_; + sentinel_base_ = std::move(other.sentinel_base_); + sentinel_path_ = std::move(other.sentinel_path_); + + detail::SharedMemoryRegistry::unregister_instance(&other); + detail::SharedMemoryRegistry::register_instance(this); + + other.reset(); + } + return *this; + } + + [[nodiscard]] bool open(const T& initial_value) noexcept { + detail::CleanupHooks::ensure_registered(); + + bool retried_stale = false; + + while (true) + { + if (is_open()) + return false; + + bool created_new = false; + fd_ = shm_open(name_.c_str(), O_CREAT | O_EXCL | O_RDWR, 0666); + + if (fd_ == -1) + { + fd_ = shm_open(name_.c_str(), O_RDWR, 0666); + if (fd_ == -1) + return false; + } + else + created_new = true; + + if (!lock_file(LOCK_EX)) + { + ::close(fd_); + reset(); + return false; + } + + bool invalid_header = false; + bool success = + created_new ? setup_new_region(initial_value) : setup_existing_region(invalid_header); + + if (!success) + { + if (created_new || invalid_header) + shm_unlink(name_.c_str()); + if (mapped_ptr_) + unmap_region(); + unlock_file(); + ::close(fd_); + reset(); + + if (!created_new && invalid_header && !retried_stale) + { + retried_stale = true; + continue; + } + return false; + } + + if (!lock_shared_mutex()) + { + if (created_new) + shm_unlink(name_.c_str()); + if (mapped_ptr_) + unmap_region(); + unlock_file(); + ::close(fd_); + reset(); + + if (!created_new && !retried_stale) + { + retried_stale = true; + continue; + } + return false; + } + + if (!create_sentinel_file_locked()) + { + unlock_shared_mutex(); + unmap_region(); + if (created_new) + shm_unlink(name_.c_str()); + unlock_file(); + ::close(fd_); + reset(); + return false; + } + + header_ptr_->ref_count.fetch_add(1, std::memory_order_acq_rel); + + unlock_shared_mutex(); + unlock_file(); + detail::SharedMemoryRegistry::register_instance(this); + return true; + } + } + + void close() noexcept override { + if (fd_ == -1 && mapped_ptr_ == nullptr) + return; + + bool remove_region = false; + bool file_locked = lock_file(LOCK_EX); + bool mutex_locked = false; + + if (file_locked && header_ptr_ != nullptr) + mutex_locked = lock_shared_mutex(); + + if (mutex_locked) + { + if (header_ptr_) + { + header_ptr_->ref_count.fetch_sub(1, std::memory_order_acq_rel); + } + remove_sentinel_file(); + remove_region = !has_other_live_sentinels_locked(); + unlock_shared_mutex(); + } + else + { + remove_sentinel_file(); + decrement_refcount_relaxed(); + } + + unmap_region(); + + if (remove_region) + shm_unlink(name_.c_str()); + + if (file_locked) + unlock_file(); + + if (fd_ != -1) + { + ::close(fd_); + fd_ = -1; + } + + reset(); + } + + const std::string& name() const noexcept override { return name_; } + + [[nodiscard]] bool is_open() const noexcept { return fd_ != -1 && mapped_ptr_ && data_ptr_; } + + [[nodiscard]] const T& get() const noexcept { return *data_ptr_; } + + [[nodiscard]] const T* operator->() const noexcept { return data_ptr_; } + + [[nodiscard]] const T& operator*() const noexcept { return *data_ptr_; } + + [[nodiscard]] uint32_t ref_count() const noexcept { + return header_ptr_ ? header_ptr_->ref_count.load(std::memory_order_acquire) : 0; + } + + [[nodiscard]] bool is_initialized() const noexcept { + return header_ptr_ ? header_ptr_->initialized.load(std::memory_order_acquire) : false; + } + + static void cleanup_all_instances() noexcept { detail::SharedMemoryRegistry::cleanup_all(); } + + private: + void reset() noexcept { + fd_ = -1; + mapped_ptr_ = nullptr; + data_ptr_ = nullptr; + header_ptr_ = nullptr; + sentinel_path_.clear(); + } + + void unmap_region() noexcept { + if (mapped_ptr_) + { + munmap(mapped_ptr_, total_size_); + mapped_ptr_ = nullptr; + data_ptr_ = nullptr; + header_ptr_ = nullptr; + } + } + + [[nodiscard]] bool lock_file(int operation) noexcept { + if (fd_ == -1) + return false; + + while (flock(fd_, operation) == -1) + { + if (errno == EINTR) + continue; + return false; + } + return true; + } + + void unlock_file() noexcept { + if (fd_ == -1) + return; + + while (flock(fd_, LOCK_UN) == -1) + { + if (errno == EINTR) + continue; + break; + } + } + + 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; + } + + void decrement_refcount_relaxed() noexcept { + if (!header_ptr_) + return; + + uint32_t expected = header_ptr_->ref_count.load(std::memory_order_relaxed); + while (expected != 0 + && !header_ptr_->ref_count.compare_exchange_weak( + expected, expected - 1, std::memory_order_acq_rel, std::memory_order_relaxed)) + {} + } + + bool create_sentinel_file_locked() noexcept { + if (!header_ptr_) + return false; + + const pid_t self_pid = getpid(); + sentinel_path_ = sentinel_full_path(self_pid); + + for (int attempt = 0; attempt < 2; ++attempt) + { + int fd = ::open(sentinel_path_.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600); + if (fd != -1) + { + ::close(fd); + return true; + } + + if (errno == EEXIST) + { + ::unlink(sentinel_path_.c_str()); + decrement_refcount_relaxed(); + continue; + } + + break; + } + + sentinel_path_.clear(); + return false; + } + + void remove_sentinel_file() noexcept { + if (!sentinel_path_.empty()) + { + ::unlink(sentinel_path_.c_str()); + sentinel_path_.clear(); + } + } + + static bool pid_is_alive(pid_t pid) noexcept { + if (pid <= 0) + return false; + + if (kill(pid, 0) == 0) + return true; + + return errno == EPERM; + } + + [[nodiscard]] bool initialize_shared_mutex() noexcept { + if (!header_ptr_) + return false; + + pthread_mutexattr_t attr; + if (pthread_mutexattr_init(&attr) != 0) + return false; + + bool success = pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) == 0; +#if _POSIX_C_SOURCE >= 200809L + if (success) + success = pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST) == 0; +#endif + + if (success) + success = pthread_mutex_init(&header_ptr_->mutex, &attr) == 0; + + pthread_mutexattr_destroy(&attr); + return success; + } + + [[nodiscard]] bool lock_shared_mutex() noexcept { + if (!header_ptr_) + return false; + + while (true) + { + int rc = pthread_mutex_lock(&header_ptr_->mutex); + if (rc == 0) + return true; + +#if _POSIX_C_SOURCE >= 200809L + if (rc == EOWNERDEAD) + { + if (pthread_mutex_consistent(&header_ptr_->mutex) == 0) + return true; + return false; + } +#endif + + if (rc == EINTR) + continue; + + return false; + } + } + + void unlock_shared_mutex() noexcept { + if (header_ptr_) + pthread_mutex_unlock(&header_ptr_->mutex); + } + + bool has_other_live_sentinels_locked() const noexcept { + DIR* dir = opendir("/dev/shm"); + if (!dir) + return false; + + std::string prefix = sentinel_base_ + "."; + bool found = false; + + while (dirent* entry = readdir(dir)) + { + std::string name = entry->d_name; + if (name.rfind(prefix, 0) != 0) + continue; + + auto pid_str = name.substr(prefix.size()); + char* end = nullptr; + long value = std::strtol(pid_str.c_str(), &end, 10); + if (!end || *end != '\0') + continue; + + pid_t pid = static_cast(value); + if (pid_is_alive(pid)) + { + found = true; + break; + } + + std::string stale_path = std::string("/dev/shm/") + name; + ::unlink(stale_path.c_str()); + const_cast(this)->decrement_refcount_relaxed(); + } + + closedir(dir); + return found; + } + + [[nodiscard]] bool setup_new_region(const T& initial_value) noexcept { + if (ftruncate(fd_, static_cast(total_size_)) == -1) + return false; + + if (detail::portable_fallocate(fd_, 0, static_cast(total_size_)) != 0) + return false; + + mapped_ptr_ = mmap(nullptr, total_size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0); + if (mapped_ptr_ == MAP_FAILED) + { + mapped_ptr_ = nullptr; + return false; + } + + data_ptr_ = static_cast(mapped_ptr_); + header_ptr_ = + reinterpret_cast(static_cast(mapped_ptr_) + sizeof(T)); + + new (header_ptr_) detail::ShmHeader{}; + new (data_ptr_) T{initial_value}; + + if (!initialize_shared_mutex()) + return false; + + header_ptr_->ref_count.store(0, std::memory_order_release); + header_ptr_->initialized.store(true, std::memory_order_release); + return true; + } + + [[nodiscard]] bool setup_existing_region(bool& invalid_header) noexcept { + invalid_header = false; + + struct stat st; + fstat(fd_, &st); + if (static_cast(st.st_size) < total_size_) + { + invalid_header = true; + return false; + } + + mapped_ptr_ = mmap(nullptr, total_size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0); + if (mapped_ptr_ == MAP_FAILED) + { + mapped_ptr_ = nullptr; + return false; + } + + data_ptr_ = static_cast(mapped_ptr_); + header_ptr_ = std::launder( + reinterpret_cast(static_cast(mapped_ptr_) + sizeof(T))); + + if (!header_ptr_->initialized.load(std::memory_order_acquire) + || header_ptr_->magic != detail::ShmHeader::SHM_MAGIC) + { + invalid_header = true; + unmap_region(); + return false; + } + + return true; + } +}; + +template +[[nodiscard]] std::optional> create_shared(const std::string& name, + const T& initial_value) noexcept { + SharedMemory shm(name); + if (shm.open(initial_value)) + return shm; + return std::nullopt; +} + +} // namespace Stockfish::shm + +#endif // #ifndef SHM_LINUX_H_INCLUDED diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index fe8d1ddb5..007a891e5 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -35,6 +35,7 @@ #include #include #include +#include #include "../bitboard.h" #include "../cluster.h" @@ -67,7 +68,7 @@ namespace { constexpr int TBPIECES = 7; // Max number of supported pieces constexpr int MAX_DTZ = - 1 << 18; // Max DTZ supported, large enough to deal with the syzygy TB limit. + 1 << 18; // Max DTZ supported times 2, large enough to deal with the syzygy TB limit. enum { BigEndian, @@ -444,6 +445,8 @@ class TBTables { std::deque> wdlTable; std::deque> dtzTable; + size_t foundDTZFiles = 0; + size_t foundWDLFiles = 0; void insert(Key key, TBTable* wdl, TBTable* dtz) { uint32_t homeBucket = uint32_t(key) & (Size - 1); @@ -487,9 +490,17 @@ class TBTables { memset(hashTable, 0, sizeof(hashTable)); wdlTable.clear(); dtzTable.clear(); + foundDTZFiles = 0; + foundWDLFiles = 0; } - size_t size() const { return wdlTable.size(); } - void add(const std::vector& pieces); + + 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; + } + + void add(const std::vector& pieces); }; TBTables TBTables; @@ -502,13 +513,22 @@ void TBTables::add(const std::vector& pieces) { for (PieceType pt : pieces) code += PieceToChar[pt]; + code.insert(code.find('K', 1), "v"); - TBFile file(code.insert(code.find('K', 1), "v") + ".rtbw"); // KRK -> KRvK + TBFile file_dtz(code + ".rtbz"); // KRK -> KRvK + if (file_dtz.is_open()) + { + file_dtz.close(); + foundDTZFiles++; + } + + TBFile file(code + ".rtbw"); // KRK -> KRvK if (!file.is_open()) // Only WDL file is checked return; file.close(); + foundWDLFiles++; MaxCardinality = std::max(int(pieces.size()), MaxCardinality); @@ -567,7 +587,7 @@ int decompress_pairs(PairsData* d, uint64_t idx) { // idx = k * d->span + idx % d->span (2) // // So from (1) and (2) we can compute idx - I(K): - int diff = idx % d->span - d->span / 2; + int diff = int(idx % d->span - d->span / 2); // Sum the above to offset to find the offset corresponding to our idx offset += diff; @@ -1075,7 +1095,7 @@ uint8_t* set_sizes(PairsData* d, uint8_t* data) { // See https://web.archive.org/web/20201106232444/http://www.larsson.dogma.net/dcc99.pdf std::vector visited(d->symlen.size()); - for (std::size_t sym = 0; sym < d->symlen.size(); ++sym) + for (Sym sym = 0; sym < d->symlen.size(); ++sym) if (!visited[sym]) d->symlen[sym] = set_symlen(d, sym, visited); @@ -1197,6 +1217,8 @@ template void* mapped(TBTable& e, const Position& pos) { static std::mutex mutex; + // Because TB is the only usage of materialKey, check it here in debug mode + assert(pos.material_key_is_ok()); // Use 'acquire' to avoid a thread reading 'ready' == true while // another is still working. (compiler reordering may cause this). @@ -1327,7 +1349,7 @@ void Tablebases::init(const std::string& paths) { MaxCardinality = 0; TBFile::Paths = paths; - if (paths.empty() || paths == "") + if (paths.empty()) return; // MapB1H1H7[] encodes a square below a1-h8 diagonal to 0..27 @@ -1467,8 +1489,7 @@ void Tablebases::init(const std::string& paths) { } } - if (Cluster::is_root()) - sync_cout << "info string Found " << TBTables.size() << " tablebases" << sync_endl; + TBTables.info(); } // Probe the WDL table for a particular position. @@ -1576,7 +1597,11 @@ int Tablebases::probe_dtz(Position& pos, ProbeState* result) { // Use the DTZ tables to rank root moves. // // A return value false indicates that not all probes were successful. -bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50) { +bool Tablebases::root_probe(Position& pos, + Search::RootMoves& rootMoves, + bool rule50, + bool rankDTZ, + const std::function& time_abort) { ProbeState result = OK; StateInfo st; @@ -1587,7 +1612,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool ru // Check whether a position was repeated since the last zeroing move. bool rep = pos.has_repeated(); - int dtz, bound = rule50 ? (MAX_DTZ - 100) : 1; + int dtz, bound = rule50 ? (MAX_DTZ / 2 - 100) : 1; // Probe and rank each move for (auto& m : rootMoves) @@ -1601,7 +1626,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool ru WDLScore wdl = -probe_wdl(pos, &result); dtz = dtz_before_zeroing(wdl); } - else if (pos.is_draw(1)) + else if ((rule50 && pos.is_draw(1)) || pos.is_repetition(1)) { // In case a root move leads to a draw by repetition or 50-move rule, // we set dtz to zero. Note: since we are only 1 ply from the root, @@ -1621,13 +1646,15 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool ru pos.undo_move(m.pv[0]); - if (result == FAIL) + if (time_abort() || result == FAIL) return false; // Better moves are ranked higher. Certain wins are ranked equally. // Losing moves are ranked equally unless a 50-move draw is in sight. - int r = dtz > 0 ? (dtz + cnt50 <= 99 && !rep ? MAX_DTZ : MAX_DTZ - (dtz + cnt50)) - : dtz < 0 ? (-dtz * 2 + cnt50 < 100 ? -MAX_DTZ : -MAX_DTZ + (-dtz + cnt50)) + int r = dtz > 0 ? (dtz + cnt50 <= 99 && !rep ? MAX_DTZ - (rankDTZ ? dtz : 0) + : MAX_DTZ / 2 - (dtz + cnt50)) + : dtz < 0 ? (-dtz * 2 + cnt50 < 100 ? -MAX_DTZ - (rankDTZ ? dtz : 0) + : -MAX_DTZ / 2 + (-dtz + cnt50)) : 0; m.tbRank = r; @@ -1635,10 +1662,11 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool ru // 1 cp to cursed wins and let it grow to 49 cp as the positions gets // closer to a real win. m.tbScore = r >= bound ? VALUE_MATE - MAX_PLY - 1 - : r > 0 ? Value((std::max(3, r - (MAX_DTZ - 200)) * int(PawnValue)) / 200) - : r == 0 ? VALUE_DRAW - : r > -bound ? Value((std::min(-3, r + (MAX_DTZ - 200)) * int(PawnValue)) / 200) - : -VALUE_MATE + MAX_PLY + 1; + : r > 0 ? Value((std::max(3, r - (MAX_DTZ / 2 - 200)) * int(PawnValue)) / 200) + : r == 0 ? VALUE_DRAW + : r > -bound + ? Value((std::min(-3, r + (MAX_DTZ / 2 - 200)) * int(PawnValue)) / 200) + : -VALUE_MATE + MAX_PLY + 1; } return true; @@ -1683,9 +1711,11 @@ bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves, boo return true; } -Config Tablebases::rank_root_moves(const OptionsMap& options, - Position& pos, - Search::RootMoves& rootMoves) { +Config Tablebases::rank_root_moves(const OptionsMap& options, + Position& pos, + Search::RootMoves& rootMoves, + bool rankDTZ, + const std::function& time_abort) { Config config; if (rootMoves.empty()) @@ -1708,10 +1738,11 @@ Config Tablebases::rank_root_moves(const OptionsMap& options, if (config.cardinality >= popcount(pos.pieces()) && !pos.can_castle(ANY_CASTLING)) { - // Rank moves using DTZ tables - config.rootInTB = root_probe(pos, rootMoves, options["Syzygy50MoveRule"]); + // Rank moves using DTZ tables, bail out if time_abort flags zeitnot + config.rootInTB = + root_probe(pos, rootMoves, options["Syzygy50MoveRule"], rankDTZ, time_abort); - if (!config.rootInTB) + if (!config.rootInTB && !time_abort()) { // DTZ tables are missing; try to rank moves using WDL tables dtz_available = false; diff --git a/src/syzygy/tbprobe.h b/src/syzygy/tbprobe.h index e10950f4e..7b60d6e20 100644 --- a/src/syzygy/tbprobe.h +++ b/src/syzygy/tbprobe.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -19,6 +19,7 @@ #ifndef TBPROBE_H #define TBPROBE_H +#include #include #include @@ -66,9 +67,18 @@ extern int MaxCardinality; void init(const std::string& paths); WDLScore probe_wdl(Position& pos, ProbeState* result); int probe_dtz(Position& pos, ProbeState* result); -bool root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50); +bool root_probe(Position& pos, + Search::RootMoves& rootMoves, + bool rule50, + bool rankDTZ, + const std::function& time_abort); bool root_probe_wdl(Position& pos, Search::RootMoves& rootMoves, bool rule50); -Config rank_root_moves(const OptionsMap& options, Position& pos, Search::RootMoves& rootMoves); +Config rank_root_moves( + const OptionsMap& options, + Position& pos, + Search::RootMoves& rootMoves, + bool rankDTZ = false, + const std::function& time_abort = []() { return false; }); } // namespace Stockfish::Tablebases diff --git a/src/thread.cpp b/src/thread.cpp index 59aeeae46..94a151346 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -21,18 +21,23 @@ #include #include #include +#include #include +#include #include #include +#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" #include "timeman.h" -#include "tt.h" #include "types.h" +#include "uci.h" #include "ucioption.h" namespace Stockfish { @@ -41,13 +46,28 @@ namespace Stockfish { // in idle_loop(). Note that 'searching' and 'exit' should be already set. Thread::Thread(Search::SharedState& sharedState, std::unique_ptr sm, - size_t n) : - worker(std::make_unique(sharedState, std::move(sm), n)), + size_t n, + size_t numaN, + size_t totalNumaCount, + OptionalThreadToNumaNodeBinder binder) : idx(n), + idxInNuma(numaN), + totalNuma(totalNumaCount), nthreads(sharedState.options["Threads"]), stdThread(&Thread::idle_loop, this) { wait_for_search_finished(); + + run_custom_job([this, &binder, &sharedState, &sm, n]() { + // Use the binder to [maybe] bind the threads to a NUMA node before doing + // the Worker allocation. Ideally we would also allocate the SearchManager + // here, but that's minor. + this->numaAccessToken = binder(); + this->worker = make_unique_large_page( + sharedState, std::move(sm), n, idxInNuma, totalNuma, this->numaAccessToken); + }); + + wait_for_search_finished(); } @@ -62,38 +82,42 @@ Thread::~Thread() { stdThread.join(); } - // Wakes up the thread that will start the search void Thread::start_searching() { - mutex.lock(); - searching = true; - mutex.unlock(); // Unlock before notifying saves a few CPU-cycles - cv.notify_one(); // Wake up the thread in idle_loop() + assert(worker != nullptr); + run_custom_job([this]() { worker->start_searching(); }); } +// Clears the histories for the thread worker (usually before a new game) +void Thread::clear_worker() { + assert(worker != nullptr); + run_custom_job([this]() { worker->clear(); }); +} -// Blocks on the condition variable -// until the thread has finished searching. +// Blocks on the condition variable until the thread has finished searching void Thread::wait_for_search_finished() { std::unique_lock lk(mutex); cv.wait(lk, [&] { return !searching; }); } +// Launching a function in the thread +void Thread::run_custom_job(std::function f) { + { + std::unique_lock lk(mutex); + cv.wait(lk, [&] { return !searching; }); + jobFunc = std::move(f); + searching = true; + } + cv.notify_one(); +} -// Thread gets parked here, blocked on the -// condition variable, when it has no work to do. +void Thread::ensure_network_replicated() { worker->ensure_network_replicated(); } + +// Thread gets parked here, blocked on the condition variable +// when the thread has no work to do. void Thread::idle_loop() { - - // If OS already scheduled us on a different group than 0 then don't overwrite - // the choice, eventually we are one of many one-threaded processes running on - // some Windows NUMA hardware, for instance in fishtest. To make it simple, - // just check if running threads are below a threshold, in this case, all this - // NUMA machinery is not needed. - if (nthreads > 8) - WinProcGroup::bind_this_thread(idx); - while (true) { std::unique_lock lk(mutex); @@ -104,74 +128,155 @@ void Thread::idle_loop() { if (exit) return; + std::function job = std::move(jobFunc); + jobFunc = nullptr; + lk.unlock(); - worker->start_searching(); + if (job) + job(); } } -Search::SearchManager* ThreadPool::main_manager() { - return static_cast(main_thread()->worker.get()->manager.get()); -} +Search::SearchManager* ThreadPool::main_manager() { return main_thread()->worker->main_manager(); } 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(Search::SharedState sharedState) { +void ThreadPool::set(const NumaConfig& numaConfig, + Search::SharedState sharedState, + Search::SearchManager::UpdateContext& updateContext) { if (threads.size() > 0) // destroy any existing thread(s) { main_thread()->wait_for_search_finished(); - while (threads.size() > 0) - delete threads.back(), threads.pop_back(); + threads.clear(); + + boundThreadToNumaNode.clear(); } const size_t requested = sharedState.options["Threads"]; if (requested > 0) // create new thread(s) { - threads.push_back(new Thread( - sharedState, std::unique_ptr(new Search::SearchManager()), 0)); + // Binding threads may be problematic when there's multiple NUMA nodes and + // multiple Stockfish instances running. In particular, if each instance + // runs a single thread then they would all be mapped to the first NUMA node. + // This is undesirable, and so the default behaviour (i.e. when the user does not + // change the NumaConfig UCI setting) is to not bind the threads to processors + // unless we know for sure that we span NUMA nodes and replication is required. + const std::string numaPolicy(sharedState.options["NumaPolicy"]); + const bool doBindThreads = [&]() { + if (numaPolicy == "none") + return false; + if (numaPolicy == "auto") + return numaConfig.suggests_binding_threads(requested); + + // numaPolicy == "system", or explicitly set by the user + return true; + }(); + + std::map counts; + boundThreadToNumaNode = doBindThreads + ? numaConfig.distribute_threads_among_numa_nodes(requested) + : std::vector{}; + + if (boundThreadToNumaNode.empty()) + counts[0] = requested; // Pretend all threads are part of numa node 0 + else + { + for (size_t i = 0; i < boundThreadToNumaNode.size(); ++i) + counts[boundThreadToNumaNode[i]]++; + } + + sharedState.sharedHistories.clear(); + for (auto pair : counts) + { + NumaIndex numaIndex = pair.first; + uint64_t count = pair.second; + auto f = [&]() { + sharedState.sharedHistories.try_emplace(numaIndex, next_power_of_two(count)); + }; + if (doBindThreads) + numaConfig.execute_on_numa_node(numaIndex, f); + else + f(); + } + + auto threadsPerNode = counts; + counts.clear(); while (threads.size() < requested) - threads.push_back(new Thread( - sharedState, std::unique_ptr(new Search::NullSearchManager()), - threads.size())); + { + const size_t threadId = threads.size(); + const NumaIndex numaId = doBindThreads ? boundThreadToNumaNode[threadId] : 0; + auto manager = threadId == 0 ? std::unique_ptr( + std::make_unique(updateContext)) + : std::make_unique(); + + // 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(sharedState, std::move(manager), threadId, + counts[numaId]++, threadsPerNode[numaId], + binder)); + } + clear(); main_thread()->wait_for_search_finished(); - - // Reallocate the hash with the new threadpool size - sharedState.tt.resize(sharedState.options["Hash"], requested); - - // Adjust cluster buffers - Cluster::ttSendRecvBuff_resize(requested); } } // Sets threadPool data to initial values void ThreadPool::clear() { + if (threads.size() == 0) + return; - for (Thread* th : threads) - th->worker->clear(); + for (auto&& th : threads) + th->clear_worker(); - main_manager()->callsCnt = 0; - main_manager()->bestPreviousScore = VALUE_INFINITE; + for (auto&& th : threads) + th->wait_for_search_finished(); + + // These two affect the time taken on the first move of a game: main_manager()->bestPreviousAverageScore = VALUE_INFINITE; - main_manager()->previousTimeReduction = 1.0; + main_manager()->previousTimeReduction = 0.85; + + main_manager()->callsCnt = 0; + main_manager()->bestPreviousScore = VALUE_INFINITE; + main_manager()->originalTimeAdjust = -1; main_manager()->tm.clear(); } +void ThreadPool::run_on_thread(size_t threadId, std::function f) { + assert(threads.size() > threadId); + threads[threadId]->run_custom_job(std::move(f)); +} -// Wakes up main thread waiting in idle_loop() and -// returns immediately. Main thread will wake up other threads and start the search. +void ThreadPool::wait_on_thread(size_t threadId) { + assert(threads.size() > threadId); + threads[threadId]->wait_for_search_finished(); +} + +size_t ThreadPool::num_threads() const { return threads.size(); } + + +// Wakes up main thread waiting in idle_loop() and returns immediately. +// Main thread will wake up other threads and start the search. void ThreadPool::start_thinking(const OptionsMap& options, Position& pos, StateListPtr& states, @@ -185,10 +290,18 @@ void ThreadPool::start_thinking(const OptionsMap& options, increaseDepth = true; Search::RootMoves rootMoves; + const auto legalmoves = MoveList(pos); - for (const auto& m : MoveList(pos)) - if (limits.searchmoves.empty() - || std::count(limits.searchmoves.begin(), limits.searchmoves.end(), m)) + for (const auto& uciMove : limits.searchmoves) + { + auto move = UCIEngine::to_move(pos, uciMove); + + if (std::find(legalmoves.begin(), legalmoves.end(), move) != legalmoves.end()) + rootMoves.emplace_back(move); + } + + if (rootMoves.empty()) + for (const auto& m : legalmoves) rootMoves.emplace_back(m); Tablebases::Config tbConfig = Tablebases::rank_root_moves(options, pos, rootMoves); @@ -203,36 +316,40 @@ void ThreadPool::start_thinking(const OptionsMap& options, // We use Position::set() to set root position across threads. But there are // some StateInfo fields (previous, pliesFromNull, capturedPiece) that cannot // be deduced from a fen string, so set() clears them and they are set from - // setupStates->back() later. The rootState is per thread, earlier states are shared - // since they are read-only. - for (Thread* th : threads) + // setupStates->back() later. The rootState is per thread, earlier states are + // shared since they are read-only. + for (auto&& th : threads) { - th->worker->limits = limits; - th->worker->nodes = th->worker->tbHits = th->worker->nmpMinPly = - th->worker->bestMoveChanges = 0; - th->worker->TTsaves = 0; - th->worker->rootDepth = th->worker->completedDepth = 0; - th->worker->rootMoves = rootMoves; - th->worker->rootPos.set(pos.fen(), pos.is_chess960(), &th->worker->rootState); - th->worker->rootState = setupStates->back(); - th->worker->tbConfig = tbConfig; + th->run_custom_job([&]() { + th->worker->limits = limits; + th->worker->nodes = th->worker->tbHits = th->worker->bestMoveChanges = 0; + th->worker->nmpMinPly = 0; + th->worker->rootDepth = th->worker->completedDepth = 0; + th->worker->rootMoves = rootMoves; + th->worker->rootPos.set(pos.fen(), pos.is_chess960(), &th->worker->rootState); + th->worker->rootState = setupStates->back(); + th->worker->tbConfig = tbConfig; + }); } - Cluster::signals_init(); + for (auto&& th : threads) + th->wait_for_search_finished(); + + Distributed::signals_init(); main_thread()->start_searching(); } Thread* ThreadPool::get_best_thread() const { - Thread* bestThread = threads.front(); + Thread* bestThread = threads.front().get(); Value minScore = VALUE_NONE; std::unordered_map votes( 2 * std::min(size(), bestThread->worker->rootMoves.size())); // Find the minimum score of all threads - for (Thread* th : threads) + for (auto&& th : threads) minScore = std::min(minScore, th->worker->rootMoves[0].score); // Vote according to score and depth, and select the best thread @@ -240,10 +357,10 @@ Thread* ThreadPool::get_best_thread() const { return (th->worker->rootMoves[0].score - minScore + 14) * int(th->worker->completedDepth); }; - for (Thread* th : threads) - votes[th->worker->rootMoves[0].pv[0]] += thread_voting_value(th); + for (auto&& th : threads) + votes[th->worker->rootMoves[0].pv[0]] += thread_voting_value(th.get()); - for (Thread* th : threads) + for (auto&& th : threads) { const auto bestThreadScore = bestThread->worker->rootMoves[0].score; const auto newThreadScore = th->worker->rootMoves[0].score; @@ -254,59 +371,82 @@ Thread* ThreadPool::get_best_thread() const { const auto bestThreadMoveVote = votes[bestThreadPV[0]]; const auto newThreadMoveVote = votes[newThreadPV[0]]; - const bool bestThreadInProvenWin = bestThreadScore >= VALUE_TB_WIN_IN_MAX_PLY; - const bool newThreadInProvenWin = newThreadScore >= VALUE_TB_WIN_IN_MAX_PLY; + const bool bestThreadInProvenWin = is_win(bestThreadScore); + const bool newThreadInProvenWin = is_win(newThreadScore); const bool bestThreadInProvenLoss = - bestThreadScore != -VALUE_INFINITE && bestThreadScore <= VALUE_TB_LOSS_IN_MAX_PLY; + bestThreadScore != -VALUE_INFINITE && is_loss(bestThreadScore); const bool newThreadInProvenLoss = - newThreadScore != -VALUE_INFINITE && newThreadScore <= VALUE_TB_LOSS_IN_MAX_PLY; + newThreadScore != -VALUE_INFINITE && is_loss(newThreadScore); - // Note that we make sure not to pick a thread with truncated-PV for better viewer experience. + // We make sure not to pick a thread with truncated principal variation const bool betterVotingValue = - thread_voting_value(th) * int(newThreadPV.size() > 2) + thread_voting_value(th.get()) * int(newThreadPV.size() > 2) > thread_voting_value(bestThread) * int(bestThreadPV.size() > 2); if (bestThreadInProvenWin) { // Make sure we pick the shortest mate / TB conversion if (newThreadScore > bestThreadScore) - bestThread = th; + bestThread = th.get(); } else if (bestThreadInProvenLoss) { // Make sure we pick the shortest mated / TB conversion if (newThreadInProvenLoss && newThreadScore < bestThreadScore) - bestThread = th; + bestThread = th.get(); } else if (newThreadInProvenWin || newThreadInProvenLoss - || (newThreadScore > VALUE_TB_LOSS_IN_MAX_PLY + || (!is_loss(newThreadScore) && (newThreadMoveVote > bestThreadMoveVote || (newThreadMoveVote == bestThreadMoveVote && betterVotingValue)))) - bestThread = th; + bestThread = th.get(); } return bestThread; } -// Start non-main threads -// Will be invoked by main thread after it has started searching +// Start non-main threads. +// Will be invoked by main thread after it has started searching. void ThreadPool::start_searching() { - for (Thread* th : threads) + for (auto&& th : threads) if (th != threads.front()) th->start_searching(); } // Wait for non-main threads - void ThreadPool::wait_for_search_finished() const { - for (Thread* th : threads) + for (auto&& th : threads) if (th != threads.front()) th->wait_for_search_finished(); } +std::vector ThreadPool::get_bound_thread_count_by_numa_node() const { + std::vector counts; + + if (!boundThreadToNumaNode.empty()) + { + NumaIndex highestNumaNode = 0; + for (NumaIndex n : boundThreadToNumaNode) + if (n > highestNumaNode) + highestNumaNode = n; + + counts.resize(highestNumaNode + 1, 0); + + for (NumaIndex n : boundThreadToNumaNode) + counts[n] += 1; + } + + return counts; +} + +void ThreadPool::ensure_network_replicated() { + for (auto&& th : threads) + th->ensure_network_replicated(); +} + } // namespace Stockfish diff --git a/src/thread.h b/src/thread.h index e6caa7d74..0330ef705 100644 --- a/src/thread.h +++ b/src/thread.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -23,11 +23,14 @@ #include #include #include +#include #include #include #include +#include "memory.h" #include "movepick.h" +#include "numa.h" #include "position.h" #include "search.h" #include "thread_win32_osx.h" @@ -38,6 +41,32 @@ namespace Stockfish { class OptionsMap; using Value = int; +// Sometimes we don't want to actually bind the threads, but the recipient still +// needs to think it runs on *some* NUMA node, such that it can access structures +// that rely on NUMA node knowledge. This class encapsulates this optional process +// such that the recipient does not need to know whether the binding happened or not. +class OptionalThreadToNumaNodeBinder { + public: + OptionalThreadToNumaNodeBinder(NumaIndex n) : + numaConfig(nullptr), + numaId(n) {} + + OptionalThreadToNumaNodeBinder(const NumaConfig& cfg, NumaIndex n) : + numaConfig(&cfg), + numaId(n) {} + + NumaReplicatedAccessToken operator()() const { + if (numaConfig != nullptr) + return numaConfig->bind_current_thread_to_numa_node(numaId); + else + return NumaReplicatedAccessToken(numaId); + } + + private: + const NumaConfig* numaConfig; + NumaIndex numaId; +}; + // Abstraction of a thread. It contains a pointer to the worker and a native thread. // After construction, the native thread is started with idle_loop() // waiting for a signal to start searching. @@ -45,22 +74,39 @@ using Value = int; // the search is finished, it goes back to idle_loop() waiting for a new signal. class Thread { public: - Thread(Search::SharedState&, std::unique_ptr, size_t); + Thread(Search::SharedState&, + std::unique_ptr, + size_t, + size_t, + size_t, + OptionalThreadToNumaNodeBinder); virtual ~Thread(); - void idle_loop(); - void start_searching(); + void idle_loop(); + void start_searching(); + void clear_worker(); + void run_custom_job(std::function f); + + void ensure_network_replicated(); + + // Thread has been slightly altered to allow running custom jobs, so + // this name is no longer correct. However, this class (and ThreadPool) + // require further work to make them properly generic while maintaining + // appropriate specificity regarding search, from the point of view of an + // outside user, so renaming of this function is left for whenever that happens. void wait_for_search_finished(); size_t id() const { return idx; } - std::unique_ptr worker; + LargePagePtr worker; + std::function jobFunc; private: - std::mutex mutex; - std::condition_variable cv; - size_t idx, nthreads; - bool exit = false, searching = true; // Set before starting std::thread - NativeThread stdThread; + std::mutex mutex; + std::condition_variable cv; + size_t idx, idxInNuma, totalNuma, nthreads; + bool exit = false, searching = true; // Set before starting std::thread + NativeThread stdThread; + NumaReplicatedAccessToken numaAccessToken; }; @@ -68,25 +114,35 @@ class Thread { // parking and, most importantly, launching a thread. All the access to threads // is done through this class. class ThreadPool { - public: + ThreadPool() {} + ~ThreadPool() { // destroy any existing thread(s) if (threads.size() > 0) { main_thread()->wait_for_search_finished(); - while (threads.size() > 0) - delete threads.back(), threads.pop_back(); + threads.clear(); } } - void start_thinking(const OptionsMap&, Position&, StateListPtr&, Search::LimitsType); - void clear(); - void set(Search::SharedState); + ThreadPool(const ThreadPool&) = delete; + ThreadPool(ThreadPool&&) = delete; + + ThreadPool& operator=(const ThreadPool&) = delete; + ThreadPool& operator=(ThreadPool&&) = delete; + + void start_thinking(const OptionsMap&, Position&, StateListPtr&, Search::LimitsType); + void run_on_thread(size_t threadId, std::function f); + void wait_on_thread(size_t threadId); + size_t num_threads() const; + void clear(); + void + set(const NumaConfig& numaConfig, Search::SharedState, Search::SearchManager::UpdateContext&); Search::SearchManager* main_manager(); - Thread* main_thread() const { return threads.front(); } + Thread* main_thread() const { return threads.front().get(); } uint64_t nodes_searched() const; uint64_t tb_hits() const; uint64_t TT_saves() const; @@ -94,6 +150,10 @@ class ThreadPool { void start_searching(); void wait_for_search_finished() const; + std::vector get_bound_thread_count_by_numa_node() const; + + void ensure_network_replicated(); + std::atomic_bool stop, abortedSearch, increaseDepth; auto cbegin() const noexcept { return threads.cbegin(); } @@ -104,13 +164,14 @@ class ThreadPool { auto empty() const noexcept { return threads.empty(); } private: - StateListPtr setupStates; - std::vector threads; + StateListPtr setupStates; + std::vector> threads; + std::vector boundThreadToNumaNode; - uint64_t accumulate(std::atomic Search::Worker::*member) const { + uint64_t accumulate(std::atomic Search::Worker::* member) const { uint64_t sum = 0; - for (Thread* th : threads) + for (auto&& th : threads) sum += (th->worker.get()->*member).load(std::memory_order_relaxed); return sum; } diff --git a/src/thread_win32_osx.h b/src/thread_win32_osx.h index 1d9a834f6..5a8d43a2e 100644 --- a/src/thread_win32_osx.h +++ b/src/thread_win32_osx.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 diff --git a/src/timeman.cpp b/src/timeman.cpp index 229ff3e9d..4e98081bc 100644 --- a/src/timeman.cpp +++ b/src/timeman.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -30,17 +30,14 @@ namespace Stockfish { TimePoint TimeManagement::optimum() const { return optimumTime; } TimePoint TimeManagement::maximum() const { return maximumTime; } -TimePoint TimeManagement::elapsed(size_t nodes) const { - return useNodesTime ? TimePoint(nodes) : now() - startTime; -} void TimeManagement::clear() { - availableNodes = 0; // When in 'nodes as time' mode + availableNodes = -1; // When in 'nodes as time' mode } void TimeManagement::advance_nodes_time(std::int64_t nodes) { assert(useNodesTime); - availableNodes += nodes; + availableNodes = std::max(int64_t(0), availableNodes - nodes); } // Called at the beginning of the search and calculates @@ -50,15 +47,19 @@ void TimeManagement::advance_nodes_time(std::int64_t nodes) { void TimeManagement::init(Search::LimitsType& limits, Color us, int ply, - const OptionsMap& options) { - // If we have no time, no need to initialize TM, except for the start time, - // which is used by movetime. - startTime = limits.startTime; + const OptionsMap& options, + double& originalTimeAdjust) { + TimePoint npmsec = TimePoint(options["nodestime"]); + + // If we have no time, we don't need to fully initialize TM. + // startTime is used by movetime and useNodesTime is used in elapsed calls. + startTime = limits.startTime; + useNodesTime = npmsec != 0; + if (limits.time[us] == 0) return; TimePoint moveOverhead = TimePoint(options["Move Overhead"]); - TimePoint npmsec = TimePoint(options["nodestime"]); // optScale is a percentage of available time to use for the current move. // maxScale is a multiplier applied to optimumTime. @@ -68,62 +69,69 @@ void TimeManagement::init(Search::LimitsType& limits, // to nodes, and use resulting values in time management formulas. // WARNING: to avoid time losses, the given npmsec (nodes per millisecond) // must be much lower than the real engine speed. - if (npmsec) + if (useNodesTime) { - useNodesTime = true; - - if (!availableNodes) // Only once at game start + if (availableNodes == -1) // Only once at game start availableNodes = npmsec * limits.time[us]; // Time is in msec // Convert from milliseconds to nodes limits.time[us] = TimePoint(availableNodes); limits.inc[us] *= npmsec; limits.npmsec = npmsec; + moveOverhead *= npmsec; } - // Maximum move horizon of 50 moves - int mtg = limits.movestogo ? std::min(limits.movestogo, 50) : 50; + // These numbers are used where multiplications, divisions or comparisons + // with constants are involved. + const int64_t scaleFactor = useNodesTime ? npmsec : 1; + const TimePoint scaledTime = limits.time[us] / scaleFactor; - // if less than one second, gradually reduce mtg - if (limits.time[us] < 1000 && (double(mtg) / limits.time[us] > 0.05)) - { - mtg = limits.time[us] * 0.05; - } + // Maximum move horizon + int centiMTG = limits.movestogo ? std::min(limits.movestogo * 100, 5000) : 5051; + + // If less than one second, gradually reduce mtg + if (scaledTime < 1000) + centiMTG = int(scaledTime * 5.051); // Make sure timeLeft is > 0 since we may use it as a divisor - TimePoint timeLeft = std::max(TimePoint(1), limits.time[us] + limits.inc[us] * (mtg - 1) - - moveOverhead * (2 + mtg)); + TimePoint timeLeft = + std::max(TimePoint(1), + limits.time[us] + + (limits.inc[us] * (centiMTG - 100) - moveOverhead * (200 + centiMTG)) / 100); // x basetime (+ z increment) // If there is a healthy increment, timeLeft can exceed the actual available // game time for the current move, so also cap to a percentage of available game time. if (limits.movestogo == 0) { - // Use extra time with larger increments - double optExtra = limits.inc[us] < 500 ? 1.0 : 1.13; + // Extra time according to timeLeft + if (originalTimeAdjust < 0) + originalTimeAdjust = 0.3128 * std::log10(timeLeft) - 0.4354; // Calculate time constants based on current time left. - double optConstant = - std::min(0.00308 + 0.000319 * std::log10(limits.time[us] / 1000.0), 0.00506); - double maxConstant = std::max(3.39 + 3.01 * std::log10(limits.time[us] / 1000.0), 2.93); + double logTimeInSec = std::log10(scaledTime / 1000.0); + double optConstant = std::min(0.0032116 + 0.000321123 * logTimeInSec, 0.00508017); + double maxConstant = std::max(3.3977 + 3.03950 * logTimeInSec, 2.94761); - optScale = std::min(0.0122 + std::pow(ply + 2.95, 0.462) * optConstant, - 0.213 * limits.time[us] / double(timeLeft)) - * optExtra; - maxScale = std::min(6.64, maxConstant + ply / 12.0); + optScale = std::min(0.0121431 + std::pow(ply + 2.94693, 0.461073) * optConstant, + 0.213035 * limits.time[us] / timeLeft) + * originalTimeAdjust; + + maxScale = std::min(6.67704, maxConstant + ply / 11.9847); } // x moves in y seconds (+ z increment) else { - optScale = std::min((0.88 + ply / 116.4) / mtg, 0.88 * limits.time[us] / double(timeLeft)); - maxScale = std::min(6.3, 1.5 + 0.11 * mtg); + optScale = + std::min((0.88 + ply / 116.4) / (centiMTG / 100.0), 0.88 * limits.time[us] / timeLeft); + maxScale = 1.3 + 0.11 * (centiMTG / 100.0); } // Limit the maximum possible time for this move optimumTime = TimePoint(optScale * timeLeft); maximumTime = - TimePoint(std::min(0.825 * limits.time[us] - moveOverhead, maxScale * optimumTime)) - 10; + TimePoint(std::min(0.825179 * limits.time[us] - moveOverhead, maxScale * optimumTime)) - 10; if (options["Ponder"]) optimumTime += optimumTime / 4; diff --git a/src/timeman.h b/src/timeman.h index 23972974a..249a71bec 100644 --- a/src/timeman.h +++ b/src/timeman.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -19,7 +19,6 @@ #ifndef TIMEMAN_H_INCLUDED #define TIMEMAN_H_INCLUDED -#include #include #include "cluster.h" @@ -28,6 +27,7 @@ namespace Stockfish { class OptionsMap; +enum Color : int8_t; namespace Search { struct LimitsType; @@ -37,11 +37,19 @@ struct LimitsType; // the maximum available time, the game move number, and other parameters. class TimeManagement { public: - void init(Search::LimitsType& limits, Color us, int ply, const OptionsMap& options); + void init(Search::LimitsType& limits, + Color us, + int ply, + const OptionsMap& options, + double& originalTimeAdjust); TimePoint optimum() const; TimePoint maximum() const; - TimePoint elapsed(std::size_t nodes) const; + template + TimePoint elapsed(FUNC nodes) const { + return useNodesTime ? TimePoint(nodes()) : elapsed_time(); + } + TimePoint elapsed_time() const { return now() - startTime; }; void clear(); void advance_nodes_time(std::int64_t nodes); @@ -51,7 +59,7 @@ class TimeManagement { TimePoint optimumTime; TimePoint maximumTime; - std::int64_t availableNodes = 0; // When in 'nodes as time' mode + std::int64_t availableNodes = -1; // When in 'nodes as time' mode bool useNodesTime = false; // True if we are in 'nodes as time' mode }; diff --git a/src/tt.cpp b/src/tt.cpp index 9d4d2eca4..ef602809f 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -23,30 +23,89 @@ #include #include #include -#include -#include +#include "memory.h" #include "misc.h" +#include "syzygy/tbprobe.h" +#include "thread.h" namespace Stockfish { + +// TTEntry struct is the 10 bytes transposition table entry, defined as below: +// +// key 16 bit +// depth 8 bit +// generation 5 bit +// pv node 1 bit +// bound type 2 bit +// move 16 bit +// value 16 bit +// evaluation 16 bit +// +// These fields are in the same order as accessed by TT::probe(), since memory is fastest sequentially. +// Equally, the store order in save() matches this order. + +struct TTEntry { + + // Convert internal bitfields to external types + TTData read() const { + return TTData{Move(move16), Value(value16), + Value(eval16), Depth(depth8 + DEPTH_ENTRY_OFFSET), + Bound(genBound8 & 0x3), bool(genBound8 & 0x4)}; + } + + bool is_occupied() const; + void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8); + // The returned age is a multiple of TranspositionTable::GENERATION_DELTA + uint8_t relative_age(const uint8_t generation8) const; + + private: + friend class TranspositionTable; + + uint16_t key16; + uint8_t depth8; + uint8_t genBound8; + Move move16; + int16_t value16; + int16_t eval16; +}; + +// `genBound8` is where most of the details are. We use the following constants to manipulate 5 leading generation bits +// and 3 trailing miscellaneous bits. + +// These bits are reserved for other things. +static constexpr unsigned GENERATION_BITS = 3; +// increment for generation field +static constexpr int GENERATION_DELTA = (1 << GENERATION_BITS); +// cycle length +static constexpr int GENERATION_CYCLE = 255 + GENERATION_DELTA; +// mask to pull out generation number +static constexpr int GENERATION_MASK = (0xFF << GENERATION_BITS) & 0xFF; + +// DEPTH_ENTRY_OFFSET exists because 1) we use `bool(depth8)` as the occupancy check, but +// 2) we need to store negative depths for QS. (`depth8` is the only field with "spare bits": +// we sacrifice the ability to store depths greater than 1<<8 less the offset, as asserted in `save`.) +bool TTEntry::is_occupied() const { return bool(depth8); } + // Populates the TTEntry with a new node's data, possibly // overwriting an old position. The update is not atomic and can be racy. void TTEntry::save( Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8) { - // Preserve any existing move for the same position + // Preserve the old ttmove if we don't have a new one if (m || uint16_t(k) != key16) move16 = m; // Overwrite less valuable entries (cheapest checks first) - if (b == BOUND_EXACT || uint16_t(k) != key16 || d - DEPTH_OFFSET + 2 * pv > depth8 - 4) + if (b == BOUND_EXACT || uint16_t(k) != key16 || d - DEPTH_ENTRY_OFFSET + 2 * pv > depth8 - 4 + || relative_age(generation8)) { - assert(d > DEPTH_OFFSET); - assert(d < 256 + DEPTH_OFFSET); + assert(d > DEPTH_ENTRY_OFFSET); + assert(d < 256 + DEPTH_ENTRY_OFFSET); key16 = uint16_t(k); - depth8 = uint8_t(d - DEPTH_OFFSET); + depth8 = uint8_t(d - DEPTH_ENTRY_OFFSET); genBound8 = uint8_t(generation8 | uint8_t(pv) << 2 | b); value16 = int16_t(v); eval16 = int16_t(ev); @@ -60,100 +119,133 @@ uint8_t TTEntry::relative_age(const uint8_t generation8) const { // is needed to keep the unrelated lowest n bits from affecting // the result) to calculate the entry age correctly even after // generation8 overflows into the next cycle. - - return (TranspositionTable::GENERATION_CYCLE + generation8 - genBound8) - & TranspositionTable::GENERATION_MASK; + return (GENERATION_CYCLE + generation8 - genBound8) & GENERATION_MASK; } +// TTWriter is but a very thin wrapper around the pointer +TTWriter::TTWriter(TTEntry* tte) : + entry(tte) {} + +void TTWriter::write( + Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8) { + entry->save(k, v, pv, b, d, m, ev, generation8); +} + + +// A TranspositionTable is an array of Cluster, of size clusterCount. Each cluster consists of ClusterSize number +// of TTEntry. Each non-empty TTEntry contains information on exactly one position. The size of a Cluster should +// divide the size of a cache line for best performance, as the cacheline is prefetched when possible. + +static constexpr int ClusterSize = 3; + +struct Cluster { + TTEntry entry[ClusterSize]; + char padding[2]; // Pad to 32 bytes +}; + +static_assert(sizeof(Cluster) == 32, "Suboptimal Cluster size"); + + // Sets the size of the transposition table, -// measured in megabytes. Transposition table consists of a power of 2 number +// measured in megabytes. Transposition table consists // of clusters and each cluster consists of ClusterSize number of TTEntry. -void TranspositionTable::resize(size_t mbSize, int threadCount) { +void TranspositionTable::resize(size_t mbSize, ThreadPool& threads) { aligned_large_pages_free(table); clusterCount = mbSize * 1024 * 1024 / sizeof(Cluster); table = static_cast(aligned_large_pages_alloc(clusterCount * sizeof(Cluster))); + if (!table) { std::cerr << "Failed to allocate " << mbSize << "MB for transposition table." << std::endl; exit(EXIT_FAILURE); } - clear(threadCount); + clear(threads); } // Initializes the entire transposition table to zero, // in a multi-threaded way. -void TranspositionTable::clear(size_t threadCount) { - std::vector threads; +void TranspositionTable::clear(ThreadPool& threads) { + generation8 = 0; + const size_t threadCount = threads.num_threads(); - for (size_t idx = 0; idx < size_t(threadCount); ++idx) + for (size_t i = 0; i < threadCount; ++i) { - threads.emplace_back([this, idx, threadCount]() { - // Thread binding gives faster search on systems with a first-touch policy - if (threadCount > 8) - WinProcGroup::bind_this_thread(idx); - + threads.run_on_thread(i, [this, i, threadCount]() { // Each thread will zero its part of the hash table - const size_t stride = size_t(clusterCount / threadCount), start = size_t(stride * idx), - len = idx != size_t(threadCount) - 1 ? stride : clusterCount - start; + const size_t stride = clusterCount / threadCount; + const size_t start = stride * i; + const size_t len = i + 1 != threadCount ? stride : clusterCount - start; std::memset(&table[start], 0, len * sizeof(Cluster)); }); } - for (std::thread& th : threads) - th.join(); -} - - -// Looks up the current position in the transposition -// table. It returns true and a pointer to the TTEntry if the position is found. -// Otherwise, it returns false and a pointer to an empty or least valuable TTEntry -// to be replaced later. The replace value of an entry is calculated as its depth -// minus 8 times its relative age. TTEntry t1 is considered more valuable than -// TTEntry t2 if its replace value is greater than that of t2. -TTEntry* TranspositionTable::probe(const Key key, bool& found) const { - - TTEntry* const tte = first_entry(key); - const uint16_t key16 = uint16_t(key); // Use the low 16 bits as key inside the cluster - - for (int i = 0; i < ClusterSize; ++i) - if (tte[i].key16 == key16 || !tte[i].depth8) - { - constexpr uint8_t lowerBits = GENERATION_DELTA - 1; - - // Refresh with new generation, keeping the lower bits the same. - tte[i].genBound8 = uint8_t(generation8 | (tte[i].genBound8 & lowerBits)); - return found = bool(tte[i].depth8), &tte[i]; - } - - // Find an entry to be replaced according to the replacement strategy - TTEntry* replace = tte; - for (int i = 1; i < ClusterSize; ++i) - if (replace->depth8 - replace->relative_age(generation8) * 2 - > tte[i].depth8 - tte[i].relative_age(generation8) * 2) - replace = &tte[i]; - - return found = false, replace; + for (size_t i = 0; i < threadCount; ++i) + threads.wait_on_thread(i); } // Returns an approximation of the hashtable // occupation during a search. The hash is x permill full, as per UCI protocol. // Only counts entries which match the current generation. -int TranspositionTable::hashfull() const { - - int cnt = 0; +int TranspositionTable::hashfull(int maxAge) const { + int maxAgeInternal = maxAge << GENERATION_BITS; + int cnt = 0; for (int i = 0; i < 1000; ++i) for (int j = 0; j < ClusterSize; ++j) - cnt += table[i].entry[j].depth8 - && (table[i].entry[j].genBound8 & GENERATION_MASK) == generation8; + cnt += table[i].entry[j].is_occupied() + && table[i].entry[j].relative_age(generation8) <= maxAgeInternal; return cnt / ClusterSize; } + +void TranspositionTable::new_search() { + // increment by delta to keep lower bits as is + generation8 += GENERATION_DELTA; +} + + +uint8_t TranspositionTable::generation() const { return generation8; } + + +// Looks up the current position in the transposition +// table. It returns true if the position is found. +// Otherwise, it returns false and a pointer to an empty or least valuable TTEntry +// to be replaced later. The replace value of an entry is calculated as its depth +// minus 8 times its relative age. TTEntry t1 is considered more valuable than +// TTEntry t2 if its replace value is greater than that of t2. +std::tuple TranspositionTable::probe(const Key key) const { + + TTEntry* const tte = first_entry(key); + const uint16_t key16 = uint16_t(key); // Use the low 16 bits as key inside the cluster + + for (int i = 0; i < ClusterSize; ++i) + if (tte[i].key16 == key16) + // This gap is the main place for read races. + // After `read()` completes that copy is final, but may be self-inconsistent. + return {tte[i].is_occupied(), tte[i].read(), TTWriter(&tte[i])}; + + // Find an entry to be replaced according to the replacement strategy + TTEntry* replace = tte; + for (int i = 1; i < ClusterSize; ++i) + if (replace->depth8 - replace->relative_age(generation8) + > tte[i].depth8 - tte[i].relative_age(generation8)) + replace = &tte[i]; + + return {false, + TTData{Move::none(), VALUE_NONE, VALUE_NONE, DEPTH_ENTRY_OFFSET, BOUND_NONE, false}, + TTWriter(replace)}; +} + + +TTEntry* TranspositionTable::first_entry(const Key key) const { + return &table[mul_hi64(key, clusterCount)].entry[0]; +} + } // namespace Stockfish diff --git a/src/tt.h b/src/tt.h index 4c0a34704..d142a105a 100644 --- a/src/tt.h +++ b/src/tt.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -21,108 +21,101 @@ #include #include +#include -#include "misc.h" +#include "memory.h" #include "types.h" namespace Stockfish { -namespace Cluster { +namespace Distributed { void init(); } -/// TTEntry struct is the 10 bytes transposition table entry, defined as below: -/// -/// key 16 bit -/// depth 8 bit -/// generation 5 bit -/// pv node 1 bit -/// bound type 2 bit -/// move 16 bit -/// value 16 bit -/// eval value 16 bit +class ThreadPool; +struct TTEntry; +struct Cluster; -struct TTEntry { - - Move move() const { return Move(move16); } - Value value() const { return Value(value16); } - Value eval() const { return Value(eval16); } - Depth depth() const { return Depth(depth8 + DEPTH_OFFSET); } - bool is_pv() const { return bool(genBound8 & 0x4); } - Bound bound() const { return Bound(genBound8 & 0x3); } - void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8); - // The returned age is a multiple of TranspositionTable::GENERATION_DELTA - uint8_t relative_age(const uint8_t generation8) const; - - private: - friend class TranspositionTable; - friend void Cluster::init(); +// There is only one global hash table for the engine and all its threads. For chess in particular, we even allow racy +// updates between threads to and from the TT, as taking the time to synchronize access would cost thinking time and +// thus elo. As a hash table, collisions are possible and may cause chess playing issues (bizarre blunders, faulty mate +// reports, etc). Fixing these also loses elo; however such risk decreases quickly with larger TT size. +// +// `probe` is the primary method: given a board position, we lookup its entry in the table, and return a tuple of: +// 1) whether the entry already has this position +// 2) a copy of the prior data (if any) (may be inconsistent due to read races) +// 3) a writer object to this entry +// The copied data and the writer are separated to maintain clear boundaries between local vs global objects. - uint16_t key16; - uint8_t depth8; - uint8_t genBound8; - Move move16; - int16_t value16; - int16_t eval16; +// A copy of the data already in the entry (possibly collided). `probe` may be racy, resulting in inconsistent data. +struct TTData { + Move move; + Value value, eval; + Depth depth; + 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) : + move(m), + value(v), + eval(ev), + depth(d), + bound(b), + is_pv(pv) {}; + // clang-format on +}; + + +// This is used to make racy writes to the global TT. +struct TTWriter { + public: + void write(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8); + + private: + friend class TranspositionTable; + friend void Distributed::init(); + + TTEntry* entry; + TTWriter(TTEntry* tte); }; -// A TranspositionTable is an array of Cluster, of size clusterCount. Each -// cluster consists of ClusterSize number of TTEntry. Each non-empty TTEntry -// contains information on exactly one position. The size of a Cluster should -// divide the size of a cache line for best performance, as the cacheline is -// prefetched when possible. class TranspositionTable { - friend void Cluster::init(); - - static constexpr int ClusterSize = 3; - - struct Cluster { - TTEntry entry[ClusterSize]; - char padding[2]; // Pad to 32 bytes - }; - - static_assert(sizeof(Cluster) == 32, "Unexpected Cluster size"); - - // Constants used to refresh the hash table periodically - - // We have 8 bits available where the lowest 3 bits are - // reserved for other things. - static constexpr unsigned GENERATION_BITS = 3; - // increment for generation field - static constexpr int GENERATION_DELTA = (1 << GENERATION_BITS); - // cycle length - static constexpr int GENERATION_CYCLE = 255 + GENERATION_DELTA; - // mask to pull out generation number - static constexpr int GENERATION_MASK = (0xFF << GENERATION_BITS) & 0xFF; + friend void Distributed::init(); public: ~TranspositionTable() { aligned_large_pages_free(table); } - void new_search() { - // increment by delta to keep lower bits as is - generation8 += GENERATION_DELTA; - } + void resize(size_t mbSize, ThreadPool& threads); // Set TT size + void clear(ThreadPool& threads); // Re-initialize memory, multithreaded + int hashfull(int maxAge = 0) + const; // Approximate what fraction of entries (permille) have been written to during this root search - TTEntry* probe(const Key key, bool& found) const; - int hashfull() const; - void resize(size_t mbSize, int threadCount); - void clear(size_t threadCount); - - TTEntry* first_entry(const Key key) const { - return &table[mul_hi64(key, clusterCount)].entry[0]; - } - - uint8_t generation() const { return generation8; } + void + new_search(); // This must be called at the beginning of each root search to track entry aging + uint8_t generation() const; // The current age, used when writing new data to the TT + std::tuple + probe(const Key key) const; // The main method, whose retvals separate local vs global objects + TTEntry* first_entry(const Key key) + const; // This is the hash function; its only external use is memory prefetching. private: friend struct TTEntry; size_t clusterCount; - Cluster* table = nullptr; - uint8_t generation8 = 0; // Size must be not bigger than TTEntry::genBound8 + Cluster* table = nullptr; + + uint8_t generation8 = 0; // Size must be not bigger than TTEntry::genBound8 }; } // namespace Stockfish diff --git a/src/tune.cpp b/src/tune.cpp index 3e5ebe5e6..f930c267e 100644 --- a/src/tune.cpp +++ b/src/tune.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -33,19 +34,19 @@ namespace Stockfish { bool Tune::update_on_last; const Option* LastOption = nullptr; OptionsMap* Tune::options; - - namespace { std::map TuneResults; -void on_tune(const Option& o) { +std::optional on_tune(const Option& o) { if (!Tune::update_on_last || LastOption == &o) Tune::read_options(); + + return std::nullopt; +} } - -void make_option(OptionsMap* options, const string& n, int v, const SetRange& r) { +void Tune::make_option(OptionsMap* opts, const string& n, int v, const SetRange& r) { // Do not generate option when there is nothing to tune (ie. min = max) if (r(v).first == r(v).second) @@ -54,15 +55,17 @@ void make_option(OptionsMap* options, const string& n, int v, const SetRange& r) if (TuneResults.count(n)) v = TuneResults[n]; - (*options)[n] << Option(v, r(v).first, r(v).second, on_tune); - LastOption = &((*options)[n]); + opts->add(n, Option(v, r(v).first, r(v).second, on_tune)); + LastOption = &((*opts)[n]); // Print formatted parameters, ready to be copy-pasted in Fishtest - std::cout << n << "," << v << "," << r(v).first << "," << r(v).second << "," - << (r(v).second - r(v).first) / 20.0 << "," + std::cout << n << "," // + << v << "," // + << r(v).first << "," // + << r(v).second << "," // + << (r(v).second - r(v).first) / 20.0 << "," // << "0.0020" << std::endl; } -} string Tune::next(string& names, bool pop) { @@ -118,7 +121,6 @@ void Tune::Entry::read_option() { namespace Stockfish { -void Tune::read_results() { /* ...insert your values here... */ -} +void Tune::read_results() { /* ...insert your values here... */ } } // namespace Stockfish diff --git a/src/tune.h b/src/tune.h index b88c085fd..4ce6e759f 100644 --- a/src/tune.h +++ b/src/tune.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -145,6 +145,8 @@ class Tune { return add(value, (next(names), std::move(names)), args...); } + static void make_option(OptionsMap* options, const std::string& n, int v, const SetRange& r); + std::vector> list; public: @@ -158,7 +160,7 @@ class Tune { for (auto& e : instance().list) e->init_option(); read_options(); - } // Deferred, due to UCI::Options access + } // Deferred, due to UCIEngine::Options access static void read_options() { for (auto& e : instance().list) e->read_option(); @@ -168,11 +170,20 @@ class Tune { static OptionsMap* options; }; +template +constexpr void tune_check_args(Args&&...) { + static_assert((!std::is_fundamental_v && ...), "TUNE macro arguments wrong"); +} + // Some macro magic :-) we define a dummy int variable that the compiler initializes calling Tune::add() #define STRINGIFY(x) #x #define UNIQUE2(x, y) x##y #define UNIQUE(x, y) UNIQUE2(x, y) // Two indirection levels to expand __LINE__ -#define TUNE(...) int UNIQUE(p, __LINE__) = Tune::add(STRINGIFY((__VA_ARGS__)), __VA_ARGS__) +#define TUNE(...) \ + int UNIQUE(p, __LINE__) = []() -> int { \ + tune_check_args(__VA_ARGS__); \ + return Tune::add(STRINGIFY((__VA_ARGS__)), __VA_ARGS__); \ + }(); #define UPDATE_ON_LAST() bool UNIQUE(p, __LINE__) = Tune::update_on_last = true diff --git a/src/types.h b/src/types.h index 8b0ffb0ca..b5a0498a8 100644 --- a/src/types.h +++ b/src/types.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -37,7 +37,10 @@ // | only in 64-bit mode and requires hardware with pext support. #include + #include #include + #include + #include "misc.h" #if defined(_MSC_VER) // Disable some silly and noisy warnings from MSVC compiler @@ -55,9 +58,15 @@ // _WIN32 Building on Windows (any) // _WIN64 Building on Windows 64 bit - #if defined(__GNUC__) && (__GNUC__ < 9 || (__GNUC__ == 9 && __GNUC_MINOR__ <= 2)) \ - && defined(_WIN32) && !defined(__clang__) - #define ALIGNAS_ON_STACK_VARIABLES_BROKEN +// Enforce minimum GCC version + #if defined(__GNUC__) && !defined(__clang__) \ + && (__GNUC__ < 9 || (__GNUC__ == 9 && __GNUC_MINOR__ < 3)) + #error "Stockfish requires GCC 9.3 or later for correct compilation" + #endif + + // Enforce minimum Clang version + #if defined(__clang__) && (__clang_major__ < 10) + #error "Stockfish requires Clang 10.0 or later for correct compilation" #endif #define ASSERT_ALIGNED(ptr, alignment) assert(reinterpret_cast(ptr) % alignment == 0) @@ -108,13 +117,13 @@ using Bitboard = uint64_t; constexpr int MAX_MOVES = 256; constexpr int MAX_PLY = 246; -enum Color { +enum Color : int8_t { WHITE, BLACK, COLOR_NB = 2 }; -enum CastlingRights { +enum CastlingRights : int8_t { NO_CASTLING, WHITE_OO, WHITE_OOO = WHITE_OO << 1, @@ -130,16 +139,16 @@ enum CastlingRights { CASTLING_RIGHT_NB = 16 }; -enum Bound { +enum Bound : int8_t { BOUND_NONE, BOUND_UPPER, BOUND_LOWER, BOUND_EXACT = BOUND_UPPER | BOUND_LOWER }; -// Value is used as an alias for int16_t, this is done to differentiate between -// a search value and any other integer value. The values used in search are always -// supposed to be in the range (-VALUE_NONE, VALUE_NONE] and should not exceed this range. +// Value is used as an alias for int, this is done to differentiate between a search +// value and any other integer value. The values used in search are always supposed +// to be in the range (-VALUE_NONE, VALUE_NONE] and should not exceed this range. using Value = int; constexpr Value VALUE_ZERO = 0; @@ -155,6 +164,21 @@ constexpr Value VALUE_TB = VALUE_MATE_IN_MAX_PLY - 1; constexpr Value VALUE_TB_WIN_IN_MAX_PLY = VALUE_TB - MAX_PLY; constexpr Value VALUE_TB_LOSS_IN_MAX_PLY = -VALUE_TB_WIN_IN_MAX_PLY; + +constexpr bool is_valid(Value value) { return value != VALUE_NONE; } + +constexpr bool is_win(Value value) { + assert(is_valid(value)); + return value >= VALUE_TB_WIN_IN_MAX_PLY; +} + +constexpr bool is_loss(Value value) { + assert(is_valid(value)); + return value <= VALUE_TB_LOSS_IN_MAX_PLY; +} + +constexpr bool is_decisive(Value value) { return is_win(value) || is_loss(value); } + // In the code, we make the assumption that these values // are such that non_pawn_material() can be used to uniquely // identify the material on the board. @@ -166,13 +190,13 @@ constexpr Value QueenValue = 2538; // clang-format off -enum PieceType { +enum PieceType : std::int8_t { NO_PIECE_TYPE, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING, ALL_PIECES = 0, PIECE_TYPE_NB = 8 }; -enum Piece { +enum Piece : std::int8_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, @@ -186,17 +210,24 @@ constexpr Value PieceValue[PIECE_NB] = { using Depth = int; -enum : int { - DEPTH_QS_CHECKS = 0, - DEPTH_QS_NO_CHECKS = -1, - - DEPTH_NONE = -6, - - DEPTH_OFFSET = -7 // value used only for TT entry occupancy check -}; +// The following DEPTH_ constants are used for transposition table entries +// and quiescence search move generation stages. In regular search, the +// depth stored in the transposition table is literal: the search depth +// (effort) used to make the corresponding transposition table value. In +// quiescence search, however, the transposition table entries only store +// the current quiescence move generation stage (which should thus compare +// lower than any regular search depth). +constexpr Depth DEPTH_QS = 0; +// For transposition table entries where no searching at all was done +// (whether regular or qsearch) we use DEPTH_UNSEARCHED, which should thus +// compare lower than any quiescence or regular depth. DEPTH_ENTRY_OFFSET +// is used only for the transposition table entry occupancy check (see tt.cpp), +// and should thus be lower than DEPTH_UNSEARCHED. +constexpr Depth DEPTH_UNSEARCHED = -2; +constexpr Depth DEPTH_ENTRY_OFFSET = -3; // clang-format off -enum Square : int { +enum Square : int8_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, @@ -212,7 +243,7 @@ enum Square : int { }; // clang-format on -enum Direction : int { +enum Direction : int8_t { NORTH = 8, EAST = 1, SOUTH = -NORTH, @@ -224,7 +255,7 @@ enum Direction : int { NORTH_WEST = NORTH + WEST }; -enum File : int { +enum File : int8_t { FILE_A, FILE_B, FILE_C, @@ -236,7 +267,7 @@ enum File : int { FILE_NB }; -enum Rank : int { +enum Rank : int8_t { RANK_1, RANK_2, RANK_3, @@ -250,23 +281,62 @@ enum Rank : int { // Keep track of what a move changes on the board (used by NNUE) struct DirtyPiece { + Piece pc; // this is never allowed to be NO_PIECE + Square from, to; // to should be SQ_NONE for promotions - // Number of changed pieces - int dirty_num; + // if {add,remove}_sq is SQ_NONE, {add,remove}_pc is allowed to be + // uninitialized + // castling uses add_sq and remove_sq to remove and add the rook + Square remove_sq, add_sq; + Piece remove_pc, add_pc; +}; - // Max 3 pieces can change in one move. A promotion with capture moves - // both the pawn and the captured piece to SQ_NONE and the piece promoted - // to from SQ_NONE to the capture square. - Piece piece[3]; +// Keep track of what threats change on the board (used by NNUE) +struct DirtyThreat { + static constexpr int PcSqOffset = 0; + static constexpr int ThreatenedSqOffset = 8; + static constexpr int ThreatenedPcOffset = 16; + static constexpr int PcOffset = 20; - // From and to squares, which may be SQ_NONE - Square from[3]; - Square to[3]; + DirtyThreat() { /* don't initialize data */ } + DirtyThreat(uint32_t raw) : + data(raw) {} + DirtyThreat(Piece pc, Piece threatened_pc, Square pc_sq, Square threatened_sq, bool add) { + data = (uint32_t(add) << 31) | (pc << PcOffset) | (threatened_pc << ThreatenedPcOffset) + | (threatened_sq << ThreatenedSqOffset) | (pc_sq << PcSqOffset); + } + + Piece pc() const { return static_cast(data >> PcOffset & 0xf); } + Piece threatened_pc() const { return static_cast(data >> ThreatenedPcOffset & 0xf); } + Square threatened_sq() const { return static_cast(data >> ThreatenedSqOffset & 0xff); } + Square pc_sq() const { return static_cast(data >> PcSqOffset & 0xff); } + bool add() const { return data >> 31; } + uint32_t raw() const { return data; } + + private: + uint32_t data; +}; + +// A piece can be involved in at most 8 outgoing attacks and 16 incoming attacks. +// Moving a piece also can reveal at most 8 discovered attacks. +// This implies that a non-castling move can change at most (8 + 16) * 3 + 8 = 80 features. +// By similar logic, a castling move can change at most (5 + 1 + 3 + 9) * 2 = 36 features. +// Thus, 80 should work as an upper bound. Finally, 16 entries are added to accommodate +// unmasked vector stores near the end of the list. + +using DirtyThreatList = ValueList; + +struct DirtyThreats { + DirtyThreatList list; + Color us; + Square prevKsq, ksq; + + Bitboard threatenedSqs, threateningSqs; }; #define ENABLE_INCR_OPERATORS_ON(T) \ - inline T& operator++(T& d) { return d = T(int(d) + 1); } \ - inline T& operator--(T& d) { return d = T(int(d) - 1); } + constexpr T& operator++(T& d) { return d = T(int(d) + 1); } \ + constexpr T& operator--(T& d) { return d = T(int(d) - 1); } ENABLE_INCR_OPERATORS_ON(PieceType) ENABLE_INCR_OPERATORS_ON(Square) @@ -279,10 +349,10 @@ constexpr Direction operator+(Direction d1, Direction d2) { return Direction(int constexpr Direction operator*(int i, Direction d) { return Direction(i * int(d)); } // Additional operators to add a Direction to a Square -constexpr Square operator+(Square s, Direction d) { return Square(int(s) + int(d)); } -constexpr Square operator-(Square s, Direction d) { return Square(int(s) - int(d)); } -inline Square& operator+=(Square& s, Direction d) { return s = s + d; } -inline Square& operator-=(Square& s, Direction d) { return s = s - d; } +constexpr Square operator+(Square s, Direction d) { return Square(int(s) + int(d)); } +constexpr Square operator-(Square s, Direction d) { return Square(int(s) - int(d)); } +constexpr Square& operator+=(Square& s, Direction d) { return s = s + d; } +constexpr Square& operator-=(Square& s, Direction d) { return s = s - d; } // Toggle color constexpr Color operator~(Color c) { return Color(c ^ BLACK); } @@ -310,7 +380,7 @@ constexpr Piece make_piece(Color c, PieceType pt) { return Piece((c << 3) + pt); constexpr PieceType type_of(Piece pc) { return PieceType(pc & 7); } -inline Color color_of(Piece pc) { +constexpr Color color_of(Piece pc) { assert(pc != NO_PIECE); return Color(pc >> 3); } @@ -351,9 +421,10 @@ enum MoveType { // bit 14-15: special move flag: promotion (1), en passant (2), castling (3) // NOTE: en passant bit is set only when a pawn can be captured // -// Special cases are Move::none() and Move::null(). We can sneak these in because in -// any normal move destination square is always different from origin square -// while Move::none() and Move::null() have the same origin and destination square. +// Special cases are Move::none() and Move::null(). We can sneak these in because +// in any normal move the destination square and origin square are always different, +// but Move::none() and Move::null() have the same origin and destination square. + class Move { public: Move() = default; @@ -378,8 +449,6 @@ class Move { return Square(data & 0x3F); } - constexpr int from_to() const { return data & 0xFFF; } - constexpr MoveType type_of() const { return MoveType(data & (3 << 14)); } constexpr PieceType promotion_type() const { return PieceType(((data >> 12) & 3) + KNIGHT); } @@ -404,6 +473,14 @@ class Move { std::uint16_t data; }; +template +struct is_all_same { + static constexpr bool value = (std::is_same_v && ...); +}; + +template +constexpr auto is_all_same_v = is_all_same::value; + } // namespace Stockfish #endif // #ifndef TYPES_H_INCLUDED diff --git a/src/uci.cpp b/src/uci.cpp index d26b37825..0d943805f 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -19,93 +19,75 @@ #include "uci.h" #include -#include #include #include #include -#include -#include -#include +#include #include #include +#include #include #include #include "benchmark.h" #include "cluster.h" -#include "evaluate.h" +#include "engine.h" +#include "memory.h" #include "movegen.h" -#include "nnue/network.h" -#include "nnue/nnue_common.h" -#include "perft.h" #include "position.h" +#include "score.h" #include "search.h" -#include "syzygy/tbprobe.h" #include "types.h" #include "ucioption.h" namespace Stockfish { -constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; -constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048; +constexpr auto BenchmarkCommand = "speedtest"; +constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; +template +struct overload: Ts... { + using Ts::operator()...; +}; -namespace NN = Eval::NNUE; +template +overload(Ts...) -> overload; - -UCI::UCI(int argc, char** argv) : - networks(NN::Networks( - NN::NetworkBig({EvalFileDefaultNameBig, "None", ""}, NN::EmbeddedNNUEType::BIG), - NN::NetworkSmall({EvalFileDefaultNameSmall, "None", ""}, NN::EmbeddedNNUEType::SMALL))), - cli(argc, argv) { - - options["Debug Log File"] << Option("", [](const Option& o) { start_logger(o); }); - - options["Threads"] << Option(1, 1, 1024, [this](const Option&) { - threads.set({options, threads, tt, networks}); - }); - - options["Hash"] << Option(16, 1, MaxHashMB, [this](const Option& o) { - threads.main_thread()->wait_for_search_finished(); - tt.resize(o, options["Threads"]); - }); - - options["Clear Hash"] << Option([this](const Option&) { search_clear(); }); - options["Ponder"] << Option(false); - options["MultiPV"] << Option(1, 1, MAX_MOVES); - options["Skill Level"] << Option(20, 0, 20); - options["Move Overhead"] << Option(10, 0, 5000); - options["nodestime"] << Option(0, 0, 10000); - options["UCI_Chess960"] << Option(false); - options["UCI_LimitStrength"] << Option(false); - options["UCI_Elo"] << Option(1320, 1320, 3190); - options["UCI_ShowWDL"] << Option(false); - options["SyzygyPath"] << Option("", [](const Option& o) { Tablebases::init(o); }); - options["SyzygyProbeDepth"] << Option(1, 1, 100); - options["Syzygy50MoveRule"] << Option(true); - options["SyzygyProbeLimit"] << Option(7, 0, 7); - options["EvalFile"] << Option(EvalFileDefaultNameBig, [this](const Option& o) { - networks.big.load(cli.binaryDirectory, o); - }); - options["EvalFileSmall"] << Option(EvalFileDefaultNameSmall, [this](const Option& o) { - networks.small.load(cli.binaryDirectory, o); - }); - - networks.big.load(cli.binaryDirectory, options["EvalFile"]); - networks.small.load(cli.binaryDirectory, options["EvalFileSmall"]); - - threads.set({options, threads, tt, networks}); - - search_clear(); // After threads are up +void UCIEngine::print_info_string(std::string_view str) { + sync_cout_start(); + for (auto& line : split(str, "\n")) + { + if (!is_whitespace(line)) + { + std::cout << "info string " << line << '\n'; + } + } + sync_cout_end(); } -void UCI::loop() { +UCIEngine::UCIEngine(int argc, char** argv) : + engine(argv[0]), + cli(argc, argv) { - Position pos; - std::string token, cmd; - StateListPtr states(new std::deque(1)); + engine.get_options().add_info_listener([](const std::optional& str) { + if (str.has_value()) + print_info_string(*str); + }); - pos.set(StartFEN, false, &states->back()); + init_search_update_listeners(); +} + +void UCIEngine::init_search_update_listeners() { + engine.set_on_iter([](const auto& i) { on_iter(i); }); + engine.set_on_update_no_moves([](const auto& i) { on_update_no_moves(i); }); + engine.set_on_update_full( + [this](const auto& i) { on_update_full(i, engine.get_options()["UCI_ShowWDL"]); }); + engine.set_on_bestmove([](const auto& bm, const auto& p) { on_bestmove(bm, p); }); + engine.set_on_verify_networks([](const auto& s) { print_info_string(s); }); +} + +void UCIEngine::loop() { + std::string token, cmd; for (int i = 1; i < cli.argc; ++i) cmd += std::string(cli.argv[i]) + " "; @@ -113,8 +95,8 @@ void UCI::loop() { do { if (cli.argc == 1 - && !Cluster::getline(std::cin, - cmd)) // Wait for an input or an end-of-file (EOF) indication + && !Distributed::getline(std::cin, + cmd)) // Wait for an input or an end-of-file (EOF) indication cmd = "quit"; std::istringstream is(cmd); @@ -123,43 +105,54 @@ void UCI::loop() { is >> std::skipws >> token; if (token == "quit" || token == "stop") - threads.stop = true; + engine.stop(); // The GUI sends 'ponderhit' to tell that the user has played the expected move. // So, 'ponderhit' is sent if pondering was done on the same move that the user // has played. The search should continue, but should also switch from pondering // to the normal search. else if (token == "ponderhit") - threads.main_manager()->ponder = false; // Switch to the normal search + engine.set_ponderhit(false); - else if (token == "uci" && Cluster::is_root()) + else if (token == "uci" && Distributed::is_root()) + { sync_cout << "id name " << engine_info(true) << "\n" - << options << "\nuciok" << sync_endl; + << engine.get_options() << sync_endl; + + sync_cout << "uciok" << sync_endl; + } else if (token == "setoption") setoption(is); else if (token == "go") - go(pos, is, states); + { + // send info strings after the go command is sent for old GUIs and python-chess + print_info_string(engine.numa_config_information_as_string()); + print_info_string(engine.thread_allocation_information_as_string()); + go(is); + } else if (token == "position") - position(pos, is, states); + position(is); else if (token == "ucinewgame") - search_clear(); - else if (token == "isready" && Cluster::is_root()) + engine.search_clear(); + else if (token == "isready" && Distributed::is_root()) sync_cout << "readyok" << sync_endl; // Add custom non-UCI commands, mainly for debugging purposes. // These commands must not be used during a search! else if (token == "flip") - pos.flip(); + engine.flip(); else if (token == "bench") - bench(pos, is, states); - else if (token == "d" && Cluster::is_root()) - sync_cout << pos << sync_endl; - else if (token == "eval" && Cluster::is_root()) - trace_eval(pos); - else if (token == "compiler" && Cluster::is_root()) + bench(is); + else if (token == BenchmarkCommand) + benchmark(is); + else if (token == "d" && Distributed::is_root()) + sync_cout << engine.visualize() << sync_endl; + else if (token == "eval" && Distributed::is_root()) + engine.trace_eval(); + else if (token == "compiler" && Distributed::is_root()) sync_cout << compiler_info() << sync_endl; - else if (token == "export_net" && Cluster::is_root()) + else if (token == "export_net" && Distributed::is_root()) { std::pair, std::string> files[2]; @@ -169,12 +162,11 @@ void UCI::loop() { if (is >> std::skipws >> files[1].second) files[1].first = files[1].second; - networks.big.save(files[0].first); - networks.small.save(files[1].first); + engine.save_network(files); } else if ((token == "--help" || token == "help" || token == "--license" || token == "license") - && Cluster::is_root()) + && Distributed::is_root()) sync_cout << "\nStockfish is a powerful chess engine for playing and analyzing." "\nIt is released as free software licensed under the GNU GPLv3 License." @@ -183,14 +175,14 @@ void UCI::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] != '#' && Cluster::is_root()) + else if (!token.empty() && token[0] != '#' && Distributed::is_root()) sync_cout << "Unknown command: '" << cmd << "'. Type help for more information." << sync_endl; } while (token != "quit" && cli.argc == 1); // The command-line arguments are one-shot } -Search::LimitsType UCI::parse_limits(const Position& pos, std::istream& is) { +Search::LimitsType UCIEngine::parse_limits(std::istream& is) { Search::LimitsType limits; std::string token; @@ -199,7 +191,7 @@ Search::LimitsType UCI::parse_limits(const Position& pos, std::istream& is) { while (is >> token) if (token == "searchmoves") // Needs to be the last command on the line while (is >> token) - limits.searchmoves.push_back(to_move(pos, token)); + limits.searchmoves.push_back(to_lower(token)); else if (token == "wtime") is >> limits.time[WHITE]; @@ -229,27 +221,28 @@ Search::LimitsType UCI::parse_limits(const Position& pos, std::istream& is) { return limits; } -void UCI::go(Position& pos, std::istringstream& is, StateListPtr& states) { +void UCIEngine::go(std::istringstream& is) { - Search::LimitsType limits = parse_limits(pos, is); - - networks.big.verify(options["EvalFile"]); - networks.small.verify(options["EvalFileSmall"]); + Search::LimitsType limits = parse_limits(is); if (limits.perft) - { - perft(pos.fen(), limits.perft, options["UCI_Chess960"]); - return; - } - - threads.start_thinking(options, pos, states, limits); + perft(limits); + else + engine.go(limits); } -void UCI::bench(Position& pos, std::istream& args, StateListPtr& states) { +void UCIEngine::bench(std::istream& args) { std::string token; uint64_t num, nodes = 0, cnt = 1; + uint64_t nodesSearched = 0; + const auto& options = engine.get_options(); - std::vector list = setup_bench(pos, args); + engine.set_on_update_full([&](const auto& i) { + nodesSearched = i.nodes; + on_update_full(i, options["UCI_ShowWDL"]); + }); + + std::vector list = Benchmark::setup_bench(engine.fen(), args); num = count_if(list.begin(), list.end(), [](const std::string& s) { return s.find("go ") == 0 || s.find("eval") == 0; }); @@ -263,25 +256,34 @@ void UCI::bench(Position& pos, std::istream& args, StateListPtr& states) { if (token == "go" || token == "eval") { - if (Cluster::is_root()) - std::cerr << "\nPosition: " << cnt++ << '/' << num << " (" << pos.fen() << ")" + if (Distributed::is_root()) + std::cerr << "\nPosition: " << cnt++ << '/' << num << " (" << engine.fen() << ")" << std::endl; if (token == "go") { - go(pos, is, states); - threads.main_thread()->wait_for_search_finished(); - nodes += Cluster::nodes_searched(threads); + Search::LimitsType limits = parse_limits(is); + + if (limits.perft) + nodesSearched = perft(limits); + else + { + engine.go(limits); + engine.wait_for_search_finished(); + } + + nodes += nodesSearched; + nodesSearched = 0; } - else if (Cluster::is_root()) - trace_eval(pos); + else if (Distributed::is_root()) + engine.trace_eval(); } else if (token == "setoption") setoption(is); else if (token == "position") - position(pos, is, states); + position(is); else if (token == "ucinewgame") { - search_clear(); // Search::clear() may take a while + engine.search_clear(); // search_clear may take a while elapsed = now(); } } @@ -290,39 +292,187 @@ void UCI::bench(Position& pos, std::istream& args, StateListPtr& states) { dbg_print(); - if (Cluster::is_root()) - std::cerr << "\n===========================" - << "\nTotal time (ms) : " << elapsed << "\nNodes searched : " << nodes + if (Distributed::is_root()) + 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"]); }); } -void UCI::trace_eval(Position& pos) { - StateListPtr states(new std::deque(1)); - Position p; - p.set(pos.fen(), options["UCI_Chess960"], &states->back()); +void UCIEngine::benchmark(std::istream& args) { + // Probably not very important for a test this long, but include for completeness and sanity. + static constexpr int NUM_WARMUP_POSITIONS = 3; - networks.big.verify(options["EvalFile"]); - networks.small.verify(options["EvalFileSmall"]); + std::string token; + uint64_t nodes = 0, cnt = 1; + uint64_t nodesSearched = 0; + engine.set_on_update_full([&](const Engine::InfoFull& i) { nodesSearched = i.nodes; }); - sync_cout << "\n" << Eval::trace(p, networks) << sync_endl; + engine.set_on_iter([](const auto&) {}); + engine.set_on_update_no_moves([](const auto&) {}); + engine.set_on_bestmove([](const auto&, const auto&) {}); + engine.set_on_verify_networks([](const auto&) {}); + + Benchmark::BenchmarkSetup setup = Benchmark::setup_benchmark(args); + + const auto numGoCommands = count_if(setup.commands.begin(), setup.commands.end(), + [](const std::string& s) { return s.find("go ") == 0; }); + + TimePoint totalTime = 0; + + // Set options once at the start. + auto ss = std::istringstream("name Threads value " + std::to_string(setup.threads)); + setoption(ss); + ss = std::istringstream("name Hash value " + std::to_string(setup.ttSize)); + setoption(ss); + ss = std::istringstream("name UCI_Chess960 value false"); + setoption(ss); + + // Warmup + for (const auto& cmd : setup.commands) + { + std::istringstream is(cmd); + is >> std::skipws >> token; + + if (token == "go") + { + // One new line is produced by the search, so omit it here + std::cerr << "\rWarmup position " << cnt++ << '/' << NUM_WARMUP_POSITIONS; + + Search::LimitsType limits = parse_limits(is); + + // Run with silenced network verification + engine.go(limits); + engine.wait_for_search_finished(); + } + else if (token == "position") + position(is); + else if (token == "ucinewgame") + { + engine.search_clear(); // search_clear may take a while + } + + if (cnt > NUM_WARMUP_POSITIONS) + break; + } + + std::cerr << "\n"; + + cnt = 1; + nodes = 0; + + int numHashfullReadings = 0; + constexpr int hashfullAges[] = {0, 999}; // Only normal hashfull and touched hash. + constexpr int hashfullAgeCount = std::size(hashfullAges); + int totalHashfull[hashfullAgeCount] = {0}; + int maxHashfull[hashfullAgeCount] = {0}; + + auto updateHashfullReadings = [&]() { + numHashfullReadings += 1; + + for (int i = 0; i < hashfullAgeCount; ++i) + { + const int hashfull = engine.get_hashfull(hashfullAges[i]); + maxHashfull[i] = std::max(maxHashfull[i], hashfull); + totalHashfull[i] += hashfull; + } + }; + + engine.search_clear(); // search_clear may take a while + + for (const auto& cmd : setup.commands) + { + std::istringstream is(cmd); + is >> std::skipws >> token; + + if (token == "go") + { + // One new line is produced by the search, so omit it here + std::cerr << "\rPosition " << cnt++ << '/' << numGoCommands; + + Search::LimitsType limits = parse_limits(is); + + nodesSearched = 0; + TimePoint elapsed = now(); + + // Run with silenced network verification + engine.go(limits); + engine.wait_for_search_finished(); + + totalTime += now() - elapsed; + + updateHashfullReadings(); + + nodes += nodesSearched; + } + else if (token == "position") + position(is); + else if (token == "ucinewgame") + { + engine.search_clear(); // search_clear may take a while + } + } + + totalTime = std::max(totalTime, 1); // Ensure positivity to avoid a 'divide by zero' + + dbg_print(); + + std::cerr << "\n"; + + static_assert( + std::size(hashfullAges) == 2 && hashfullAges[0] == 0 && hashfullAges[1] == 999, + "Hardcoded for display. Would complicate the code needlessly in the current state."); + + std::string threadBinding = engine.thread_binding_information_as_string(); + if (threadBinding.empty()) + threadBinding = "none"; + + // clang-format off + + std::cerr << "===========================" + << "\nVersion : " + << engine_version_info() + // "\nCompiled by : " + << compiler_info() + << "Large pages : " << (has_large_pages() ? "yes" : "no") + << "\nUser invocation : " << BenchmarkCommand << " " + << setup.originalInvocation << "\nFilled invocation : " << BenchmarkCommand + << " " << setup.filledInvocation + << "\nAvailable processors : " << engine.get_numa_config_as_string() + << "\nThread count : " << setup.threads + << "\nThread binding : " << threadBinding + << "\nTT size [MiB] : " << setup.ttSize + << "\nHash max, avg [per mille] : " + << "\n single search : " << maxHashfull[0] << ", " + << totalHashfull[0] / numHashfullReadings + << "\n single game : " << maxHashfull[1] << ", " + << totalHashfull[1] / numHashfullReadings + << "\nTotal nodes searched : " << nodes + << "\nTotal search time [s] : " << totalTime / 1000.0 + << "\nNodes/second : " << 1000 * nodes / totalTime << std::endl; + + // clang-format on + + init_search_update_listeners(); } -void UCI::search_clear() { - threads.main_thread()->wait_for_search_finished(); - - tt.clear(options["Threads"]); - threads.clear(); - Tablebases::init(options["SyzygyPath"]); // Free mapped files +void UCIEngine::setoption(std::istringstream& is) { + engine.wait_for_search_finished(); + engine.get_options().setoption(is); } -void UCI::setoption(std::istringstream& is) { - threads.main_thread()->wait_for_search_finished(); - options.setoption(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; + return nodes; } -void UCI::position(Position& pos, std::istringstream& is, StateListPtr& states) { - Move m; +void UCIEngine::position(std::istringstream& is) { std::string token, fen; is >> token; @@ -338,15 +488,14 @@ void UCI::position(Position& pos, std::istringstream& is, StateListPtr& states) else return; - states = StateListPtr(new std::deque(1)); // Drop the old state and create a new one - pos.set(fen, options["UCI_Chess960"], &states->back()); + std::vector moves; - // Parse the move list, if any - while (is >> token && (m = to_move(pos, token)) != Move::none()) + while (is >> token) { - states->emplace_back(); - pos.do_move(m, states->back()); + moves.push_back(token); } + + engine.set_position(fen, moves); } namespace { @@ -361,12 +510,12 @@ WinRateParams win_rate_params(const Position& pos) { int material = pos.count() + 3 * pos.count() + 3 * pos.count() + 5 * pos.count() + 9 * pos.count(); - // The fitted model only uses data for material counts in [10, 78], and is anchored at count 58. - double m = std::clamp(material, 10, 78) / 58.0; + // The fitted model only uses data for material counts in [17, 78], and is anchored at count 58. + 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[] = {-185.71965483, 504.85014385, -438.58295743, 474.04604627}; - constexpr double bs[] = {89.23542728, -137.02141296, 73.28669021, 47.53376190}; + constexpr double as[] = {-13.50030198, 40.92780883, -36.82753545, 386.83004070}; + constexpr double bs[] = {96.53354896, -165.79058388, 90.89679019, 49.29561889}; 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]; @@ -385,53 +534,53 @@ int win_rate_model(Value v, const Position& pos) { } } -std::string UCI::to_score(Value v, const Position& pos) { - assert(-VALUE_INFINITE < v && v < VALUE_INFINITE); +std::string UCIEngine::format_score(const Score& s) { + constexpr int TB_CP = 20000; + const auto format = + overload{[](Score::Mate mate) -> std::string { + auto m = (mate.plies > 0 ? (mate.plies + 1) : mate.plies) / 2; + return std::string("mate ") + std::to_string(m); + }, + [](Score::Tablebase tb) -> std::string { + return std::string("cp ") + + std::to_string((tb.win ? TB_CP - tb.plies : -TB_CP - tb.plies)); + }, + [](Score::InternalUnits units) -> std::string { + return std::string("cp ") + std::to_string(units.value); + }}; - std::stringstream ss; - - if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY) - ss << "cp " << to_cp(v, pos); - else if (std::abs(v) <= VALUE_TB) - { - const int ply = VALUE_TB - std::abs(v); // recompute ss->ply - ss << "cp " << (v > 0 ? 20000 - ply : -20000 + ply); - } - else - ss << "mate " << (v > 0 ? VALUE_MATE - v + 1 : -VALUE_MATE - v) / 2; - - return ss.str(); + return s.visit(format); } // Turns a Value to an integer centipawn number, // without treatment of mate and similar special scores. -int UCI::to_cp(Value v, const Position& pos) { +int UCIEngine::to_cp(Value v, const Position& pos) { - // In general, the score can be defined via the the WDL as - // (log(1/L - 1) - log(1/W - 1)) / ((log(1/L - 1) + log(1/W - 1)) + // In general, the score can be defined via the WDL as + // (log(1/L - 1) - log(1/W - 1)) / (log(1/L - 1) + log(1/W - 1)). // Based on our win_rate_model, this simply yields v / a. auto [a, b] = win_rate_params(pos); - return std::round(100 * int(v) / a); + return int(std::round(100 * int(v) / a)); } -std::string UCI::wdl(Value v, const Position& pos) { +std::string UCIEngine::wdl(Value v, const Position& pos) { std::stringstream ss; int wdl_w = win_rate_model(v, pos); int wdl_l = win_rate_model(-v, pos); int wdl_d = 1000 - wdl_w - wdl_l; - ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l; + ss << wdl_w << " " << wdl_d << " " << wdl_l; return ss.str(); } -std::string UCI::square(Square s) { +std::string UCIEngine::square(Square s) { return std::string{char('a' + file_of(s)), char('1' + rank_of(s))}; } -std::string UCI::move(Move m, bool chess960) { +std::string UCIEngine::move(Move m, bool chess960) { if (m == Move::none()) return "(none)"; @@ -453,9 +602,14 @@ std::string UCI::move(Move m, bool chess960) { } -Move UCI::to_move(const Position& pos, std::string& str) { - if (str.length() == 5) - str[4] = char(tolower(str[4])); // The promotion piece character must be lowercased +std::string UCIEngine::to_lower(std::string str) { + std::transform(str.begin(), str.end(), str.begin(), [](auto c) { return std::tolower(c); }); + + return str; +} + +Move UCIEngine::to_move(const Position& pos, std::string str) { + str = to_lower(str); for (const auto& m : MoveList(pos)) if (str == move(m, pos.is_chess960())) @@ -464,4 +618,51 @@ Move UCI::to_move(const Position& pos, std::string& str) { return Move::none(); } +void UCIEngine::on_update_no_moves(const Engine::InfoShort& info) { + sync_cout << "info depth " << info.depth << " score " << format_score(info.score) << sync_endl; +} + +void UCIEngine::on_update_full(const Engine::InfoFull& info, bool showWDL) { + std::stringstream ss; + + ss << "info"; + ss << " depth " << info.depth // + << " seldepth " << info.selDepth // + << " multipv " << info.multiPV // + << " score " << format_score(info.score); // + + if (!info.bound.empty()) + ss << " " << info.bound; + + if (showWDL) + ss << " wdl " << info.wdl; + + ss << " nodes " << info.nodes // + << " nps " << info.nps // + << " hashfull " << info.hashfull // + << " tbhits " << info.tbHits // + << " time " << info.timeMs // + << " pv " << info.pv; // + + sync_cout << ss.str() << sync_endl; +} + +void UCIEngine::on_iter(const Engine::InfoIter& info) { + std::stringstream ss; + + ss << "info"; + ss << " depth " << info.depth // + << " currmove " << info.currmove // + << " currmovenumber " << info.currmovenumber; // + + sync_cout << ss.str() << sync_endl; +} + +void UCIEngine::on_bestmove(std::string_view bestmove, std::string_view ponder) { + sync_cout << "bestmove " << bestmove; + if (!ponder.empty()) + std::cout << " ponder " << ponder; + std::cout << sync_endl; +} + } // namespace Stockfish diff --git a/src/uci.h b/src/uci.h index 237928d9a..c9b594393 100644 --- a/src/uci.h +++ b/src/uci.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -19,54 +19,60 @@ #ifndef UCI_H_INCLUDED #define UCI_H_INCLUDED +#include #include #include +#include +#include "engine.h" #include "misc.h" -#include "nnue/network.h" -#include "position.h" #include "search.h" -#include "thread.h" -#include "tt.h" -#include "ucioption.h" namespace Stockfish { +class Position; class Move; -enum Square : int; +class Score; +enum Square : int8_t; using Value = int; -class UCI { +class UCIEngine { public: - UCI(int argc, char** argv); + UCIEngine(int argc, char** argv); void loop(); static int to_cp(Value v, const Position& pos); - static std::string to_score(Value v, const Position& pos); + static std::string format_score(const Score& s); static std::string square(Square s); static std::string move(Move m, bool chess960); static std::string wdl(Value v, const Position& pos); - static Move to_move(const Position& pos, std::string& str); + static std::string to_lower(std::string str); + static Move to_move(const Position& pos, std::string str); - static Search::LimitsType parse_limits(const Position& pos, std::istream& is); + static Search::LimitsType parse_limits(std::istream& is); - const std::string& working_directory() const { return cli.workingDirectory; } - - OptionsMap options; - Eval::NNUE::Networks networks; + auto& engine_options() { return engine.get_options(); } private: - TranspositionTable tt; - ThreadPool threads; - CommandLine cli; + Engine engine; + CommandLine cli; - void go(Position& pos, std::istringstream& is, StateListPtr& states); - void bench(Position& pos, std::istream& args, StateListPtr& states); - void position(Position& pos, std::istringstream& is, StateListPtr& states); - void trace_eval(Position& pos); - void search_clear(); - void setoption(std::istringstream& is); + static void print_info_string(std::string_view str); + + void go(std::istringstream& is); + void bench(std::istream& args); + void benchmark(std::istream& args); + void position(std::istringstream& is); + void setoption(std::istringstream& is); + std::uint64_t perft(const Search::LimitsType&); + + static void on_update_no_moves(const Engine::InfoShort& info); + static void on_update_full(const Engine::InfoFull& info, bool showWDL); + static void on_iter(const Engine::InfoIter& info); + static void on_bestmove(std::string_view bestmove, std::string_view ponder); + + void init_search_update_listeners(); }; } // namespace Stockfish diff --git a/src/ucioption.cpp b/src/ucioption.cpp index 075be2c73..1e10850b1 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,8 @@ bool CaseInsensitiveLess::operator()(const std::string& s1, const std::string& s [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }); } +void OptionsMap::add_info_listener(InfoListener&& message_func) { info = std::move(message_func); } + void OptionsMap::setoption(std::istringstream& is) { std::string token, name, value; @@ -52,19 +55,40 @@ void OptionsMap::setoption(std::istringstream& is) { if (options_map.count(name)) options_map[name] = value; - else if (Cluster::is_root()) + else if (Distributed::is_root()) sync_cout << "No such option: " << name << sync_endl; } -Option OptionsMap::operator[](const std::string& name) const { +const Option& OptionsMap::operator[](const std::string& name) const { auto it = options_map.find(name); - return it != options_map.end() ? it->second : Option(); + assert(it != options_map.end()); + return it->second; +} + +// Inits options and assigns idx in the correct printing order +void OptionsMap::add(const std::string& name, const Option& option) { + if (!options_map.count(name)) + { + static size_t insert_order = 0; + + options_map[name] = option; + + options_map[name].parent = this; + options_map[name].idx = insert_order++; + } + else + { + std::cerr << "Option \"" << name << "\" was already added!" << std::endl; + std::exit(EXIT_FAILURE); + } } -Option& OptionsMap::operator[](const std::string& name) { return options_map[name]; } std::size_t OptionsMap::count(const std::string& name) const { return options_map.count(name); } +Option::Option(const OptionsMap* map) : + parent(map) {} + Option::Option(const char* v, OnChange f) : type("string"), min(0), @@ -87,7 +111,7 @@ Option::Option(OnChange f) : max(0), on_change(std::move(f)) {} -Option::Option(double v, int minv, int maxv, OnChange f) : +Option::Option(int v, int minv, int maxv, OnChange f) : type("spin"), min(minv), max(maxv), @@ -119,16 +143,7 @@ bool Option::operator==(const char* s) const { return !CaseInsensitiveLess()(currentValue, s) && !CaseInsensitiveLess()(s, currentValue); } - -// Inits options and assigns idx in the correct printing order - -void Option::operator<<(const Option& o) { - - static size_t insert_order = 0; - - *this = o; - idx = insert_order++; -} +bool Option::operator!=(const char* s) const { return !(*this == s); } // Updates currentValue and triggers on_change() action. It's up to @@ -140,7 +155,7 @@ Option& Option::operator=(const std::string& v) { if ((type != "button" && type != "string" && v.empty()) || (type == "check" && v != "true" && v != "false") - || (type == "spin" && (std::stof(v) < min || std::stof(v) > max))) + || (type == "spin" && (std::stoi(v) < min || std::stoi(v) > max))) return *this; if (type == "combo") @@ -149,16 +164,23 @@ Option& Option::operator=(const std::string& v) { std::string token; std::istringstream ss(defaultValue); while (ss >> token) - comboMap[token] << Option(); + comboMap.add(token, Option()); if (!comboMap.count(v) || v == "var") return *this; } - if (type != "button") + if (type == "string") + currentValue = v == "" ? "" : v; + else if (type != "button") currentValue = v; if (on_change) - on_change(*this); + { + const auto ret = on_change(*this); + + if (ret && parent != nullptr && parent->info != nullptr) + parent->info(ret); + } return *this; } @@ -171,11 +193,17 @@ std::ostream& operator<<(std::ostream& os, const OptionsMap& om) { const Option& o = it.second; os << "\noption name " << it.first << " type " << o.type; - if (o.type == "string" || o.type == "check" || o.type == "combo") + if (o.type == "check" || o.type == "combo") os << " default " << o.defaultValue; - if (o.type == "spin") - os << " default " << int(stof(o.defaultValue)) << " min " << o.min << " max " + else if (o.type == "string") + { + std::string defaultValue = o.defaultValue.empty() ? "" : o.defaultValue; + os << " default " << defaultValue; + } + + else if (o.type == "spin") + os << " default " << stoi(o.defaultValue) << " min " << o.min << " max " << o.max; break; diff --git a/src/ucioption.h b/src/ucioption.h index b575d1646..4f6d7541c 100644 --- a/src/ucioption.h +++ b/src/ucioption.h @@ -1,6 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + 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 @@ -23,6 +23,7 @@ #include #include #include +#include #include namespace Stockfish { @@ -31,50 +32,74 @@ struct CaseInsensitiveLess { bool operator()(const std::string&, const std::string&) const; }; -class Option; - -class OptionsMap { - public: - void setoption(std::istringstream&); - - friend std::ostream& operator<<(std::ostream&, const OptionsMap&); - - Option operator[](const std::string&) const; - Option& operator[](const std::string&); - - std::size_t count(const std::string&) const; - - private: - // The options container is defined as a std::map - using OptionsStore = std::map; - - OptionsStore options_map; -}; +class OptionsMap; // The Option class implements each option as specified by the UCI protocol class Option { public: - using OnChange = std::function; + using OnChange = std::function(const Option&)>; + Option(const OptionsMap*); Option(OnChange = nullptr); Option(bool v, OnChange = nullptr); Option(const char* v, OnChange = nullptr); - Option(double v, int minv, int maxv, OnChange = nullptr); + Option(int v, int minv, int maxv, OnChange = nullptr); Option(const char* v, const char* cur, OnChange = nullptr); Option& operator=(const std::string&); - void operator<<(const Option&); operator int() const; operator std::string() const; bool operator==(const char*) const; + bool operator!=(const char*) const; friend std::ostream& operator<<(std::ostream&, const OptionsMap&); + int operator<<(const Option&) = delete; + private: - std::string defaultValue, currentValue, type; - int min, max; - size_t idx; - OnChange on_change; + friend class OptionsMap; + friend class Engine; + friend class Tune; + + + std::string defaultValue, currentValue, type; + int min, max; + size_t idx; + OnChange on_change; + const OptionsMap* parent = nullptr; +}; + +class OptionsMap { + public: + using InfoListener = std::function)>; + + OptionsMap() = default; + OptionsMap(const OptionsMap&) = delete; + OptionsMap(OptionsMap&&) = delete; + OptionsMap& operator=(const OptionsMap&) = delete; + OptionsMap& operator=(OptionsMap&&) = delete; + + void add_info_listener(InfoListener&&); + + void setoption(std::istringstream&); + + const Option& operator[](const std::string&) const; + + void add(const std::string&, const Option& option); + + std::size_t count(const std::string&) const; + + private: + friend class Engine; + friend class Option; + + friend std::ostream& operator<<(std::ostream&, const OptionsMap&); + + // The options container is defined as a std::map + using OptionsStore = std::map; + + OptionsStore options_map; + InfoListener info; }; } diff --git a/tests/.gitattributes b/tests/.gitattributes new file mode 100644 index 000000000..dfdb8b771 --- /dev/null +++ b/tests/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/tests/instrumented.py b/tests/instrumented.py new file mode 100644 index 000000000..80831ce3f --- /dev/null +++ b/tests/instrumented.py @@ -0,0 +1,519 @@ +import argparse +import re +import sys +import subprocess +import pathlib +import os + +from testing import ( + EPD, + TSAN, + Stockfish as Engine, + MiniTestFramework, + OrderedClassMembers, + Valgrind, + Syzygy, +) + +PATH = pathlib.Path(__file__).parent.resolve() +CWD = os.getcwd() + + +def get_prefix(): + if args.valgrind: + return Valgrind.get_valgrind_command() + if args.valgrind_thread: + return Valgrind.get_valgrind_thread_command() + + return [] + + +def get_threads(): + if args.valgrind_thread or args.sanitizer_thread: + return 2 + return 1 + + +def get_path(): + return os.path.abspath(os.path.join(CWD, args.stockfish_path)) + + +def postfix_check(output): + if args.sanitizer_undefined: + for idx, line in enumerate(output): + if "runtime error:" in line: + # print next possible 50 lines + for i in range(50): + debug_idx = idx + i + if debug_idx < len(output): + print(output[debug_idx]) + return False + + if args.sanitizer_thread: + for idx, line in enumerate(output): + if "WARNING: ThreadSanitizer:" in line: + # print next possible 50 lines + for i in range(50): + debug_idx = idx + i + if debug_idx < len(output): + print(output[debug_idx]) + return False + + return True + + +def Stockfish(*args, **kwargs): + return Engine(get_prefix(), get_path(), *args, **kwargs) + + +class TestCLI(metaclass=OrderedClassMembers): + def beforeAll(self): + pass + + def afterAll(self): + pass + + def beforeEach(self): + self.stockfish = None + + def afterEach(self): + assert postfix_check(self.stockfish.get_output()) == True + self.stockfish.clear_output() + + def test_eval(self): + self.stockfish = Stockfish("eval".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_go_nodes_1000(self): + self.stockfish = Stockfish("go nodes 1000".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_go_depth_10(self): + self.stockfish = Stockfish("go depth 10".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_go_perft_4(self): + self.stockfish = Stockfish("go perft 4".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_go_movetime_1000(self): + self.stockfish = Stockfish("go movetime 1000".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_go_wtime_8000_btime_8000_winc_500_binc_500(self): + self.stockfish = Stockfish( + "go wtime 8000 btime 8000 winc 500 binc 500".split(" "), + True, + ) + assert self.stockfish.process.returncode == 0 + + def test_go_wtime_1000_btime_1000_winc_0_binc_0(self): + self.stockfish = Stockfish( + "go wtime 1000 btime 1000 winc 0 binc 0".split(" "), + True, + ) + assert self.stockfish.process.returncode == 0 + + def test_go_wtime_1000_btime_1000_winc_0_binc_0_movestogo_5(self): + self.stockfish = Stockfish( + "go wtime 1000 btime 1000 winc 0 binc 0 movestogo 5".split(" "), + True, + ) + assert self.stockfish.process.returncode == 0 + + def test_go_movetime_200(self): + self.stockfish = Stockfish("go movetime 200".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_go_nodes_20000_searchmoves_e2e4_d2d4(self): + self.stockfish = Stockfish( + "go nodes 20000 searchmoves e2e4 d2d4".split(" "), True + ) + assert self.stockfish.process.returncode == 0 + + def test_bench_128_threads_8_default_depth(self): + self.stockfish = Stockfish( + f"bench 128 {get_threads()} 8 default depth".split(" "), + True, + ) + assert self.stockfish.process.returncode == 0 + + def test_bench_128_threads_3_bench_tmp_epd_depth(self): + self.stockfish = Stockfish( + f"bench 128 {get_threads()} 3 {os.path.join(PATH, 'bench_tmp.epd')} depth".split( + " " + ), + True, + ) + assert self.stockfish.process.returncode == 0 + + def test_d(self): + self.stockfish = Stockfish("d".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_compiler(self): + self.stockfish = Stockfish("compiler".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_license(self): + self.stockfish = Stockfish("license".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_uci(self): + self.stockfish = Stockfish("uci".split(" "), True) + assert self.stockfish.process.returncode == 0 + + def test_export_net_verify_nnue(self): + current_path = os.path.abspath(os.getcwd()) + self.stockfish = Stockfish( + f"export_net {os.path.join(current_path, 'verify.nnue')}".split(" "), True + ) + assert self.stockfish.process.returncode == 0 + + # verify the generated net equals the base net + + def test_network_equals_base(self): + self.stockfish = Stockfish( + ["uci"], + True, + ) + + output = self.stockfish.process.stdout + + # find line + for line in output.split("\n"): + if "option name EvalFile type string default" in line: + network = line.split(" ")[-1] + break + + # find network file in src dir + network = os.path.join(PATH.parent.resolve(), "src", network) + + if not os.path.exists(network): + print( + f"Network file {network} not found, please download the network file over the make command." + ) + assert False + + diff = subprocess.run(["diff", network, f"verify.nnue"]) + + assert diff.returncode == 0 + + +class TestInteractive(metaclass=OrderedClassMembers): + def beforeAll(self): + self.stockfish = Stockfish() + + def afterAll(self): + self.stockfish.quit() + assert self.stockfish.close() == 0 + + def afterEach(self): + assert postfix_check(self.stockfish.get_output()) == True + self.stockfish.clear_output() + + def test_startup_output(self): + self.stockfish.starts_with("Stockfish") + + def test_uci_command(self): + self.stockfish.send_command("uci") + self.stockfish.equals("uciok") + + def test_set_threads_option(self): + self.stockfish.send_command(f"setoption name Threads value {get_threads()}") + + def test_ucinewgame_and_startpos_nodes_1000(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("position startpos") + self.stockfish.send_command("go nodes 1000") + self.stockfish.starts_with("bestmove") + + def test_ucinewgame_and_startpos_moves(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("position startpos moves e2e4 e7e6") + self.stockfish.send_command("go nodes 1000") + self.stockfish.starts_with("bestmove") + + def test_fen_position_1(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("position fen 5rk1/1K4p1/8/8/3B4/8/8/8 b - - 0 1") + self.stockfish.send_command("go nodes 1000") + self.stockfish.starts_with("bestmove") + + def test_fen_position_2_flip(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("position fen 5rk1/1K4p1/8/8/3B4/8/8/8 b - - 0 1") + self.stockfish.send_command("flip") + self.stockfish.send_command("go nodes 1000") + self.stockfish.starts_with("bestmove") + + def test_depth_5_with_callback(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("position startpos") + self.stockfish.send_command("go depth 5") + + def callback(output): + regex = r"info depth \d+ seldepth \d+ multipv \d+ score cp -?\d+ nodes \d+ nps \d+ hashfull \d+ tbhits \d+ time \d+ pv" + if output.startswith("info depth") and not re.match(regex, output): + assert False + if output.startswith("bestmove"): + return True + return False + + self.stockfish.check_output(callback) + + def test_ucinewgame_and_go_depth_9(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("setoption name UCI_ShowWDL value true") + self.stockfish.send_command("position startpos") + self.stockfish.send_command("go depth 9") + + depth = 1 + + def callback(output): + nonlocal depth + + regex = rf"info depth {depth} seldepth \d+ multipv \d+ score cp -?\d+ wdl \d+ \d+ \d+ nodes \d+ nps \d+ hashfull \d+ tbhits \d+ time \d+ pv" + + if output.startswith("info depth"): + if not re.match(regex, output): + assert False + depth += 1 + + if output.startswith("bestmove"): + assert depth == 10 + return True + + return False + + self.stockfish.check_output(callback) + + def test_clear_hash(self): + self.stockfish.send_command("setoption name Clear Hash") + + def test_fen_position_mate_1(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 5K2/8/2qk4/2nPp3/3r4/6B1/B7/3R4 w - e6" + ) + self.stockfish.send_command("go depth 18") + + self.stockfish.expect("* score mate 1 * pv d5e6") + self.stockfish.equals("bestmove d5e6") + + def test_fen_position_mate_minus_1(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 2brrb2/8/p7/Q7/1p1kpPp1/1P1pN1K1/3P4/8 b - -" + ) + self.stockfish.send_command("go depth 18") + self.stockfish.expect("* score mate -1 *") + self.stockfish.starts_with("bestmove") + + def test_fen_position_fixed_node(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 5K2/8/2P1P1Pk/6pP/3p2P1/1P6/3P4/8 w - - 0 1" + ) + self.stockfish.send_command("go nodes 500000") + self.stockfish.starts_with("bestmove") + + def test_fen_position_with_mate_go_depth(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 8/5R2/2K1P3/4k3/8/b1PPpp1B/5p2/8 w - -" + ) + self.stockfish.send_command("go depth 18 searchmoves c6d7") + self.stockfish.expect("* score mate 2 * pv c6d7 * f7f5") + + self.stockfish.starts_with("bestmove") + + def test_fen_position_with_mate_go_mate(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 8/5R2/2K1P3/4k3/8/b1PPpp1B/5p2/8 w - -" + ) + self.stockfish.send_command("go mate 2 searchmoves c6d7") + self.stockfish.expect("* score mate 2 * pv c6d7 *") + + self.stockfish.starts_with("bestmove") + + def test_fen_position_with_mate_go_nodes(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 8/5R2/2K1P3/4k3/8/b1PPpp1B/5p2/8 w - -" + ) + self.stockfish.send_command("go nodes 500000 searchmoves c6d7") + self.stockfish.expect("* score mate 2 * pv c6d7 * f7f5") + + self.stockfish.starts_with("bestmove") + + def test_fen_position_depth_27(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen r1b2r1k/pp1p2pp/2p5/2B1q3/8/8/P1PN2PP/R4RK1 w - - 0 18" + ) + self.stockfish.send_command("go") + self.stockfish.contains("score mate 1") + + self.stockfish.starts_with("bestmove") + + def test_fen_position_with_mate_go_depth_and_promotion(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 8/5R2/2K1P3/4k3/8/b1PPpp1B/5p2/8 w - - moves c6d7 f2f1q" + ) + self.stockfish.send_command("go depth 18") + self.stockfish.expect("* score mate 1 * pv f7f5") + self.stockfish.starts_with("bestmove f7f5") + + def test_fen_position_with_mate_go_depth_and_searchmoves(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 8/5R2/2K1P3/4k3/8/b1PPpp1B/5p2/8 w - -" + ) + self.stockfish.send_command("go depth 18 searchmoves c6d7") + self.stockfish.expect("* score mate 2 * pv c6d7 * f7f5") + + self.stockfish.starts_with("bestmove c6d7") + + def test_fen_position_with_moves_with_mate_go_depth_and_searchmoves(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command( + "position fen 8/5R2/2K1P3/4k3/8/b1PPpp1B/5p2/8 w - - moves c6d7" + ) + self.stockfish.send_command("go depth 18 searchmoves e3e2") + self.stockfish.expect("* score mate -1 * pv e3e2 f7f5") + self.stockfish.starts_with("bestmove e3e2") + + def test_verify_nnue_network(self): + current_path = os.path.abspath(os.getcwd()) + Stockfish( + f"export_net {os.path.join(current_path, 'verify.nnue')}".split(" "), True + ) + + self.stockfish.send_command("setoption name EvalFile value verify.nnue") + self.stockfish.send_command("position startpos") + self.stockfish.send_command("go depth 5") + self.stockfish.starts_with("bestmove") + + def test_multipv_setting(self): + self.stockfish.send_command("setoption name MultiPV value 4") + self.stockfish.send_command("position startpos") + self.stockfish.send_command("go depth 5") + self.stockfish.starts_with("bestmove") + + def test_fen_position_with_skill_level(self): + self.stockfish.send_command("setoption name Skill Level value 10") + self.stockfish.send_command("position startpos") + self.stockfish.send_command("go depth 5") + self.stockfish.starts_with("bestmove") + + self.stockfish.send_command("setoption name Skill Level value 20") + + +class TestSyzygy(metaclass=OrderedClassMembers): + def beforeAll(self): + self.stockfish = Stockfish() + + def afterAll(self): + self.stockfish.quit() + assert self.stockfish.close() == 0 + + def afterEach(self): + assert postfix_check(self.stockfish.get_output()) == True + self.stockfish.clear_output() + + def test_syzygy_setup(self): + self.stockfish.starts_with("Stockfish") + self.stockfish.send_command("uci") + self.stockfish.send_command( + f"setoption name SyzygyPath value {os.path.join(PATH, 'syzygy')}" + ) + self.stockfish.expect( + "info string Found 35 WDL and 35 DTZ tablebase files (up to 4-man)." + ) + + def test_syzygy_bench(self): + self.stockfish.send_command("bench 128 1 8 default depth") + self.stockfish.expect("Nodes searched :*") + + def test_syzygy_position(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("position fen 4k3/PP6/8/8/8/8/8/4K3 w - - 0 1") + self.stockfish.send_command("go depth 5") + + def check_output(output): + if "score cp 20000" in output or "score mate" in output: + return True + + self.stockfish.check_output(check_output) + self.stockfish.expect("bestmove *") + + def test_syzygy_position_2(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("position fen 8/1P6/2B5/8/4K3/8/6k1/8 w - - 0 1") + self.stockfish.send_command("go depth 5") + + def check_output(output): + if "score cp 20000" in output or "score mate" in output: + return True + + self.stockfish.check_output(check_output) + self.stockfish.expect("bestmove *") + + def test_syzygy_position_3(self): + self.stockfish.send_command("ucinewgame") + self.stockfish.send_command("position fen 8/1P6/2B5/8/4K3/8/6k1/8 b - - 0 1") + self.stockfish.send_command("go depth 5") + + def check_output(output): + if "score cp -20000" in output or "score mate -" in output: + return True + + self.stockfish.check_output(check_output) + self.stockfish.expect("bestmove *") + + +def parse_args(): + parser = argparse.ArgumentParser(description="Run Stockfish with testing options") + parser.add_argument("--valgrind", action="store_true", help="Run valgrind testing") + parser.add_argument( + "--valgrind-thread", action="store_true", help="Run valgrind-thread testing" + ) + parser.add_argument( + "--sanitizer-undefined", + action="store_true", + help="Run sanitizer-undefined testing", + ) + parser.add_argument( + "--sanitizer-thread", action="store_true", help="Run sanitizer-thread testing" + ) + + parser.add_argument( + "--none", action="store_true", help="Run without any testing options" + ) + parser.add_argument("stockfish_path", type=str, help="Path to Stockfish binary") + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + + EPD.create_bench_epd() + TSAN.set_tsan_option() + Syzygy.download_syzygy() + + framework = MiniTestFramework() + + # Each test suite will be run inside a temporary directory + framework.run([TestCLI, TestInteractive, TestSyzygy]) + + EPD.delete_bench_epd() + TSAN.unset_tsan_option() + + if framework.has_failed(): + sys.exit(1) + + sys.exit(0) diff --git a/tests/instrumented.sh b/tests/instrumented.sh deleted file mode 100755 index 525c7e040..000000000 --- a/tests/instrumented.sh +++ /dev/null @@ -1,206 +0,0 @@ -#!/bin/bash -# check for errors under Valgrind or sanitizers. - -error() -{ - echo "instrumented testing failed on line $1" - exit 1 -} -trap 'error ${LINENO}' ERR - -# Since Linux Kernel 6.5 we are getting false positives from the ci, -# lower the ALSR entropy to disable ALSR, which works as a temporary workaround. -# https://github.com/google/sanitizers/issues/1716 -# https://bugs.launchpad.net/ubuntu/+source/linux/+bug/2056762 -sudo sysctl -w vm.mmap_rnd_bits=28 - - -# define suitable post and prefixes for testing options -case $1 in - --valgrind) - echo "valgrind testing started" - prefix='' - exeprefix='valgrind --error-exitcode=42 --errors-for-leak-kinds=all --leak-check=full' - postfix='1>/dev/null' - threads="1" - ;; - --valgrind-thread) - echo "valgrind-thread testing started" - prefix='' - exeprefix='valgrind --fair-sched=try --error-exitcode=42' - postfix='1>/dev/null' - threads="2" - ;; - --sanitizer-undefined) - echo "sanitizer-undefined testing started" - prefix='!' - exeprefix='' - postfix='2>&1 | grep -A50 "runtime error:"' - threads="1" - ;; - --sanitizer-thread) - echo "sanitizer-thread testing started" - prefix='!' - exeprefix='' - postfix='2>&1 | grep -A50 "WARNING: ThreadSanitizer:"' - threads="2" - -cat << EOF > tsan.supp -race:Stockfish::TTEntry::move -race:Stockfish::TTEntry::depth -race:Stockfish::TTEntry::bound -race:Stockfish::TTEntry::save -race:Stockfish::TTEntry::value -race:Stockfish::TTEntry::eval -race:Stockfish::TTEntry::is_pv - -race:Stockfish::TranspositionTable::probe -race:Stockfish::TranspositionTable::hashfull - -EOF - - export TSAN_OPTIONS="suppressions=./tsan.supp" - - ;; - *) - echo "unknown testing started" - prefix='' - exeprefix='' - postfix='' - threads="1" - ;; -esac - -cat << EOF > bench_tmp.epd -Rn6/1rbq1bk1/2p2n1p/2Bp1p2/3Pp1pP/1N2P1P1/2Q1NPB1/6K1 w - - 2 26 -rnbqkb1r/ppp1pp2/5n1p/3p2p1/P2PP3/5P2/1PP3PP/RNBQKBNR w KQkq - 0 3 -3qnrk1/4bp1p/1p2p1pP/p2bN3/1P1P1B2/P2BQ3/5PP1/4R1K1 w - - 9 28 -r4rk1/1b2ppbp/pq4pn/2pp1PB1/1p2P3/1P1P1NN1/1PP3PP/R2Q1RK1 w - - 0 13 -EOF - -# simple command line testing -for args in "eval" \ - "go nodes 1000" \ - "go depth 10" \ - "go perft 4" \ - "go movetime 1000" \ - "go wtime 8000 btime 8000 winc 500 binc 500" \ - "go wtime 1000 btime 1000 winc 0 binc 0" \ - "go wtime 1000 btime 1000 winc 0 binc 0" \ - "go wtime 1000 btime 1000 winc 0 binc 0 movestogo 5" \ - "go movetime 200" \ - "go nodes 20000 searchmoves e2e4 d2d4" \ - "bench 128 $threads 8 default depth" \ - "bench 128 $threads 3 bench_tmp.epd depth" \ - "export_net verify.nnue" \ - "d" \ - "compiler" \ - "license" \ - "uci" -do - - echo "$prefix $exeprefix ./stockfish $args $postfix" - eval "$prefix $exeprefix ./stockfish $args $postfix" - -done - -# verify the generated net equals the base net -network=`./stockfish uci | grep 'option name EvalFile type string default' | awk '{print $NF}'` -echo "Comparing $network to the written verify.nnue" -diff $network verify.nnue - -# more general testing, following an uci protocol exchange -cat << EOF > game.exp - set timeout 240 - spawn $exeprefix ./stockfish - - send "uci\n" - expect "uciok" - - # send "setoption name Debug Log File value debug.log\n" - send "setoption name Threads value $threads\n" - - send "ucinewgame\n" - send "position startpos\n" - send "go nodes 1000\n" - expect "bestmove" - - send "position startpos moves e2e4 e7e6\n" - send "go nodes 1000\n" - expect "bestmove" - - send "position fen 5rk1/1K4p1/8/8/3B4/8/8/8 b - - 0 1\n" - send "go depth 10\n" - expect "bestmove" - - send "setoption name UCI_ShowWDL value true\n" - send "position startpos\n" - send "flip\n" - send "go depth 5\n" - expect "bestmove" - - send "setoption name Skill Level value 10\n" - send "position startpos\n" - send "go depth 5\n" - expect "bestmove" - - send "setoption name Clear Hash\n" - - send "setoption name EvalFile value verify.nnue\n" - send "position startpos\n" - send "go depth 5\n" - expect "bestmove" - - send "setoption name MultiPV value 4\n" - send "position startpos\n" - send "go depth 5\n" - - send "quit\n" - expect eof - - # return error code of the spawned program, useful for Valgrind - lassign [wait] pid spawnid os_error_flag value - exit \$value -EOF - -#download TB as needed -if [ ! -d ../tests/syzygy ]; then - curl -sL https://api.github.com/repos/niklasf/python-chess/tarball/9b9aa13f9f36d08aadfabff872882f4ab1494e95 | tar -xzf - - mv niklasf-python-chess-9b9aa13 ../tests/syzygy -fi - -cat << EOF > syzygy.exp - set timeout 240 - spawn $exeprefix ./stockfish - send "uci\n" - send "setoption name SyzygyPath value ../tests/syzygy/\n" - expect "info string Found 35 tablebases" {} timeout {exit 1} - send "bench 128 1 8 default depth\n" - send "ucinewgame\n" - send "position fen 4k3/PP6/8/8/8/8/8/4K3 w - - 0 1\n" - send "go depth 5\n" - expect "bestmove" - send "position fen 8/1P6/2B5/8/4K3/8/6k1/8 w - - 0 1\n" - send "go depth 5\n" - expect "bestmove" - send "quit\n" - expect eof - - # return error code of the spawned program, useful for Valgrind - lassign [wait] pid spawnid os_error_flag value - exit \$value -EOF - -for exp in game.exp syzygy.exp -do - - echo "$prefix expect $exp $postfix" - eval "$prefix expect $exp $postfix" - - rm $exp - -done - -rm -f tsan.supp bench_tmp.epd - -echo "instrumented testing OK" diff --git a/tests/perft.sh b/tests/perft.sh index 545e750fe..bb32bee56 100755 --- a/tests/perft.sh +++ b/tests/perft.sh @@ -1,5 +1,7 @@ #!/bin/bash -# verify perft numbers (positions from www.chessprogramming.org/Perft_Results) +# verify perft numbers (positions from https://www.chessprogramming.org/Perft_Results) + +TESTS_FAILED=0 error() { @@ -10,23 +12,82 @@ trap 'error ${LINENO}' ERR echo "perft testing started" -cat << EOF > perft.exp - set timeout 10 - lassign \$argv pos depth result - spawn ./stockfish - send "position \$pos\\ngo perft \$depth\\n" - expect "Nodes searched? \$result" {} timeout {exit 1} - send "quit\\n" - expect eof +EXPECT_SCRIPT=$(mktemp) + +cat << 'EOF' > $EXPECT_SCRIPT +#!/usr/bin/expect -f +set timeout 120 +lassign [lrange $argv 0 4] pos depth result chess960 logfile +log_file -noappend $logfile +spawn ./stockfish +if {$chess960 == "true"} { + send "setoption name UCI_Chess960 value true\n" +} +send "position $pos\ngo perft $depth\n" +expect { + "Nodes searched: $result" {} + timeout {puts "TIMEOUT: Expected $result nodes"; exit 1} + eof {puts "EOF: Stockfish crashed"; exit 2} +} +send "quit\n" +expect eof EOF -expect perft.exp startpos 5 4865609 > /dev/null -expect perft.exp "fen r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -" 5 193690690 > /dev/null -expect perft.exp "fen 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -" 6 11030083 > /dev/null -expect perft.exp "fen r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" 5 15833292 > /dev/null -expect perft.exp "fen rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8" 5 89941194 > /dev/null -expect perft.exp "fen r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10" 5 164075551 > /dev/null +chmod +x $EXPECT_SCRIPT -rm perft.exp +run_test() { + local pos="$1" + local depth="$2" + local expected="$3" + local chess960="$4" + local tmp_file=$(mktemp) -echo "perft testing OK" + echo -n "Testing depth $depth: ${pos:0:40}... " + + if $EXPECT_SCRIPT "$pos" "$depth" "$expected" "$chess960" "$tmp_file" > /dev/null 2>&1; then + echo "OK" + rm -f "$tmp_file" + else + local exit_code=$? + echo "FAILED (exit code: $exit_code)" + echo "===== Output for failed test =====" + cat "$tmp_file" + echo "==================================" + rm -f "$tmp_file" + TESTS_FAILED=1 + fi +} + +# standard positions + +run_test "startpos" 7 3195901860 "false" +run_test "fen r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -" 5 193690690 "false" +run_test "fen 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -" 7 178633661 "false" +run_test "fen r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" 6 706045033 "false" +run_test "fen rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8" 5 89941194 "false" +run_test "fen r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10" 5 164075551 "false" +run_test "fen r7/4p3/5p1q/3P4/4pQ2/4pP2/6pp/R3K1kr w Q - 1 3" 5 11609488 "false" + +# chess960 positions + +run_test "fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w AHah - 0 1" 6 119060324 "true" +run_test "fen 1rqbkrbn/1ppppp1p/1n6/p1N3p1/8/2P4P/PP1PPPP1/1RQBKRBN w FBfb - 0 9" 6 191762235 "true" +run_test "fen rbbqn1kr/pp2p1pp/6n1/2pp1p2/2P4P/P7/BP1PPPP1/R1BQNNKR w HAha - 0 9" 6 924181432 "true" +run_test "fen rqbbknr1/1ppp2pp/p5n1/4pp2/P7/1PP5/1Q1PPPPP/R1BBKNRN w GAga - 0 9" 6 308553169 "true" +run_test "fen 4rrb1/1kp3b1/1p1p4/pP1Pn2p/5p2/1PR2P2/2P1NB1P/2KR1B2 w D - 0 21" 6 872323796 "true" +run_test "fen 1rkr3b/1ppn3p/3pB1n1/6q1/R2P4/4N1P1/1P5P/2KRQ1B1 b Dbd - 0 14" 6 2678022813 "true" +run_test "fen qbbnrkr1/p1pppppp/1p4n1/8/2P5/6N1/PPNPPPPP/1BRKBRQ1 b FCge - 1 3" 6 521301336 "true" +run_test "fen rr6/2kpp3/1ppn2p1/p2b1q1p/P4P1P/1PNN2P1/2PP4/1K2R2R b E - 1 20" 2 1438 "true" +run_test "fen rr6/2kpp3/1ppn2p1/p2b1q1p/P4P1P/1PNN2P1/2PP4/1K2RR2 w E - 0 20" 3 37340 "true" +run_test "fen rr6/2kpp3/1ppnb1p1/p2Q1q1p/P4P1P/1PNN2P1/2PP4/1K2RR2 b E - 2 19" 4 2237725 "true" +run_test "fen rr6/2kpp3/1ppnb1p1/p4q1p/P4P1P/1PNN2P1/2PP2Q1/1K2RR2 w E - 1 19" 4 2098209 "true" +run_test "fen rr6/2kpp3/1ppnb1p1/p4q1p/P4P1P/1PNN2P1/2PP2Q1/1K2RR2 w E - 1 19" 5 79014522 "true" +run_test "fen rr6/2kpp3/1ppnb1p1/p4q1p/P4P1P/1PNN2P1/2PP2Q1/1K2RR2 w E - 1 19" 6 2998685421 "true" + +rm -f $EXPECT_SCRIPT +echo "perft testing completed" + +if [ $TESTS_FAILED -ne 0 ]; then + echo "Some tests failed" + exit 1 +fi diff --git a/tests/signature.sh b/tests/signature.sh index 06bd1892e..ef781a0f4 100755 --- a/tests/signature.sh +++ b/tests/signature.sh @@ -2,16 +2,26 @@ # obtain and optionally verify Bench / signature # if no reference is given, the output is deliberately limited to just the signature +STDOUT_FILE=$(mktemp) +STDERR_FILE=$(mktemp) + error() { echo "running bench for signature failed on line $1" + echo "===== STDOUT =====" + cat "$STDOUT_FILE" + echo "===== STDERR =====" + cat "$STDERR_FILE" + rm -f "$STDOUT_FILE" "$STDERR_FILE" exit 1 } trap 'error ${LINENO}' ERR # obtain +eval "$RUN_PREFIX ./stockfish bench" > "$STDOUT_FILE" 2> "$STDERR_FILE" || error ${LINENO} +signature=$(grep "Nodes searched : " "$STDERR_FILE" | awk '{print $4}') -signature=`eval "$WINE_PATH ./stockfish bench 2>&1" | grep "Nodes searched : " | awk '{print $4}'` +rm -f "$STDOUT_FILE" "$STDERR_FILE" if [ $# -gt 0 ]; then # compare to given reference @@ -28,4 +38,4 @@ if [ $# -gt 0 ]; then else # just report signature echo $signature -fi +fi \ No newline at end of file diff --git a/tests/testing.py b/tests/testing.py new file mode 100644 index 000000000..3a4b537e9 --- /dev/null +++ b/tests/testing.py @@ -0,0 +1,400 @@ +import subprocess +from typing import List +import os +import collections +import time +import sys +import traceback +import fnmatch +from functools import wraps +from contextlib import redirect_stdout +import io +import tarfile +import pathlib +import concurrent.futures +import tempfile +import shutil +import requests + +CYAN_COLOR = "\033[36m" +GRAY_COLOR = "\033[2m" +RED_COLOR = "\033[31m" +GREEN_COLOR = "\033[32m" +RESET_COLOR = "\033[0m" +WHITE_BOLD = "\033[1m" + +MAX_TIMEOUT = 60 * 5 + +PATH = pathlib.Path(__file__).parent.resolve() + + +class Valgrind: + @staticmethod + def get_valgrind_command(): + return [ + "valgrind", + "--error-exitcode=42", + "--errors-for-leak-kinds=all", + "--leak-check=full", + ] + + @staticmethod + def get_valgrind_thread_command(): + return ["valgrind", "--error-exitcode=42", "--fair-sched=try"] + + +class TSAN: + @staticmethod + def set_tsan_option(): + with open(f"tsan.supp", "w") as f: + f.write( + """ +race:Stockfish::TTEntry::read +race:Stockfish::TTEntry::save +race:Stockfish::TranspositionTable::probe +race:Stockfish::TranspositionTable::hashfull +""" + ) + + os.environ["TSAN_OPTIONS"] = "suppressions=./tsan.supp" + + @staticmethod + def unset_tsan_option(): + os.environ.pop("TSAN_OPTIONS", None) + os.remove(f"tsan.supp") + + +class EPD: + @staticmethod + def create_bench_epd(): + with open(f"{os.path.join(PATH,'bench_tmp.epd')}", "w") as f: + f.write( + """ +Rn6/1rbq1bk1/2p2n1p/2Bp1p2/3Pp1pP/1N2P1P1/2Q1NPB1/6K1 w - - 2 26 +rnbqkb1r/ppp1pp2/5n1p/3p2p1/P2PP3/5P2/1PP3PP/RNBQKBNR w KQkq - 0 3 +3qnrk1/4bp1p/1p2p1pP/p2bN3/1P1P1B2/P2BQ3/5PP1/4R1K1 w - - 9 28 +r4rk1/1b2ppbp/pq4pn/2pp1PB1/1p2P3/1P1P1NN1/1PP3PP/R2Q1RK1 w - - 0 13 +""" + ) + + @staticmethod + def delete_bench_epd(): + os.remove(f"{os.path.join(PATH,'bench_tmp.epd')}") + + +class Syzygy: + @staticmethod + def get_syzygy_path(): + return os.path.abspath("syzygy") + + @staticmethod + def download_syzygy(): + if not os.path.isdir(os.path.join(PATH, "syzygy")): + url = "https://api.github.com/repos/niklasf/python-chess/tarball/9b9aa13f9f36d08aadfabff872882f4ab1494e95" + file = "niklasf-python-chess-9b9aa13" + + with tempfile.TemporaryDirectory() as tmpdirname: + tarball_path = os.path.join(tmpdirname, f"{file}.tar.gz") + + response = requests.get(url, stream=True) + with open(tarball_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + with tarfile.open(tarball_path, "r:gz") as tar: + tar.extractall(tmpdirname) + + shutil.move( + os.path.join(tmpdirname, file), os.path.join(PATH, "syzygy") + ) + + +class OrderedClassMembers(type): + @classmethod + def __prepare__(self, name, bases): + return collections.OrderedDict() + + def __new__(self, name, bases, classdict): + classdict["__ordered__"] = [ + key for key in classdict.keys() if key not in ("__module__", "__qualname__") + ] + return type.__new__(self, name, bases, classdict) + + +class TimeoutException(Exception): + def __init__(self, message: str, timeout: int): + self.message = message + self.timeout = timeout + + +def timeout_decorator(timeout: float): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(func, *args, **kwargs) + try: + result = future.result(timeout=timeout) + except concurrent.futures.TimeoutError: + raise TimeoutException( + f"Function {func.__name__} timed out after {timeout} seconds", + timeout, + ) + return result + + return wrapper + + return decorator + + +class MiniTestFramework: + def __init__(self): + self.passed_test_suites = 0 + self.failed_test_suites = 0 + self.passed_tests = 0 + self.failed_tests = 0 + self.stop_on_failure = True + + def has_failed(self) -> bool: + return self.failed_test_suites > 0 + + def run(self, classes: List[type]) -> bool: + self.start_time = time.time() + + for test_class in classes: + with tempfile.TemporaryDirectory() as tmpdirname: + original_cwd = os.getcwd() + os.chdir(tmpdirname) + + try: + if self.__run(test_class): + self.failed_test_suites += 1 + else: + self.passed_test_suites += 1 + except Exception as e: + self.failed_test_suites += 1 + print(f"\n{RED_COLOR}Error: {e}{RESET_COLOR}") + finally: + os.chdir(original_cwd) + + self.__print_summary(round(time.time() - self.start_time, 2)) + return self.has_failed() + + def __run(self, test_class) -> bool: + test_instance = test_class() + test_name = test_instance.__class__.__name__ + test_methods = [m for m in test_instance.__ordered__ if m.startswith("test_")] + + print(f"\nTest Suite: {test_name}") + + if hasattr(test_instance, "beforeAll"): + test_instance.beforeAll() + + fails = 0 + + for method in test_methods: + fails += self.__run_test_method(test_instance, method) + + if hasattr(test_instance, "afterAll"): + test_instance.afterAll() + + self.failed_tests += fails + + return fails > 0 + + def __run_test_method(self, test_instance, method: str) -> int: + print(f" Running {method}... \r", end="", flush=True) + + buffer = io.StringIO() + fails = 0 + + try: + t0 = time.time() + + with redirect_stdout(buffer): + if hasattr(test_instance, "beforeEach"): + test_instance.beforeEach() + + getattr(test_instance, method)() + + if hasattr(test_instance, "afterEach"): + test_instance.afterEach() + + duration = time.time() - t0 + + self.print_success(f" {method} ({duration * 1000:.2f}ms)") + self.passed_tests += 1 + except Exception as e: + if isinstance(e, TimeoutException): + self.print_failure( + f" {method} (hit execution limit of {e.timeout} seconds)" + ) + + if isinstance(e, AssertionError): + self.__handle_assertion_error(t0, method) + + if self.stop_on_failure: + self.__print_buffer_output(buffer) + raise e + + fails += 1 + finally: + self.__print_buffer_output(buffer) + + return fails + + def __handle_assertion_error(self, start_time, method: str): + duration = time.time() - start_time + self.print_failure(f" {method} ({duration * 1000:.2f}ms)") + traceback_output = "".join(traceback.format_tb(sys.exc_info()[2])) + + colored_traceback = "\n".join( + f" {CYAN_COLOR}{line}{RESET_COLOR}" + for line in traceback_output.splitlines() + ) + + print(colored_traceback) + + def __print_buffer_output(self, buffer: io.StringIO): + output = buffer.getvalue() + if output: + indented_output = "\n".join(f" {line}" for line in output.splitlines()) + print(f" {RED_COLOR}⎯⎯⎯⎯⎯OUTPUT⎯⎯⎯⎯⎯{RESET_COLOR}") + print(f"{GRAY_COLOR}{indented_output}{RESET_COLOR}") + print(f" {RED_COLOR}⎯⎯⎯⎯⎯OUTPUT⎯⎯⎯⎯⎯{RESET_COLOR}") + + def __print_summary(self, duration: float): + print(f"\n{WHITE_BOLD}Test Summary{RESET_COLOR}\n") + print( + f" Test Suites: {GREEN_COLOR}{self.passed_test_suites} passed{RESET_COLOR}, {RED_COLOR}{self.failed_test_suites} failed{RESET_COLOR}, {self.passed_test_suites + self.failed_test_suites} total" + ) + print( + f" Tests: {GREEN_COLOR}{self.passed_tests} passed{RESET_COLOR}, {RED_COLOR}{self.failed_tests} failed{RESET_COLOR}, {self.passed_tests + self.failed_tests} total" + ) + print(f" Time: {duration}s\n") + + def print_failure(self, add: str): + print(f" {RED_COLOR}✗{RESET_COLOR}{add}", flush=True) + + def print_success(self, add: str): + print(f" {GREEN_COLOR}✓{RESET_COLOR}{add}", flush=True) + + +class Stockfish: + def __init__( + self, + prefix: List[str], + path: str, + args: List[str] = [], + cli: bool = False, + ): + self.path = path + self.process = None + self.args = args + self.cli = cli + self.prefix = prefix + self.output = [] + + self.start() + + def _check_process_alive(self): + if not self.process or self.process.poll() is not None: + print("\n".join(self.output)) + raise RuntimeError("Stockfish process has terminated") + + def start(self): + if self.cli: + self.process = subprocess.run( + self.prefix + [self.path] + self.args, + capture_output=True, + text=True, + ) + + if self.process.returncode != 0: + print(self.process.stdout) + print(self.process.stderr) + print(f"Process failed with return code {self.process.returncode}") + + return + + self.process = subprocess.Popen( + self.prefix + [self.path] + self.args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1, + ) + + def setoption(self, name: str, value: str): + self.send_command(f"setoption name {name} value {value}") + + def send_command(self, command: str): + if not self.process: + raise RuntimeError("Stockfish process is not started") + + self._check_process_alive() + + self.process.stdin.write(command + "\n") + self.process.stdin.flush() + + @timeout_decorator(MAX_TIMEOUT) + def equals(self, expected_output: str): + for line in self.readline(): + if line == expected_output: + return + + @timeout_decorator(MAX_TIMEOUT) + def expect(self, expected_output: str): + for line in self.readline(): + if fnmatch.fnmatch(line, expected_output): + return + + @timeout_decorator(MAX_TIMEOUT) + def contains(self, expected_output: str): + for line in self.readline(): + if expected_output in line: + return + + @timeout_decorator(MAX_TIMEOUT) + def starts_with(self, expected_output: str): + for line in self.readline(): + if line.startswith(expected_output): + return + + @timeout_decorator(MAX_TIMEOUT) + def check_output(self, callback): + if not callback: + raise ValueError("Callback function is required") + + for line in self.readline(): + if callback(line) == True: + return + + def readline(self): + if not self.process: + raise RuntimeError("Stockfish process is not started") + + while True: + self._check_process_alive() + line = self.process.stdout.readline().strip() + self.output.append(line) + + yield line + + def clear_output(self): + self.output = [] + + def get_output(self) -> List[str]: + return self.output + + def quit(self): + self.send_command("quit") + + def close(self): + if self.process: + self.process.stdin.close() + self.process.stdout.close() + return self.process.wait() + + return 0