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/matrix.json b/.github/ci/matrix.json index 44e0596ea..c414c51fc 100644 --- a/.github/ci/matrix.json +++ b/.github/ci/matrix.json @@ -40,6 +40,18 @@ "ext": ".exe", "sde": "/d/a/Stockfish/Stockfish/.output/sde-temp-files/sde-external-9.27.0-2023-09-13-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" } ], "binaries": [ @@ -51,7 +63,9 @@ "x86-64-avx512", "x86-64-vnni256", "x86-64-vnni512", - "apple-silicon" + "apple-silicon", + "armv8", + "armv8-dotprod" ], "exclude": [ { @@ -84,12 +98,6 @@ "os": "macos-14" } }, - { - "binaries": "x86-64-avxvnni", - "config": { - "os": "macos-14" - } - }, { "binaries": "x86-64-avx512", "config": { @@ -108,12 +116,6 @@ "os": "macos-14" } }, - { - "binaries": "x86-64-avxvnni", - "config": { - "ubuntu-22.04": null - } - }, { "binaries": "x86-64-avxvnni", "config": { @@ -138,6 +140,54 @@ "os": "macos-13" } }, + { + "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-vnni256", + "config": { + "os": "windows-11-arm" + } + }, + { + "binaries": "x86-64-vnni512", + "config": { + "os": "windows-11-arm" + } + }, { "binaries": "apple-silicon", "config": { @@ -147,7 +197,13 @@ { "binaries": "apple-silicon", "config": { - "os": "macos-13" + "os": "windows-11-arm" + } + }, + { + "binaries": "apple-silicon", + "config": { + "os": "ubuntu-20.04" } }, { @@ -155,6 +211,72 @@ "config": { "os": "ubuntu-22.04" } + }, + { + "binaries": "apple-silicon", + "config": { + "os": "macos-13" + } + }, + { + "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-13" + } + }, + { + "binaries": "armv8", + "config": { + "os": "macos-14" + } + }, + { + "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-13" + } + }, + { + "binaries": "armv8-dotprod", + "config": { + "os": "macos-14" + } } ] } diff --git a/.github/workflows/arm_compilation.yml b/.github/workflows/arm_compilation.yml index 5bf2a93e5..781bd8070 100644 --- a/.github/workflows/arm_compilation.yml +++ b/.github/workflows/arm_compilation.yml @@ -38,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 diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index ab6b4350e..46607c1e9 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -25,11 +25,11 @@ jobs: 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: "18" + clang-format-version: "20" exclude-regex: "incbin" - name: Comment on PR @@ -37,9 +37,9 @@ jobs: uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # @v2.5.0 with: message: | - clang-format 18 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/noble/clang-format-18. + 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 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d01ed41fe..a338083cf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,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 5878adecb..67b2e1c55 100644 --- a/.github/workflows/compilation.yml +++ b/.github/workflows/compilation.yml @@ -63,13 +63,13 @@ jobs: - name: Check compiler 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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 57d0d53f0..95ca12092 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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++ @@ -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 @@ -118,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 @@ -302,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 @@ -311,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 diff --git a/AUTHORS b/AUTHORS index 54c5f8a21..b43a9e5b6 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) @@ -34,6 +35,7 @@ Artem Solopiy (EntityFX) Auguste Pop Balazs Szilagyi Balint Pfliegel +Baptiste Rech (breatn) Ben Chaney (Chaneybenjamini) Ben Koshy (BKSpurgeon) Bill Henry (VoyagerOne) @@ -131,6 +133,7 @@ Kenneth Lee (kennethlee33) Kian E (KJE-98) kinderchocolate Kiran Panditrao (Krgp) +Kirill Zaripov (kokodio) Kojirion Krisztián Peőcz Krystian Kuzniarek (kuzkry) @@ -147,6 +150,7 @@ Lucas Braesch (lucasart) Lyudmil Antonov (lantonov) Maciej Żenczykowski (zenczykowski) Malcolm Campbell (xoto10) +Mark Marosi (Mapika) Mark Tenzer (31m059) marotear Mathias Parnaudeau (mparnaudeau) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b6fbce0d..07fe9f542 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 18 to be installed on your system. +`make format`. This requires clang-format version 20 to be installed on your system. ## Navigate diff --git a/scripts/get_native_properties.sh b/scripts/get_native_properties.sh index 132bd6f48..e8c8f23f2 100755 --- a/scripts/get_native_properties.sh +++ b/scripts/get_native_properties.sh @@ -39,7 +39,9 @@ set_arch_loongarch64() { # Set the file CPU x86_64 architecture set_arch_x86_64() { - if check_flags 'avx512vnni' 'avx512dq' 'avx512f' 'avx512bw' 'avx512vl'; then + 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-vnni256' elif check_flags 'avx512f' 'avx512bw'; then true_arch='x86-64-avx512' @@ -130,7 +132,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/src/Makefile b/src/Makefile index 827798ff8..f61a0f9a8 100644 --- a/src/Makefile +++ b/src/Makefile @@ -60,9 +60,9 @@ SRCS = benchmark.cpp bitboard.cpp evaluate.cpp main.cpp \ 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/layers/affine_transform.h \ - nnue/layers/affine_transform_sparse_input.h nnue/layers/clipped_relu.h nnue/layers/simd.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 position.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 @@ -100,6 +100,7 @@ VPATH = syzygy:nnue:nnue/features # 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 @@ -127,10 +128,10 @@ 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-avx512icl 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-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)) @@ -156,6 +157,7 @@ avxvnni = no avx512 = no vnni256 = no vnni512 = no +avx512icl = no altivec = no vsx = no neon = no @@ -166,8 +168,8 @@ lsx = no lasx = no STRIP = strip -ifneq ($(shell which clang-format-18 2> /dev/null),) - CLANG-FORMAT = clang-format-18 +ifneq ($(shell which clang-format-20 2> /dev/null),) + CLANG-FORMAT = clang-format-20 else CLANG-FORMAT = clang-format endif @@ -293,6 +295,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 @@ -536,14 +551,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 @@ -552,7 +565,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 @@ -560,14 +573,28 @@ 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 +ifeq ($(wildcard $(LLVM_PROFDATA)),) + LLVM_PROFDATA := llvm-profdata endif ifeq ($(comp),icx) @@ -584,11 +611,6 @@ else endif 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) @@ -697,7 +719,7 @@ endif ifeq ($(avx512),yes) CXXFLAGS += -DUSE_AVX512 ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) - CXXFLAGS += -mavx512f -mavx512bw + CXXFLAGS += -mavx512f -mavx512bw -mavx512dq -mavx512vl endif endif @@ -715,6 +737,13 @@ ifeq ($(vnni512),yes) 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 + ifeq ($(sse41),yes) CXXFLAGS += -DUSE_SSE41 ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) @@ -882,6 +911,7 @@ help: 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 Lane or AMD Zen 4" && \ 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" && \ @@ -1001,10 +1031,6 @@ net: format: $(CLANG-FORMAT) -i $(SRCS) $(HEADERS) -style=file -# default target -default: - help - ### ========================================================================== ### Section 5. Private Targets ### ========================================================================== @@ -1034,6 +1060,7 @@ config-sanity: net echo "avx512: '$(avx512)'" && \ echo "vnni256: '$(vnni256)'" && \ echo "vnni512: '$(vnni512)'" && \ + echo "avx512icl: '$(avx512icl)'" && \ echo "altivec: '$(altivec)'" && \ echo "vsx: '$(vsx)'" && \ echo "neon: '$(neon)'" && \ @@ -1071,6 +1098,7 @@ config-sanity: net (test "$(avx512)" = "yes" || test "$(avx512)" = "no") && \ (test "$(vnni256)" = "yes" || test "$(vnni256)" = "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") && \ @@ -1094,7 +1122,7 @@ clang-profile-make: 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-use=stockfish.profdata' \ EXTRALDFLAGS='-fprofile-use ' \ @@ -1131,6 +1159,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/evaluate.cpp b/src/evaluate.cpp index 92b03af37..23f4b8c2b 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -101,8 +101,6 @@ std::string Eval::trace(Position& pos, const Eval::NNUE::Networks& networks) { Eval::NNUE::AccumulatorStack accumulators; auto caches = std::make_unique(networks); - accumulators.reset(pos, networks, *caches); - std::stringstream ss; ss << std::showpoint << std::noshowpos << std::fixed << std::setprecision(2); ss << '\n' << NNUE::trace(pos, networks, *caches) << '\n'; diff --git a/src/history.h b/src/history.h index f2ef7661f..46914789e 100644 --- a/src/history.h +++ b/src/history.h @@ -36,7 +36,7 @@ namespace Stockfish { constexpr int PAWN_HISTORY_SIZE = 512; // has to be a power of 2 constexpr int CORRECTION_HISTORY_SIZE = 32768; // has to be a power of 2 constexpr int CORRECTION_HISTORY_LIMIT = 1024; -constexpr int LOW_PLY_HISTORY_SIZE = 4; +constexpr int LOW_PLY_HISTORY_SIZE = 5; static_assert((PAWN_HISTORY_SIZE & (PAWN_HISTORY_SIZE - 1)) == 0, "PAWN_HISTORY_SIZE has to be a power of 2"); @@ -166,7 +166,7 @@ struct CorrHistTypedef { template using CorrectionHistory = typename Detail::CorrHistTypedef::type; -using TTMoveHistory = Stats; +using TTMoveHistory = StatsEntry; } // namespace Stockfish diff --git a/src/memory.h b/src/memory.h index 4dc232878..e4a59fec5 100644 --- a/src/memory.h +++ b/src/memory.h @@ -52,7 +52,6 @@ void memory_deleter(T* ptr, FREE_FUNC free_func) { ptr->~T(); free_func(ptr); - return; } // Frees memory which was placed there with placement new. diff --git a/src/misc.cpp b/src/misc.cpp index f85356c59..3bdde000f 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -237,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 diff --git a/src/misc.h b/src/misc.h index 84f11d6de..756433290 100644 --- a/src/misc.h +++ b/src/misc.h @@ -317,6 +317,22 @@ void move_to_front(std::vector& vec, Predicate pred) { } } +#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 #endif // #ifndef MISC_H_INCLUDED diff --git a/src/movegen.cpp b/src/movegen.cpp index a73bd8501..10adc70be 100644 --- a/src/movegen.cpp +++ b/src/movegen.cpp @@ -24,12 +24,88 @@ #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) { + _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 +124,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,17 +151,8 @@ ExtMove* generate_pawn_moves(const Position& pos, ExtMove* moveList, Bitboard ta b2 &= target; } - 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 @@ -114,17 +181,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) { @@ -148,7 +206,7 @@ ExtMove* generate_pawn_moves(const Position& pos, ExtMove* moveList, Bitboard ta template -ExtMove* generate_moves(const Position& pos, ExtMove* moveList, Bitboard target) { +Move* generate_moves(const Position& pos, Move* moveList, Bitboard target) { static_assert(Pt != KING && Pt != PAWN, "Unsupported piece type in generate_moves()"); @@ -159,8 +217,7 @@ ExtMove* generate_moves(const Position& pos, ExtMove* moveList, Bitboard target) Square from = pop_lsb(bb); Bitboard b = attacks_bb(from, pos.pieces()) & target; - while (b) - *moveList++ = Move(from, pop_lsb(b)); + moveList = splat_moves(moveList, from, b); } return moveList; @@ -168,7 +225,7 @@ 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()"); @@ -192,8 +249,7 @@ ExtMove* generate_all(const Position& pos, ExtMove* moveList) { 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}) @@ -213,7 +269,7 @@ ExtMove* generate_all(const Position& pos, ExtMove* moveList) { // // 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())); @@ -225,21 +281,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 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 7c6cceb7c..287fd8927 100644 --- a/src/movegen.h +++ b/src/movegen.h @@ -49,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 @@ -59,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 cc6d47901..79b6f55a2 100644 --- a/src/movepick.cpp +++ b/src/movepick.cpp @@ -19,8 +19,8 @@ #include "movepick.h" #include -#include #include +#include #include "bitboard.h" #include "misc.h" @@ -56,6 +56,7 @@ enum Stages { QCAPTURE }; + // 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) { @@ -122,42 +123,41 @@ MovePicker::MovePicker(const Position& p, Move ttm, int th, const CapturePieceTo // 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[QUEEN + 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[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]; } - 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]) + 1024 * bool(pos.check_squares(pt) & to); 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 * (*mainHistory)[us][m.from_to()]; m.value += 2 * (*pawnHistory)[pawn_structure_index(pos)][pc][to]; m.value += (*continuationHistory[0])[pc][to]; m.value += (*continuationHistory[1])[pc][to]; @@ -166,33 +166,34 @@ void MovePicker::score() { 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) ? 51700 - : pt == ROOK && !(to & threatenedByMinor) ? 25600 - : !(to & threatenedByPawn) ? 14450 - : 0) - : 0; - - // malus for putting piece en prise - m.value -= (pt == QUEEN && bool(to & threatenedByRook) ? 49000 - : pt == ROOK && bool(to & threatenedByMinor) ? 24335 - : 0); + // penalty for moving to a square threatened by a lesser piece + // or bonus for escaping an attack by a lesser piece. + if (KNIGHT <= pt && pt <= QUEEN) + { + static constexpr int bonus[QUEEN + 1] = {0, 0, 144, 144, 256, 517}; + int v = threatByLesser[pt] & to ? -95 : 100 * bool(threatByLesser[pt] & from); + m.value += bonus[pt] * v; + } if (ply < LOW_PLY_HISTORY_SIZE) - m.value += 8 * (*lowPlyHistory)[ply][m.from_to()] / (1 + 2 * ply); + m.value += 8 * (*lowPlyHistory)[ply][m.from_to()] / (1 + ply); } else // Type == EVASIONS { if (pos.capture_stage(m)) - m.value = PieceValue[pos.piece_on(m.to_sq())] + (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.from_to()] + (*continuationHistory[0])[pc][to]; + if (ply < LOW_PLY_HISTORY_SIZE) + m.value += 2 * (*lowPlyHistory)[ply][m.from_to()] / (1 + ply); + } } + } + return it; } // Returns the next move satisfying a predicate function. @@ -200,7 +201,7 @@ void MovePicker::score() { template Move MovePicker::select(Pred filter) { - for (; cur < endMoves; ++cur) + for (; cur < endCur; ++cur) if (*cur != ttMove && filter()) return *cur++; @@ -212,8 +213,7 @@ Move MovePicker::select(Pred filter) { // picking the move with the highest score from a list of generated moves. Move MovePicker::next_move() { - auto quiet_threshold = [](Depth d) { return -3560 * d; }; - + constexpr int goodQuietThreshold = -14000; top: switch (stage) { @@ -227,20 +227,23 @@ 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 (pos.see_ge(*cur, -cur->value / 18)) + return true; + std::swap(*endBadCaptures++, *cur); + return false; })) return *(cur - 1); @@ -250,29 +253,23 @@ top: 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 true; })) - { - if ((cur - 1)->value > -7998 || (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]]; @@ -281,27 +278,29 @@ top: 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 true; }); + 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(); - partial_insertion_sort(cur, endMoves, std::numeric_limits::min()); + cur = moves; + endCur = endGenerated = score(ml); + + partial_insertion_sort(cur, endCur, std::numeric_limits::min()); ++stage; [[fallthrough]]; + } case EVASION : case QCAPTURE : @@ -317,29 +316,17 @@ top: void MovePicker::skip_quiet_moves() { skipQuiets = true; } -bool MovePicker::otherPieceTypesMobile(PieceType pt, ValueList& capturesSearched) { - if (stage != GOOD_QUIET && stage != BAD_QUIET) - return true; +// this function must be called after all quiet moves and captures have been generated +bool MovePicker::can_move_king_or_pawn() const { + // SEE negative captures shouldn't be returned in GOOD_CAPTURE stage + assert(stage > GOOD_CAPTURE && stage != EVASION_INIT); - // verify good captures - for (std::size_t i = 0; i < capturesSearched.size(); i++) - if (type_of(pos.moved_piece(capturesSearched[i])) != pt) - { - if (type_of(pos.moved_piece(capturesSearched[i])) != KING) - return true; - if (pos.legal(capturesSearched[i])) - return true; - } - - // now verify bad captures and quiets - for (ExtMove* c = moves; c < endBadQuiets; ++c) - if (type_of(pos.moved_piece(*c)) != pt) - { - if (type_of(pos.moved_piece(*c)) != KING) - return true; - if (pos.legal(*c)) - return true; - } + for (const ExtMove* m = moves; m < endGenerated; ++m) + { + PieceType movedPieceType = type_of(pos.moved_piece(*m)); + if ((movedPieceType == PAWN || movedPieceType == KING) && pos.legal(*m)) + return true; + } return false; } diff --git a/src/movepick.h b/src/movepick.h index dfafe69a5..9d6c02b0e 100644 --- a/src/movepick.h +++ b/src/movepick.h @@ -19,8 +19,6 @@ #ifndef MOVEPICK_H_INCLUDED #define MOVEPICK_H_INCLUDED -#include - #include "history.h" #include "movegen.h" #include "types.h" @@ -29,9 +27,6 @@ namespace Stockfish { class Position; -template -class ValueList; - // 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 @@ -55,15 +50,15 @@ class MovePicker { MovePicker(const Position&, Move, int, const CapturePieceToHistory*); Move next_move(); void skip_quiet_moves(); - bool otherPieceTypesMobile(PieceType pt, ValueList& capturesSearched); + bool can_move_king_or_pawn() const; private: 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; @@ -72,7 +67,7 @@ class MovePicker { const PieceToHistory** continuationHistory; const PawnHistory* pawnHistory; Move ttMove; - ExtMove * cur, *endMoves, *endBadCaptures, *beginBadQuiets, *endBadQuiets; + ExtMove * cur, *endCur, *endBadCaptures, *endCaptures, *endGenerated; int stage; int threshold; Depth depth; diff --git a/src/nnue/features/half_ka_v2_hm.cpp b/src/nnue/features/half_ka_v2_hm.cpp index eb3c7e6a7..70e9beeb1 100644 --- a/src/nnue/features/half_ka_v2_hm.cpp +++ b/src/nnue/features/half_ka_v2_hm.cpp @@ -23,7 +23,7 @@ #include "../../bitboard.h" #include "../../position.h" #include "../../types.h" -#include "../nnue_accumulator.h" +#include "../nnue_common.h" namespace Stockfish::Eval::NNUE::Features { @@ -58,13 +58,15 @@ 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)); - } + removed.push_back(make_index(dp.from, dp.pc, ksq)); + if (dp.to != SQ_NONE) + added.push_back(make_index(dp.to, dp.pc, ksq)); + + if (dp.remove_sq != SQ_NONE) + removed.push_back(make_index(dp.remove_sq, dp.remove_pc, ksq)); + + if (dp.add_sq != SQ_NONE) + added.push_back(make_index(dp.add_sq, dp.add_pc, ksq)); } // Explicit template instantiations @@ -78,7 +80,7 @@ template void HalfKAv2_hm::append_changed_indices(Square ksq, IndexList& added); bool HalfKAv2_hm::requires_refresh(const DirtyPiece& dirtyPiece, Color perspective) { - return dirtyPiece.piece[0] == make_piece(perspective, KING); + return dirtyPiece.pc == make_piece(perspective, KING); } } // namespace Stockfish::Eval::NNUE::Features diff --git a/src/nnue/layers/affine_transform.h b/src/nnue/layers/affine_transform.h index 3920efb17..b4440c1e3 100644 --- a/src/nnue/layers/affine_transform.h +++ b/src/nnue/layers/affine_transform.h @@ -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). @@ -102,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] = Simd::neon_m128_reduce_add_epi32(sum); + output[i] = SIMD::neon_m128_reduce_add_epi32(sum); #endif } @@ -191,20 +191,20 @@ class AffineTransform { #if defined(USE_AVX512) using vec_t = __m512i; #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 vec_t = __m256i; #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 vec_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 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), \ + SIMD::dotprod_m128_add_dpbusd_epi32(acc, vreinterpretq_s8_s32(a), \ vreinterpretq_s8_s32(b)) #endif @@ -245,20 +245,20 @@ class AffineTransform { #if defined(USE_AVX2) using vec_t = __m256i; #define vec_setzero() _mm256_setzero_si256() - #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 + #define vec_hadd SIMD::m256_hadd #elif defined(USE_SSSE3) using vec_t = __m128i; #define vec_setzero() _mm_setzero_si128() - #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 + #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), \ + SIMD::dotprod_m128_add_dpbusd_epi32(acc, vreinterpretq_s8_s32(a), \ vreinterpretq_s8_s32(b)) - #define vec_hadd Simd::neon_m128_hadd + #define vec_hadd SIMD::neon_m128_hadd #endif const auto inputVector = reinterpret_cast(input); diff --git a/src/nnue/layers/affine_transform_sparse_input.h b/src/nnue/layers/affine_transform_sparse_input.h index be5e30b5e..e77c98f8c 100644 --- a/src/nnue/layers/affine_transform_sparse_input.h +++ b/src/nnue/layers/affine_transform_sparse_input.h @@ -22,14 +22,12 @@ #define NNUE_LAYERS_AFFINE_TRANSFORM_SPARSE_INPUT_H_INCLUDED #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. @@ -51,11 +49,7 @@ constexpr int constexpr_lsb(uint64_t bb) { alignas(CacheLineSize) static constexpr struct OffsetIndices { - #if (USE_SSE41) - std::uint8_t offset_indices[256][8]; - #else std::uint16_t offset_indices[256][8]; - #endif constexpr OffsetIndices() : offset_indices() { @@ -74,56 +68,52 @@ alignas(CacheLineSize) static constexpr struct OffsetIndices { } 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) - #if (USE_SSE41) - #define vec128_load(a) _mm_cvtepu8_epi16(_mm_loadl_epi64(a)) - #else - #define vec128_load(a) _mm_load_si128(a) - #endif - #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); +void find_nnz(const std::int32_t* RESTRICT input, + std::uint16_t* RESTRICT out, + IndexType& count_out) { + + #ifdef 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); // 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; - const auto inputVector = reinterpret_cast(input); + const auto inputVector = reinterpret_cast(input); IndexType count = 0; vec128_t base = vec128_zero; const vec128_t increment = vec128_set_16(8); @@ -133,7 +123,7 @@ 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) @@ -147,13 +137,9 @@ void find_nnz(const std::int32_t* input, std::uint16_t* out, IndexType& count_ou } } 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 @@ -232,27 +218,27 @@ class AffineTransformSparseInput { using invec_t = __m512i; using outvec_t = __m512i; #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_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); diff --git a/src/nnue/layers/simd.h b/src/nnue/layers/simd.h deleted file mode 100644 index 70ca68a0c..000000000 --- a/src/nnue/layers/simd.h +++ /dev/null @@ -1,134 +0,0 @@ -/* - Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2025 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; -} - -[[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/network.cpp b/src/nnue/network.cpp index 7517cef27..46da3ec66 100644 --- a/src/nnue/network.cpp +++ b/src/nnue/network.cpp @@ -213,21 +213,11 @@ NetworkOutput Network::evaluate(const Position& pos, AccumulatorStack& accumulatorStack, AccumulatorCaches::Cache* cache) 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 ASSERT_ALIGNED(transformedFeatures, alignment); @@ -286,20 +276,11 @@ NnueEvalTrace Network::trace_evaluate(const Position& pos, AccumulatorStack& accumulatorStack, AccumulatorCaches::Cache* cache) 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 ASSERT_ALIGNED(transformedFeatures, alignment); diff --git a/src/nnue/network.h b/src/nnue/network.h index cd32c5312..c9358823b 100644 --- a/src/nnue/network.h +++ b/src/nnue/network.h @@ -32,6 +32,7 @@ #include "../types.h" #include "nnue_accumulator.h" #include "nnue_architecture.h" +#include "nnue_common.h" #include "nnue_feature_transformer.h" #include "nnue_misc.h" diff --git a/src/nnue/nnue_accumulator.cpp b/src/nnue/nnue_accumulator.cpp index 2153cd4a6..d13105aa4 100644 --- a/src/nnue/nnue_accumulator.cpp +++ b/src/nnue/nnue_accumulator.cpp @@ -19,34 +19,31 @@ #include "nnue_accumulator.h" #include +#include #include -#include #include #include "../bitboard.h" #include "../misc.h" #include "../position.h" #include "../types.h" -#include "network.h" #include "nnue_architecture.h" -#include "nnue_feature_transformer.h" +#include "nnue_feature_transformer.h" // IWYU pragma: keep +#include "simd.h" namespace Stockfish::Eval::NNUE { -#if defined(__GNUC__) && !defined(__clang__) - #define sf_assume(cond) \ - do \ - { \ - if (!(cond)) \ - __builtin_unreachable(); \ - } while (0) -#else - // do nothing for other compilers - #define sf_assume(cond) -#endif +using namespace SIMD; namespace { +template +void double_inc_update(const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& middle_state, + AccumulatorState& target_state, + const AccumulatorState& computed); + template void update_accumulator_incremental( const FeatureTransformer& featureTransformer, @@ -68,39 +65,24 @@ void AccumulatorState::reset(const DirtyPiece& dp) noexcept { accumulatorSmall.computed.fill(false); } -const AccumulatorState& AccumulatorStack::latest() const noexcept { - return m_accumulators[m_current_idx - 1]; -} +const AccumulatorState& AccumulatorStack::latest() const noexcept { return accumulators[size - 1]; } -AccumulatorState& AccumulatorStack::mut_latest() noexcept { - return m_accumulators[m_current_idx - 1]; -} +AccumulatorState& AccumulatorStack::mut_latest() noexcept { return accumulators[size - 1]; } -void AccumulatorStack::reset(const Position& rootPos, - const Networks& networks, - AccumulatorCaches& caches) noexcept { - m_current_idx = 1; - - update_accumulator_refresh_cache( - *networks.big.featureTransformer, rootPos, m_accumulators[0], caches.big); - update_accumulator_refresh_cache( - *networks.big.featureTransformer, rootPos, m_accumulators[0], caches.big); - - update_accumulator_refresh_cache( - *networks.small.featureTransformer, rootPos, m_accumulators[0], caches.small); - update_accumulator_refresh_cache( - *networks.small.featureTransformer, rootPos, m_accumulators[0], caches.small); +void AccumulatorStack::reset() noexcept { + accumulators[0].reset({}); + size = 1; } void AccumulatorStack::push(const DirtyPiece& dirtyPiece) noexcept { - assert(m_current_idx + 1 < m_accumulators.size()); - m_accumulators[m_current_idx].reset(dirtyPiece); - m_current_idx++; + assert(size + 1 < accumulators.size()); + accumulators[size].reset(dirtyPiece); + size++; } void AccumulatorStack::pop() noexcept { - assert(m_current_idx > 1); - m_current_idx--; + assert(size > 1); + size--; } template @@ -119,7 +101,7 @@ void AccumulatorStack::evaluate_side(const Position& pos, const auto last_usable_accum = find_last_usable_accumulator(); - if ((m_accumulators[last_usable_accum].template acc()).computed[Perspective]) + if ((accumulators[last_usable_accum].template acc()).computed[Perspective]) forward_update_incremental(pos, featureTransformer, last_usable_accum); else @@ -134,12 +116,12 @@ void AccumulatorStack::evaluate_side(const Position& pos, template std::size_t AccumulatorStack::find_last_usable_accumulator() const noexcept { - for (std::size_t curr_idx = m_current_idx - 1; curr_idx > 0; curr_idx--) + for (std::size_t curr_idx = size - 1; curr_idx > 0; curr_idx--) { - if ((m_accumulators[curr_idx].template acc()).computed[Perspective]) + if ((accumulators[curr_idx].template acc()).computed[Perspective]) return curr_idx; - if (FeatureSet::requires_refresh(m_accumulators[curr_idx].dirtyPiece, Perspective)) + if (FeatureSet::requires_refresh(accumulators[curr_idx].dirtyPiece, Perspective)) return curr_idx; } @@ -152,14 +134,33 @@ void AccumulatorStack::forward_update_incremental( const FeatureTransformer& featureTransformer, const std::size_t begin) noexcept { - assert(begin < m_accumulators.size()); - assert((m_accumulators[begin].acc()).computed[Perspective]); + assert(begin < accumulators.size()); + assert((accumulators[begin].acc()).computed[Perspective]); const Square ksq = pos.square(Perspective); - for (std::size_t next = begin + 1; next < m_current_idx; next++) + for (std::size_t next = begin + 1; next < size; next++) + { + if (next + 1 < size) + { + DirtyPiece& dp1 = accumulators[next].dirtyPiece; + DirtyPiece& dp2 = accumulators[next + 1].dirtyPiece; + + 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(featureTransformer, ksq, accumulators[next], + accumulators[next + 1], accumulators[next - 1]); + dp1.to = dp2.remove_sq = captureSq; + + next++; + continue; + } + } update_accumulator_incremental( - featureTransformer, ksq, m_accumulators[next], m_accumulators[next - 1]); + featureTransformer, ksq, accumulators[next], accumulators[next - 1]); + } assert((latest().acc()).computed[Perspective]); } @@ -170,17 +171,17 @@ void AccumulatorStack::backward_update_incremental( const FeatureTransformer& featureTransformer, const std::size_t end) noexcept { - assert(end < m_accumulators.size()); - assert(end < m_current_idx); + assert(end < accumulators.size()); + assert(end < size); assert((latest().acc()).computed[Perspective]); const Square ksq = pos.square(Perspective); - for (std::size_t next = m_current_idx - 2; next >= end; next--) + for (std::int64_t next = std::int64_t(size) - 2; next >= std::int64_t(end); next--) update_accumulator_incremental( - featureTransformer, ksq, m_accumulators[next], m_accumulators[next + 1]); + featureTransformer, ksq, accumulators[next], accumulators[next + 1]); - assert((m_accumulators[end].acc()).computed[Perspective]); + assert((accumulators[end].acc()).computed[Perspective]); } // Explicit template instantiations @@ -256,6 +257,49 @@ auto make_accumulator_update_context(const FeatureTransformer& featu accumulatorTo}; } +template +void double_inc_update(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]); + + FeatureSet::IndexList removed, added; + FeatureSet::append_changed_indices(ksq, middle_state.dirtyPiece, 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); + FeatureSet::append_changed_indices(ksq, target_state.dirtyPiece, 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(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 update_accumulator_incremental( const FeatureTransformer& featureTransformer, @@ -323,7 +367,8 @@ void update_accumulator_refresh_cache(const FeatureTransformer& feat const Position& pos, AccumulatorState& accumulatorState, AccumulatorCaches::Cache& cache) { - using Tiling [[maybe_unused]] = SIMDTiling; + + using Tiling [[maybe_unused]] = SIMDTiling; const Square ksq = pos.square(Perspective); auto& entry = cache[ksq][Perspective]; diff --git a/src/nnue/nnue_accumulator.h b/src/nnue/nnue_accumulator.h index d83a5a446..10aadc917 100644 --- a/src/nnue/nnue_accumulator.h +++ b/src/nnue/nnue_accumulator.h @@ -37,12 +37,6 @@ class Position; namespace Stockfish::Eval::NNUE { -using BiasType = std::int16_t; -using PSQTWeightType = std::int32_t; -using IndexType = std::uint32_t; - -struct Networks; - template struct alignas(CacheLineSize) Accumulator; @@ -149,13 +143,12 @@ struct AccumulatorState { class AccumulatorStack { public: AccumulatorStack() : - m_accumulators(MAX_PLY + 1), - m_current_idx{} {} + accumulators(MAX_PLY + 1), + size{1} {} [[nodiscard]] const AccumulatorState& latest() const noexcept; - void - reset(const Position& rootPos, const Networks& networks, AccumulatorCaches& caches) noexcept; + void reset() noexcept; void push(const DirtyPiece& dirtyPiece) noexcept; void pop() noexcept; @@ -185,8 +178,8 @@ class AccumulatorStack { const FeatureTransformer& featureTransformer, const std::size_t end) noexcept; - std::vector m_accumulators; - std::size_t m_current_idx; + std::vector accumulators; + std::size_t size; }; } // namespace Stockfish::Eval::NNUE diff --git a/src/nnue/nnue_architecture.h b/src/nnue/nnue_architecture.h index 0c9f097dc..c020ce05b 100644 --- a/src/nnue/nnue_architecture.h +++ b/src/nnue/nnue_architecture.h @@ -49,6 +49,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; diff --git a/src/nnue/nnue_common.h b/src/nnue/nnue_common.h index f21a8dec7..550bcaad4 100644 --- a/src/nnue/nnue_common.h +++ b/src/nnue/nnue_common.h @@ -48,6 +48,11 @@ namespace Stockfish::Eval::NNUE { +using BiasType = std::int16_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 +81,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 diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h index beb0c7f1c..37634cbc6 100644 --- a/src/nnue/nnue_feature_transformer.h +++ b/src/nnue/nnue_feature_transformer.h @@ -31,174 +31,10 @@ #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_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() - #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_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() - #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_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() - #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_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} - #define NumRegistersSIMD 16 - #define MaxChunkSize 16 - -#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(); - } -} - // Returns the inverse of a permutation template constexpr std::array @@ -240,61 +76,6 @@ void permute(T (&data)[N], const std::array& order) { } } -// 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 -}; - - // Input feature converter template class FeatureTransformer { @@ -397,6 +178,8 @@ class FeatureTransformer { OutputType* output, int bucket) const { + using namespace SIMD; + accumulatorStack.evaluate(pos, *this, *cache); const auto& accumulatorState = accumulatorStack.latest(); diff --git a/src/nnue/nnue_misc.cpp b/src/nnue/nnue_misc.cpp index 809d454b5..c99874076 100644 --- a/src/nnue/nnue_misc.cpp +++ b/src/nnue/nnue_misc.cpp @@ -121,7 +121,6 @@ trace(Position& pos, const Eval::NNUE::Networks& networks, Eval::NNUE::Accumulat }; AccumulatorStack accumulators; - accumulators.reset(pos, networks, caches); // 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. @@ -140,7 +139,7 @@ trace(Position& pos, const Eval::NNUE::Networks& networks, Eval::NNUE::Accumulat { pos.remove_piece(sq); - accumulators.reset(pos, networks, caches); + 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; @@ -157,7 +156,7 @@ trace(Position& pos, const Eval::NNUE::Networks& networks, Eval::NNUE::Accumulat ss << board[row] << '\n'; ss << '\n'; - accumulators.reset(pos, networks, caches); + accumulators.reset(); auto t = networks.big.trace_evaluate(pos, accumulators, &caches.big); ss << " NNUE network contributions " diff --git a/src/nnue/simd.h b/src/nnue/simd.h new file mode 100644 index 000000000..f37eeb930 --- /dev/null +++ b/src/nnue/simd.h @@ -0,0 +1,406 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 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 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_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 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_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 16 + #define MaxChunkSize 32 + +#elif USE_SSE2 +using vec_t = __m128i; +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 + + #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 ? 16 : 8) + #define MaxChunkSize 16 + +#elif USE_NEON +using vec_t = int16x8_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 + +#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/position.cpp b/src/position.cpp index 0e5748e6a..5e2c27822 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -597,10 +597,14 @@ bool Position::pseudo_legal(const Move m) const { if ((Rank8BB | Rank1BB) & to) return false; - if (!(attacks_bb(from, us) & 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)) @@ -698,7 +702,6 @@ DirtyPiece Position::do_move(Move m, // our state pointer to point to the new (ready to be updated) state. std::memcpy(&newSt, st, offsetof(StateInfo, key)); newSt.previous = st; - st->next = &newSt; st = &newSt; // Increment ply counters. In particular, rule50 will be reset to zero later on @@ -707,9 +710,6 @@ DirtyPiece Position::do_move(Move m, ++st->rule50; ++st->pliesFromNull; - DirtyPiece dp; - dp.dirty_num = 1; - Color us = sideToMove; Color them = ~us; Square from = m.from_sq(); @@ -717,6 +717,12 @@ DirtyPiece Position::do_move(Move m, Piece pc = piece_on(from); Piece captured = m.type_of() == EN_PASSANT ? make_piece(them, PAWN) : piece_on(to); + DirtyPiece dp; + dp.pc = pc; + dp.from = from; + dp.to = to; + dp.add_sq = SQ_NONE; + assert(color_of(pc) == us); assert(captured == NO_PIECE || color_of(captured) == (m.type_of() != CASTLING ? them : us)); assert(type_of(captured) != KING); @@ -763,10 +769,8 @@ DirtyPiece Position::do_move(Move m, st->minorPieceKey ^= 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; + dp.remove_pc = captured; + dp.remove_sq = capsq; // Update board and piece lists remove_piece(capsq); @@ -777,6 +781,8 @@ DirtyPiece Position::do_move(Move m, // 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]; @@ -799,9 +805,6 @@ DirtyPiece Position::do_move(Move m, // 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); } @@ -828,12 +831,9 @@ DirtyPiece Position::do_move(Move m, remove_piece(to); put_piece(promotion, to); - // 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 // Zobrist::psq[pc][to] is zero, so we don't need to clear it @@ -900,6 +900,10 @@ DirtyPiece Position::do_move(Move m, 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)); return dp; } @@ -982,13 +986,10 @@ void Position::do_castling( if (Do) { - 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 @@ -1011,7 +1012,6 @@ void Position::do_null_move(StateInfo& newSt, const TranspositionTable& tt) { std::memcpy(&newSt, st, sizeof(StateInfo)); newSt.previous = st; - st->next = &newSt; st = &newSt; if (st->epSquare != SQ_NONE) diff --git a/src/position.h b/src/position.h index 75f22c7df..cf6b1c472 100644 --- a/src/position.h +++ b/src/position.h @@ -53,7 +53,6 @@ struct StateInfo { Key key; Bitboard checkersBB; StateInfo* previous; - StateInfo* next; Bitboard blockersForKing[COLOR_NB]; Bitboard pinners[COLOR_NB]; Bitboard checkSquares[PIECE_TYPE_NB]; @@ -87,9 +86,9 @@ 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; @@ -165,7 +164,6 @@ class Position { bool pos_is_ok() const; void flip(); - // Used by NNUE StateInfo* state() const; void put_piece(Piece pc, Square s); @@ -216,11 +214,11 @@ 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]; } diff --git a/src/search.cpp b/src/search.cpp index 625fd2599..6beb8d096 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -65,25 +64,15 @@ using namespace Search; namespace { +constexpr int SEARCHEDLIST_CAPACITY = 32; +using SearchedList = ValueList; + // (*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. -// Futility margin -Value futility_margin(Depth d, bool noTtCutNode, bool improving, bool oppWorsening) { - Value futilityMult = 110 - 25 * noTtCutNode; - Value improvingDeduction = improving * futilityMult * 2; - Value worseningDeduction = oppWorsening * futilityMult / 3; - - return futilityMult * d - improvingDeduction - worseningDeduction; -} - -constexpr int futility_move_count(bool improving, Depth depth) { - return (3 + depth * depth) / (2 - improving); -} - 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; @@ -95,32 +84,7 @@ int correction_value(const Worker& w, const Position& pos, const Stack* const ss m.is_ok() ? (*(ss - 2)->continuationCorrectionHistory)[pos.piece_on(m.to_sq())][m.to_sq()] : 0; - return 7685 * pcv + 7495 * micv + 9144 * (wnpcv + bnpcv) + 6469 * cntcv; -} - -int risk_tolerance(const Position& pos, Value v) { - // Returns (some constant of) second derivative of sigmoid. - static constexpr auto sigmoid_d2 = [](int x, int y) { - return 644800 * x / ((x * x + 3 * y * y) * y); - }; - - int m = pos.count() + pos.non_pawn_material() / 300; - - // a and b are the crude approximation of the wdl model. - // The win rate is: 1/(1+exp((a-v)/b)) - // The loss rate is 1/(1+exp((v+a)/b)) - int a = 356; - int b = ((65 * m - 3172) * m + 240578) / 2048; - - // guard against overflow - assert(abs(v) + a <= std::numeric_limits::max() / 644800); - - // The risk utility is therefore d/dv^2 (1/(1+exp(-(v-a)/b)) -1/(1+exp(-(-v-a)/b))) - // -115200x/(x^2+3) = -345600(ab) / (a^2+3b^2) (multiplied by some constant) (second degree pade approximant) - int winning_risk = sigmoid_d2(v - a, b); - int losing_risk = sigmoid_d2(v + a, b); - - return -(winning_risk + losing_risk) * 32; + return 7696 * pcv + 7689 * micv + 9708 * (wnpcv + bnpcv) + 6978 * cntcv; } // Add correctionHistory value to raw staticEval and guarantee evaluation @@ -136,11 +100,11 @@ void update_correction_history(const Position& pos, const Move m = (ss - 1)->currentMove; const Color us = pos.side_to_move(); - static constexpr int nonPawnWeight = 162; + static constexpr int nonPawnWeight = 172; workerThread.pawnCorrectionHistory[pawn_structure_index(pos)][us] << bonus * 111 / 128; - workerThread.minorPieceCorrectionHistory[minor_piece_index(pos)][us] << bonus * 146 / 128; + workerThread.minorPieceCorrectionHistory[minor_piece_index(pos)][us] << bonus * 151 / 128; workerThread.nonPawnCorrectionHistory[non_pawn_index(pos)][WHITE][us] << bonus * nonPawnWeight / 128; workerThread.nonPawnCorrectionHistory[non_pawn_index(pos)][BLACK][us] @@ -148,7 +112,7 @@ void update_correction_history(const Position& pos, if (m.is_ok()) (*(ss - 2)->continuationCorrectionHistory)[pos.piece_on(m.to_sq())][m.to_sq()] - << bonus * 143 / 128; + << bonus * 141 / 128; } // Add a small random component to draw evaluations to avoid 3-fold blindness @@ -159,16 +123,16 @@ 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_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, - Square prevSq, - ValueList& quietsSearched, - ValueList& capturesSearched, - Depth depth, - bool isTTMove, - int moveCount); +void update_all_stats(const Position& pos, + Stack* ss, + Search::Worker& workerThread, + Move bestMove, + Square prevSq, + SearchedList& quietsSearched, + SearchedList& capturesSearched, + Depth depth, + Move TTMove, + int moveCount); } // namespace @@ -196,7 +160,7 @@ void Search::Worker::ensure_network_replicated() { void Search::Worker::start_searching() { - accumulatorStack.reset(rootPos, networks[numaAccessToken], refreshTable); + accumulatorStack.reset(); // Non-main threads go directly to iterative_deepening() if (!is_mainthread()) @@ -366,7 +330,7 @@ void Search::Worker::iterative_deepening() { int searchAgainCounter = 0; - lowPlyHistory.fill(92); + lowPlyHistory.fill(86); // Iterative deepening loop until requested to stop or the target depth is reached while (++rootDepth < MAX_PLY && !threads.stop @@ -402,13 +366,13 @@ void Search::Worker::iterative_deepening() { selDepth = 0; // Reset aspiration window starting size - delta = 5 + std::abs(rootMoves[pvIdx].meanSquaredScore) / 11834; + delta = 5 + std::abs(rootMoves[pvIdx].meanSquaredScore) / 11134; Value avg = rootMoves[pvIdx].averageScore; alpha = std::max(avg - delta, -VALUE_INFINITE); beta = std::min(avg + delta, VALUE_INFINITE); // Adjust optimism based on root move's averageScore - optimism[us] = 138 * avg / (std::abs(avg) + 84); + optimism[us] = 137 * avg / (std::abs(avg) + 91); optimism[~us] = -optimism[us]; // Start with a small aspiration window and, in the case of a fail @@ -549,7 +513,9 @@ void Search::Worker::iterative_deepening() { fallingEval = std::clamp(fallingEval, 0.5786, 1.6752); // If the bestMove is stable over several iterations, reduce time accordingly - timeReduction = lastBestMoveDepth + 8 < completedDepth ? 1.4857 : 0.7046; + double k = 0.527; + double center = lastBestMoveDepth + 11; + timeReduction = 0.8 + 0.84 / (1.077 + std::exp(-k * (completedDepth - center))); double reduction = (1.4540 + mainThread->previousTimeReduction) / (2.1593 * timeReduction); double bestMoveInstability = 0.9929 + 1.8519 * totBestMoveChanges / threads.size(); @@ -604,6 +570,7 @@ void Search::Worker::do_move(Position& pos, const Move move, StateInfo& st) { void Search::Worker::do_move(Position& pos, const Move move, StateInfo& st, const bool givesCheck) { DirtyPiece dp = pos.do_move(move, st, givesCheck, &tt); + nodes.fetch_add(1, std::memory_order_relaxed); accumulatorStack.push(dp); } @@ -619,28 +586,27 @@ void Search::Worker::undo_null_move(Position& pos) { pos.undo_null_move(); } // Reset histories, usually before a new game void Search::Worker::clear() { - mainHistory.fill(66); - lowPlyHistory.fill(105); - captureHistory.fill(-646); - pawnHistory.fill(-1262); - pawnCorrectionHistory.fill(6); + mainHistory.fill(67); + captureHistory.fill(-688); + pawnHistory.fill(-1287); + pawnCorrectionHistory.fill(5); minorPieceCorrectionHistory.fill(0); nonPawnCorrectionHistory.fill(0); - ttMoveHistory.fill(0); + ttMoveHistory = 0; for (auto& to : continuationCorrectionHistory) for (auto& h : to) - h.fill(5); + 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(-468); + h.fill(-473); for (size_t i = 1; i < reductions.size(); ++i) - reductions[i] = int(2954 / 128.0 * std::log(i)); + reductions[i] = int(2796 / 128.0 * std::log(i)); refreshTable.clear(networks[numaAccessToken]); } @@ -687,12 +653,11 @@ Value Search::Worker::search( Value bestValue, value, eval, maxValue, probCutBeta; bool givesCheck, improving, priorCapture, opponentWorsening; bool capture, ttCapture; - int priorReduction = (ss - 1)->reduction; - (ss - 1)->reduction = 0; + int priorReduction; Piece movedPiece; - ValueList capturesSearched; - ValueList quietsSearched; + SearchedList capturesSearched; + SearchedList quietsSearched; // Step 1. Initialize node Worker* thisThread = this; @@ -733,10 +698,12 @@ Value Search::Worker::search( assert(0 <= ss->ply && ss->ply < MAX_PLY); - bestMove = Move::none(); + 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; - Square prevSq = ((ss - 1)->currentMove).is_ok() ? ((ss - 1)->currentMove).to_sq() : SQ_NONE; - ss->statScore = 0; // Step 4. Transposition table lookup excludedMove = ss->excludedMove; @@ -766,17 +733,34 @@ Value Search::Worker::search( // Bonus for a quiet ttMove that fails high if (!ttCapture) update_quiet_histories(pos, ss, *this, ttData.move, - std::min(120 * depth - 75, 1241)); + std::min(125 * depth - 77, 1157)); // Extra penalty for early quiet moves of the previous ply if (prevSq != SQ_NONE && (ss - 1)->moveCount <= 3 && !priorCapture) - update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -2200); + update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -2301); } // Partial workaround for the graph history interaction problem // For high rule50 counts don't produce transposition table cutoffs. if (pos.rule50_count() < 90) - return ttData.value; + { + if (depth >= 8 && ttData.move && pos.pseudo_legal(ttData.move) && pos.legal(ttData.move) + && !is_decisive(ttData.value)) + { + do_move(pos, ttData.move, st); + Key nextPosKey = pos.key(); + auto [ttHitNext, ttDataNext, ttWriterNext] = tt.probe(nextPosKey); + undo_move(pos, 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 @@ -844,10 +828,7 @@ Value Search::Worker::search( goto moves_loop; } else if (excludedMove) - { - // Providing the hint that this node's accumulator will be used often unadjustedStaticEval = eval = ss->staticEval; - } else if (ss->ttHit) { // Never assume anything about values stored in TT @@ -874,13 +855,13 @@ Value Search::Worker::search( } // Use static evaluation difference to improve quiet move ordering - if (((ss - 1)->currentMove).is_ok() && !(ss - 1)->inCheck && !priorCapture) + if (((ss - 1)->currentMove).is_ok() && !(ss - 1)->inCheck && !priorCapture && !ttHit) { - int bonus = std::clamp(-10 * int((ss - 1)->staticEval + ss->staticEval), -1950, 1416) + 655; - thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << bonus * 1124 / 1024; + int bonus = std::clamp(-10 * int((ss - 1)->staticEval + ss->staticEval), -1858, 1492) + 661; + thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << bonus * 1057 / 1024; 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 * 1196 / 1024; + << bonus * 1266 / 1024; } // Set up the improving flag, which is true if current static evaluation is @@ -893,34 +874,42 @@ Value Search::Worker::search( if (priorReduction >= 3 && !opponentWorsening) depth++; - if (priorReduction >= 1 && depth >= 2 && ss->staticEval + (ss - 1)->staticEval > 188) + if (priorReduction >= 1 && depth >= 2 && ss->staticEval + (ss - 1)->staticEval > 175) depth--; // 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 - 461 - 315 * depth * depth) + if (!PvNode && eval < alpha - 486 - 325 * depth * depth) return qsearch(pos, ss, alpha, beta); // Step 8. Futility pruning: child node // The depth condition is important for mate finding. - if (!ss->ttPv && depth < 14 - && eval - futility_margin(depth, cutNode && !ss->ttHit, improving, opponentWorsening) - - (ss - 1)->statScore / 301 + 37 + ((eval - beta) / 8) - - std::abs(correctionValue) / 139878 - >= beta - && eval >= beta && (!ttData.move || ttCapture) && !is_loss(beta) && !is_win(eval)) - return beta + (eval - beta) / 3; + { + auto futility_margin = [&](Depth d) { + Value futilityMult = 93 - 20 * (cutNode && !ss->ttHit); + + return futilityMult * d // + - improving * futilityMult * 2 // + - opponentWorsening * futilityMult / 3 // + + (ss - 1)->statScore / 376 // + + std::abs(correctionValue) / 168639; + }; + + if (!ss->ttPv && depth < 14 && eval - futility_margin(depth) >= beta && eval >= beta + && (!ttData.move || ttCapture) && !is_loss(beta) && !is_win(eval)) + return beta + (eval - beta) / 3; + } // Step 9. Null move search with verification search if (cutNode && (ss - 1)->currentMove != Move::null() && eval >= beta - && ss->staticEval >= beta - 19 * depth + 418 && !excludedMove && pos.non_pawn_material(us) + && ss->staticEval >= beta - 19 * depth + 389 && !excludedMove && pos.non_pawn_material(us) && ss->ply >= thisThread->nmpMinPly && !is_loss(beta)) { assert(eval - beta >= 0); - // Null move dynamic reduction based on depth and eval - Depth R = std::min(int(eval - beta) / 232, 6) + depth / 3 + 5; + // Null move dynamic reduction based on depth + Depth R = 7 + depth / 3; ss->currentMove = Move::null(); ss->continuationHistory = &thisThread->continuationHistory[0][0][NO_PIECE][0]; @@ -957,26 +946,24 @@ Value Search::Worker::search( // Step 10. Internal iterative reductions // For PV nodes without a ttMove as well as for deep enough cutNodes, we decrease depth. - // (* Scaler) Especially if they make IIR more aggressive. - if (((PvNode || cutNode) && depth >= 7 - 3 * PvNode) && !ttData.move) + // (*Scaler) Especially if they make IIR less aggressive. + if (!allNode && depth >= 6 && !ttData.move) depth--; // 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 + 185 - 58 * improving; + probCutBeta = beta + 201 - 58 * improving; if (depth >= 3 && !is_decisive(beta) // 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. + // probCut there && !(is_valid(ttData.value) && ttData.value < probCutBeta)) { assert(probCutBeta < VALUE_INFINITE && probCutBeta > beta); MovePicker mp(pos, ttData.move, probCutBeta - ss->staticEval, &thisThread->captureHistory); - Depth probCutDepth = std::max(depth - 4, 0); + Depth probCutDepth = std::max(depth - 5, 0); while ((move = mp.next_move()) != Move::none()) { @@ -990,10 +977,8 @@ Value Search::Worker::search( movedPiece = pos.moved_piece(move); do_move(pos, move, st); - thisThread->nodes.fetch_add(1, std::memory_order_relaxed); ss->currentMove = move; - ss->isTTMove = (move == ttData.move); ss->continuationHistory = &this->continuationHistory[ss->inCheck][true][movedPiece][move.to_sq()]; ss->continuationCorrectionHistory = @@ -1025,7 +1010,7 @@ Value Search::Worker::search( moves_loop: // When in check, search starts here // Step 12. A small Probcut idea - probCutBeta = beta + 415; + probCutBeta = beta + 400; 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; @@ -1078,6 +1063,8 @@ moves_loop: // When in check, search starts here movedPiece = pos.moved_piece(move); givesCheck = pos.gives_check(move); + (ss + 1)->quietMoveStreak = (!capture && !givesCheck) ? (ss->quietMoveStreak + 1) : 0; + // Calculate new depth for this move newDepth = depth - 1; @@ -1085,20 +1072,18 @@ moves_loop: // When in check, search starts here Depth r = reduction(improving, depth, moveCount, delta); - r -= 32 * moveCount; - // Increase reduction for ttPv nodes (*Scaler) // Smaller or even negative value is better for short time controls // Bigger value is better for long time controls if (ss->ttPv) - r += 979; + r += 968; // Step 14. Pruning at shallow depth. // Depth conditions are important for mate finding. if (!rootNode && pos.non_pawn_material(us) && !is_loss(bestValue)) { // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold - if (moveCount >= futility_move_count(improving, depth)) + if (moveCount >= (3 + depth * depth) / (2 - improving)) mp.skip_quiet_moves(); // Reduced depth of the next LMR search @@ -1113,26 +1098,25 @@ moves_loop: // When in check, search starts here // Futility pruning for captures if (!givesCheck && lmrDepth < 7 && !ss->inCheck) { - Value futilityValue = ss->staticEval + 242 + 230 * lmrDepth - + PieceValue[capturedPiece] + 133 * captHist / 1024; + Value futilityValue = ss->staticEval + 232 + 224 * lmrDepth + + PieceValue[capturedPiece] + 131 * captHist / 1024; if (futilityValue <= alpha) continue; } // SEE based pruning for captures and checks - int seeHist = std::clamp(captHist / 32, -138 * depth, 135 * depth); - if (!pos.see_ge(move, -154 * depth - seeHist)) + int seeHist = std::clamp(captHist / 31, -137 * depth, 125 * depth); + if (!pos.see_ge(move, -158 * depth - seeHist)) { - bool skip = true; - if (depth > 2 && !capture && givesCheck && alpha < 0 - && pos.non_pawn_material(us) == PieceValue[movedPiece] - && PieceValue[movedPiece] >= RookValue - && !(PseudoAttacks[KING][pos.square(us)] & move.from_sq())) - skip = mp.otherPieceTypesMobile( - type_of(movedPiece), - capturesSearched); // if the opponent captures last mobile piece it might be stalemate + bool mayStalemateTrap = + depth > 2 && alpha < 0 && pos.non_pawn_material(us) == PieceValue[movedPiece] + && PieceValue[movedPiece] >= RookValue + // it can't be stalemate if we moved a piece adjacent to the king + && !(attacks_bb(pos.square(us)) & move.from_sq()) + && !mp.can_move_king_or_pawn(); - if (skip) + // avoid pruning sacrifices of our last piece for stalemate + if (!mayStalemateTrap) continue; } } @@ -1144,15 +1128,16 @@ moves_loop: // When in check, search starts here + thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][move.to_sq()]; // Continuation history based pruning - if (history < -4348 * depth) + if (history < -4229 * depth) continue; history += 68 * thisThread->mainHistory[us][move.from_to()] / 32; - lmrDepth += history / 3593; + lmrDepth += history / 3388; - Value futilityValue = ss->staticEval + (bestMove ? 48 : 146) + 116 * lmrDepth - + 103 * (bestValue < ss->staticEval - 128); + Value baseFutility = (bestMove ? 46 : 230); + Value futilityValue = + ss->staticEval + baseFutility + 117 * lmrDepth + 102 * (ss->staticEval > alpha); // Futility pruning: parent node // (*Scaler): Generally, more frequent futility pruning @@ -1174,84 +1159,77 @@ moves_loop: // When in check, search starts here } // Step 15. Extensions - // We take care to not overdo to avoid search getting stuck. - if (ss->ply < thisThread->rootDepth * 2) + // 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 - (thisThread->completedDepth > 27) + ss->ttPv && is_valid(ttData.value) + && !is_decisive(ttData.value) && (ttData.bound & BOUND_LOWER) + && ttData.depth >= depth - 3) { - // 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. + Value singularBeta = ttData.value - (58 + 76 * (ss->ttPv && !PvNode)) * depth / 57; + Depth singularDepth = newDepth / 2; - // (* Scaler) Generally, higher singularBeta (i.e closer to ttValue) - // and lower extension margins scale well. + ss->excludedMove = move; + value = search(pos, ss, singularBeta - 1, singularBeta, singularDepth, cutNode); + ss->excludedMove = Move::none(); - if (!rootNode && move == ttData.move && !excludedMove - && depth >= 6 - (thisThread->completedDepth > 29) + ss->ttPv - && is_valid(ttData.value) && !is_decisive(ttData.value) - && (ttData.bound & BOUND_LOWER) && ttData.depth >= depth - 3) + if (value < singularBeta) { - Value singularBeta = ttData.value - (59 + 77 * (ss->ttPv && !PvNode)) * depth / 54; - Depth singularDepth = newDepth / 2; + int corrValAdj = std::abs(correctionValue) / 248400; + int doubleMargin = -4 + 244 * PvNode - 206 * !ttCapture - corrValAdj + - 997 * ttMoveHistory / 131072 + - (ss->ply > thisThread->rootDepth) * 47; + int tripleMargin = 84 + 269 * PvNode - 253 * !ttCapture + 91 * ss->ttPv - corrValAdj + - (ss->ply * 2 > thisThread->rootDepth * 3) * 54; - 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) - { - int corrValAdj1 = std::abs(correctionValue) / 248873; - int corrValAdj2 = std::abs(correctionValue) / 255331; - int doubleMargin = 262 * PvNode - 188 * !ttCapture - corrValAdj1 - - ttMoveHistory[pawn_structure_index(pos)][us] / 128; - int tripleMargin = - 88 + 265 * PvNode - 256 * !ttCapture + 93 * ss->ttPv - corrValAdj2; - - extension = 1 + (value < singularBeta - doubleMargin) - + (value < singularBeta - tripleMargin); - - depth++; - } - - // 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)) - 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; + depth++; } + + // 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)) + 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); - thisThread->nodes.fetch_add(1, std::memory_order_relaxed); // Add extension to new depth newDepth += extension; // Update the current move (this must be done after singular extension search) ss->currentMove = move; - ss->isTTMove = (move == ttData.move); ss->continuationHistory = &thisThread->continuationHistory[ss->inCheck][capture][movedPiece][move.to_sq()]; ss->continuationCorrectionHistory = @@ -1260,49 +1238,44 @@ moves_loop: // When in check, search starts here // Decrease reduction for PvNodes (*Scaler) if (ss->ttPv) - r -= 2381 + PvNode * 1008 + (ttData.value > alpha) * 880 - + (ttData.depth >= depth) * (1022 + cutNode * 1140); + r -= 2437 + PvNode * 926 + (ttData.value > alpha) * 901 + + (ttData.depth >= depth) * (943 + cutNode * 1180); // These reduction adjustments have no proven non-linear scaling - r += 306 - moveCount * 34; - - r -= std::abs(correctionValue) / 29696; - - if (PvNode && std::abs(bestValue) <= 2000) - r -= risk_tolerance(pos, bestValue); + r += 650; // Base reduction offset to compensate for other tweaks + r -= moveCount * 66; + r -= std::abs(correctionValue) / 28047; // Increase reduction for cut nodes if (cutNode) - r += 2784 + 1038 * !ttData.move; + r += 2864 + 966 * !ttData.move; - // Increase reduction if ttMove is a capture but the current move is not a capture - if (ttCapture && !capture) - r += 1171 + (depth < 8) * 985; + // Increase reduction if ttMove is a capture + if (ttCapture) + r += 1350; // Increase reduction if next ply has a lot of fail high if ((ss + 1)->cutoffCnt > 2) - r += 1042 + allNode * 864; + r += 1036 + allNode * 848; + + r += (ss + 1)->quietMoveStreak * 50; // For first picked move (ttMove) reduce reduction - else if (move == ttData.move) - r -= 1937; + if (move == ttData.move) + r -= 2006; if (capture) ss->statScore = - 846 * int(PieceValue[pos.captured_piece()]) / 128 - + thisThread->captureHistory[movedPiece][move.to_sq()][type_of(pos.captured_piece())] - - 4822; - else if (ss->inCheck) - ss->statScore = thisThread->mainHistory[us][move.from_to()] - + (*contHist[0])[movedPiece][move.to_sq()] - 2771; + 826 * int(PieceValue[pos.captured_piece()]) / 128 + + thisThread->captureHistory[movedPiece][move.to_sq()][type_of(pos.captured_piece())]; else ss->statScore = 2 * thisThread->mainHistory[us][move.from_to()] + (*contHist[0])[movedPiece][move.to_sq()] - + (*contHist[1])[movedPiece][move.to_sq()] - 3271; + + (*contHist[1])[movedPiece][move.to_sq()]; // Decrease/increase reduction for moves with a good/bad history - r -= ss->statScore * 1582 / 16384; + r -= ss->statScore * 826 / 8192; // Step 17. Late moves reduction / extension (LMR) if (depth >= 2 && moveCount > 1) @@ -1312,23 +1285,22 @@ moves_loop: // When in check, search starts here // 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 / 1024, newDepth + !allNode + (PvNode && !bestMove))); + Depth d = std::max(1, std::min(newDepth - r / 1024, + newDepth + !allNode + (PvNode && !bestMove))) + + PvNode; 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 + // (*Scaler) Usually doing more shallower searches + // doesn't scale well to longer TCs if (value > alpha && d < newDepth) { // 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 + 43 + 2 * newDepth); + const bool doDeeperSearch = value > (bestValue + 42 + 2 * newDepth); const bool doShallowerSearch = value < bestValue + 9; newDepth += doDeeperSearch - doShallowerSearch; @@ -1337,7 +1309,7 @@ moves_loop: // When in check, search starts here value = -search(pos, ss + 1, -(alpha + 1), -alpha, newDepth, !cutNode); // Post LMR continuation history updates - update_continuation_histories(ss, movedPiece, move.to_sq(), 1600); + update_continuation_histories(ss, movedPiece, move.to_sq(), 1508); } else if (value > alpha && value < bestValue + 9) newDepth--; @@ -1348,11 +1320,11 @@ moves_loop: // When in check, search starts here { // Increase reduction if ttMove is not present if (!ttData.move) - r += 1156; + r += 1128; // Note that if expected reduction is high, we reduce search depth here value = -search(pos, ss + 1, -(alpha + 1), -alpha, - newDepth - (r > 3495) - (r > 5510 && newDepth > 2), !cutNode); + newDepth - (r > 3200) - (r > 4600 && newDepth > 2), !cutNode); } // For PV nodes only, do a full PV search on the first move or after a fail high, @@ -1456,21 +1428,19 @@ moves_loop: // When in check, search starts here assert(value >= beta); // Fail high break; } - else - { - // Reduce other moves if we have found at least one score improvement - if (depth > 2 && depth < 16 && !is_decisive(value)) - 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 < 16 && !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.push_back(move); @@ -1487,7 +1457,7 @@ moves_loop: // When in check, search starts here assert(moveCount || !ss->inCheck || excludedMove || !MoveList(pos).size()); // Adjust best value for fail high cases - if (bestValue >= beta && !is_decisive(bestValue) && !is_decisive(beta) && !is_decisive(alpha)) + if (bestValue >= beta && !is_decisive(bestValue) && !is_decisive(alpha)) bestValue = (bestValue * depth + beta) / (depth + 1); if (!moveCount) @@ -1498,37 +1468,34 @@ moves_loop: // When in check, search starts here else if (bestMove) { update_all_stats(pos, ss, *this, bestMove, prevSq, quietsSearched, capturesSearched, depth, - bestMove == ttData.move, moveCount); + ttData.move, moveCount); if (!PvNode) - { - int bonus = (ttData.move == move) ? 800 : -600 * moveCount; - ttMoveHistory[pawn_structure_index(pos)][us] << bonus; - } + ttMoveHistory << (bestMove == ttData.move ? 800 : -879); } // Bonus for prior quiet countermove that caused the fail low else if (!priorCapture && prevSq != SQ_NONE) { - int bonusScale = - (std::min(78 * depth - 312, 194) + 34 * !allNode + 164 * ((ss - 1)->moveCount > 8) - + 141 * (!ss->inCheck && bestValue <= ss->staticEval - 100) - + 121 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 75) - + 86 * ((ss - 1)->isTTMove) + 86 * (ss->cutoffCnt <= 3) - + std::min(-(ss - 1)->statScore / 112, 303)); + int bonusScale = -220; + bonusScale += std::min(-(ss - 1)->statScore / 103, 323); + bonusScale += std::min(73 * depth, 531); + bonusScale += 174 * ((ss - 1)->moveCount > 8); + bonusScale += 144 * (!ss->inCheck && bestValue <= ss->staticEval - 104); + bonusScale += 128 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 82); bonusScale = std::max(bonusScale, 0); - const int scaledBonus = std::min(160 * depth - 99, 1492) * bonusScale; + const int scaledBonus = std::min(159 * depth - 94, 1501) * bonusScale; update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, - scaledBonus * 388 / 32768); + scaledBonus * 412 / 32768); thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] - << scaledBonus * 212 / 32768; + << scaledBonus * 203 / 32768; 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] - << scaledBonus * 1055 / 32768; + << scaledBonus * 1040 / 32768; } // Bonus for prior capture countermove that caused the fail low @@ -1536,7 +1503,7 @@ moves_loop: // When in check, search starts here { Piece capturedPiece = pos.captured_piece(); assert(capturedPiece != NO_PIECE); - thisThread->captureHistory[pos.piece_on(prevSq)][prevSq][type_of(capturedPiece)] << 1100; + thisThread->captureHistory[pos.piece_on(prevSq)][prevSq][type_of(capturedPiece)] << 1080; } if (PvNode) @@ -1667,9 +1634,8 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) } else { - // In case of null move search, use previous static eval with opposite sign - unadjustedStaticEval = - (ss - 1)->currentMove != Move::null() ? evaluate(pos) : -(ss - 1)->staticEval; + unadjustedStaticEval = evaluate(pos); + ss->staticEval = bestValue = to_corrected_static_eval(unadjustedStaticEval, correctionValue); } @@ -1691,7 +1657,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) if (bestValue > alpha) alpha = bestValue; - futilityBase = ss->staticEval + 359; + futilityBase = ss->staticEval + 376; } const PieceToHistory* contHist[] = {(ss - 1)->continuationHistory, @@ -1753,11 +1719,11 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) && (*contHist[0])[pos.moved_piece(move)][move.to_sq()] + thisThread->pawnHistory[pawn_structure_index(pos)][pos.moved_piece(move)] [move.to_sq()] - <= 6290) + <= 6218) continue; // Do not search moves with bad enough SEE values - if (!pos.see_ge(move, -75)) + if (!pos.see_ge(move, -74)) continue; } @@ -1765,7 +1731,6 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) Piece movedPiece = pos.moved_piece(move); do_move(pos, move, st, givesCheck); - thisThread->nodes.fetch_add(1, std::memory_order_relaxed); // Update the current move ss->currentMove = move; @@ -1840,7 +1805,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) Depth Search::Worker::reduction(bool i, Depth d, int mn, int delta) const { int reductionScale = reductions[d] * reductions[mn]; - return reductionScale - delta * 764 / rootDelta + !i * reductionScale * 191 / 512 + 1087; + return reductionScale - delta * 794 / rootDelta + !i * reductionScale * 205 / 512 + 1086; } // elapsed() returns the time elapsed since the search started. If the @@ -1921,50 +1886,50 @@ void update_pv(Move* pv, Move move, const Move* childPv) { // Updates stats at the end of search() when a bestMove is found -void update_all_stats(const Position& pos, - Stack* ss, - Search::Worker& workerThread, - Move bestMove, - Square prevSq, - ValueList& quietsSearched, - ValueList& capturesSearched, - Depth depth, - bool isTTMove, - int moveCount) { +void update_all_stats(const Position& pos, + Stack* ss, + Search::Worker& workerThread, + Move bestMove, + Square prevSq, + SearchedList& quietsSearched, + SearchedList& capturesSearched, + Depth depth, + Move ttMove, + int moveCount) { CapturePieceToHistory& captureHistory = workerThread.captureHistory; - Piece moved_piece = pos.moved_piece(bestMove); - PieceType captured; + Piece movedPiece = pos.moved_piece(bestMove); + PieceType capturedPiece; - int bonus = std::min(141 * depth - 89, 1613) + 311 * isTTMove; - int malus = std::min(695 * depth - 215, 2808) - 31 * (moveCount - 1); + int bonus = std::min(143 * depth - 89, 1496) + 302 * (bestMove == ttMove); + int malus = std::min(737 * depth - 179, 3141) - 30 * moveCount; if (!pos.capture_stage(bestMove)) { - update_quiet_histories(pos, ss, workerThread, bestMove, bonus * 1129 / 1024); + update_quiet_histories(pos, ss, workerThread, bestMove, bonus * 1059 / 1024); // Decrease stats for all non-best quiet moves for (Move move : quietsSearched) - update_quiet_histories(pos, ss, workerThread, move, -malus * 1246 / 1024); + update_quiet_histories(pos, ss, workerThread, move, -malus * 1310 / 1024); } 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] << bonus * 1187 / 1024; + capturedPiece = type_of(pos.piece_on(bestMove.to_sq())); + captureHistory[movedPiece][bestMove.to_sq()][capturedPiece] << bonus * 1213 / 1024; } // 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 * 987 / 1024); + update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -malus * 580 / 1024); // Decrease stats for all non-best capture moves for (Move move : capturesSearched) { - moved_piece = pos.moved_piece(move); - captured = type_of(pos.piece_on(move.to_sq())); - captureHistory[moved_piece][move.to_sq()][captured] << -malus * 1377 / 1024; + movedPiece = pos.moved_piece(move); + capturedPiece = type_of(pos.piece_on(move.to_sq())); + captureHistory[movedPiece][move.to_sq()][capturedPiece] << -malus * 1388 / 1024; } } @@ -1973,7 +1938,7 @@ void update_all_stats(const Position& pos, // at ply -1, -2, -3, -4, and -6 with current move. void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus) { static constexpr std::array conthist_bonuses = { - {{1, 1103}, {2, 659}, {3, 323}, {4, 533}, {5, 121}, {6, 474}}}; + {{1, 1092}, {2, 631}, {3, 294}, {4, 517}, {5, 126}, {6, 445}}}; for (const auto [i, weight] : conthist_bonuses) { @@ -1994,12 +1959,14 @@ void update_quiet_histories( workerThread.mainHistory[us][move.from_to()] << bonus; // Untuned to prevent duplicate effort if (ss->ply < LOW_PLY_HISTORY_SIZE) - workerThread.lowPlyHistory[ss->ply][move.from_to()] << bonus * 829 / 1024; + workerThread.lowPlyHistory[ss->ply][move.from_to()] << bonus * 792 / 1024; - update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus * 1004 / 1024); + update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), + bonus * (bonus > 0 ? 1082 : 784) / 1024); int pIndex = pawn_structure_index(pos); - workerThread.pawnHistory[pIndex][pos.moved_piece(move)][move.to_sq()] << bonus * 587 / 1024; + workerThread.pawnHistory[pIndex][pos.moved_piece(move)][move.to_sq()] + << bonus * (bonus > 0 ? 705 : 450) / 1024; } } diff --git a/src/search.h b/src/search.h index f721e9c36..e0055f2e9 100644 --- a/src/search.h +++ b/src/search.h @@ -77,7 +77,7 @@ struct Stack { bool ttHit; int cutoffCnt; int reduction; - bool isTTMove; + int quietMoveStreak; }; diff --git a/src/timeman.cpp b/src/timeman.cpp index 29ebffcaa..5840e2556 100644 --- a/src/timeman.cpp +++ b/src/timeman.cpp @@ -85,16 +85,13 @@ void TimeManagement::init(Search::LimitsType& limits, // with constants are involved. const int64_t scaleFactor = useNodesTime ? npmsec : 1; const TimePoint scaledTime = limits.time[us] / scaleFactor; - const TimePoint scaledInc = limits.inc[us] / scaleFactor; // 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 && double(centiMTG) / scaledInc > 5.051) - { + if (scaledTime < 1000) centiMTG = scaledTime * 5.051; - } // Make sure timeLeft is > 0 since we may use it as a divisor TimePoint timeLeft = diff --git a/src/timeman.h b/src/timeman.h index 1175e39c5..483f3a4d8 100644 --- a/src/timeman.h +++ b/src/timeman.h @@ -27,6 +27,7 @@ namespace Stockfish { class OptionsMap; +enum Color : int8_t; namespace Search { struct LimitsType; diff --git a/src/tt.cpp b/src/tt.cpp index 5d8457611..953348987 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -110,6 +110,8 @@ void TTEntry::save( value16 = int16_t(v); eval16 = int16_t(ev); } + else if (depth8 + DEPTH_ENTRY_OFFSET >= 5 && Bound(genBound8 & 0x3) != BOUND_EXACT) + depth8--; } @@ -234,8 +236,8 @@ std::tuple TranspositionTable::probe(const Key key) cons // 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) + if (replace->depth8 - replace->relative_age(generation8) + > tte[i].depth8 - tte[i].relative_age(generation8)) replace = &tte[i]; return {false, diff --git a/src/types.h b/src/types.h index d7f9dfb74..d40e1e292 100644 --- a/src/types.h +++ b/src/types.h @@ -57,9 +57,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) @@ -110,13 +116,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, @@ -132,7 +138,7 @@ enum CastlingRights { CASTLING_RIGHT_NB = 16 }; -enum Bound { +enum Bound : int8_t { BOUND_NONE, BOUND_UPPER, BOUND_LOWER, @@ -183,13 +189,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, @@ -203,26 +209,24 @@ constexpr Value PieceValue[PIECE_NB] = { using Depth = int; -enum : int { - // 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). - 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. - DEPTH_UNSEARCHED = -2, - DEPTH_ENTRY_OFFSET = -3 -}; +// 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, @@ -238,7 +242,7 @@ enum Square : int { }; // clang-format on -enum Direction : int { +enum Direction : int8_t { NORTH = 8, EAST = 1, SOUTH = -NORTH, @@ -250,7 +254,7 @@ enum Direction : int { NORTH_WEST = NORTH + WEST }; -enum File : int { +enum File : int8_t { FILE_A, FILE_B, FILE_C, @@ -262,7 +266,7 @@ enum File : int { FILE_NB }; -enum Rank : int { +enum Rank : int8_t { RANK_1, RANK_2, RANK_3, @@ -276,18 +280,14 @@ 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; - - // 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]; - - // From and to squares, which may be SQ_NONE - Square from[3]; - Square to[3]; + // 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; }; #define ENABLE_INCR_OPERATORS_ON(T) \ diff --git a/src/uci.h b/src/uci.h index 5c1c07f7b..1686b3a72 100644 --- a/src/uci.h +++ b/src/uci.h @@ -33,7 +33,7 @@ namespace Stockfish { class Position; class Move; class Score; -enum Square : int; +enum Square : int8_t; using Value = int; class UCIEngine { diff --git a/tests/signature.sh b/tests/signature.sh index 06bd1892e..0f6dd7585 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 "$WINE_PATH ./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