From d5bbc6b67d3184fc39fa87a18596e77e92af990c Mon Sep 17 00:00:00 2001 From: anematode Date: Fri, 3 Jul 2026 20:29:50 +0200 Subject: [PATCH] [RfC] RISC-V port and universal binary Performance on Spacemit K3, thanks @edolnx for testing master: Total time (ms) : 65200 Nodes searched : 3493826 Nodes/second : 53586 riscv-scalable-port: Total time (ms) : 15834 Nodes searched : 3493826 Nodes/second : 220653 Also thanks to @camel-cdr for guidance on RVV programming, and https://cloud-v.co for supplying an RVV instance to test with passed STC: LLR: 2.81 (-2.94,2.94) <0.00,2.00> Total: 1152 W: 527 L: 108 D: 517 Ptnml(0-2): 0, 17, 167, 348, 44 https://tests.stockfishchess.org/tests/view/6a39895b3036e45021aeb368 ## Summary We've had a `riscv64` target for a while, but haven't really optimized for it, in particular the vector extension (RVV). RVV, like SVE, is based on a scalable vector system where the vector length ranges from 128 to 65536. In practice implementations are between 128 and 2048, and 256 bits is quite common (e.g. the Spacemit K3 system above). Unfortunately this doesn't fit well into the rest of our code which assumes a fixed vector length, so what I've done is bypass the `VECTOR` ifdef (which now basically means "FIXED_LENGTH_VECTOR") and just have RVV-specific paths. The ability to explicitly control `vl` makes the code quite readable, in my opinion. We use LMUL>1 in most places to take advantage of multi-vector instructions. Generally the LMULs were chosen to best support a 256-bit vlen, which is very common, but by virtue of how the vlen control works, the code works with any vlen. In a couple places, i.e., `get_changed_pieces` and `AffineTransformSparseInput::propagate`, we have separate implementations depending on the vlen, because the optimal LMUL varies a lot between implementations. One little wrinkle is that `load_as` is compiled to a sequence of byte loads, because although unaligned loads are legal in RVA23, the spec says that they *may* be extremely slow (even though they usually aren't, in actual hw), so compilers are conservative. Thus I aligned the relevant buffers and made the semantics of `load_as` that the operand is aligned, by adding a runtime assertion. ### Universal binary Adding a universal binary is pretty easy and we can just cross-compile. There are two targets: baseline rv64gc and riscv64-rva23, which is actually a smaller subset of RVA23 that also works on some older processors that don't support the full thing. We use clang because GCC, until recently, has a nasty bug with LTO and RVV. Like the universal ARM and x86 builds, we check all the builds in CI. In this case we run bench with multiple vlens, 128 through 1024. In the meantime I deleted the existing broken and unused riscv64 tests. ### Follow-ups - Optimizations - zvdot4a8i path closes https://github.com/official-stockfish/Stockfish/pull/6920 No functional change --- .github/ci/universal_matrix.json | 9 ++ .github/workflows/tests.yml | 18 --- .github/workflows/universal_compilation.yml | 62 ++++++++ scripts/check_universal_riscv.sh | 73 ++++++++++ scripts/get_native_properties.sh | 32 ++++- src/Makefile | 46 ++++-- src/memory.h | 9 ++ src/nnue/layers/affine_transform.h | 20 +++ .../layers/affine_transform_sparse_input.h | 51 ++++++- src/nnue/layers/clipped_relu.h | 18 +++ src/nnue/layers/sqr_clipped_relu.h | 16 +++ src/nnue/nnue_accumulator.cpp | 135 ++++++++++++++++++ src/nnue/nnue_feature_transformer.h | 34 +++++ src/nnue/nnz_helper.h | 2 +- src/nnue/simd.h | 33 +++++ src/universal/entry_riscv64.cpp | 96 +++++++++++++ 16 files changed, 617 insertions(+), 37 deletions(-) create mode 100755 scripts/check_universal_riscv.sh create mode 100644 src/universal/entry_riscv64.cpp diff --git a/.github/ci/universal_matrix.json b/.github/ci/universal_matrix.json index 307f29549..ce31b602b 100644 --- a/.github/ci/universal_matrix.json +++ b/.github/ci/universal_matrix.json @@ -28,6 +28,15 @@ }, "binaries": "arm64-universal" }, + { + "config": { + "name": "Ubuntu 22.04 GCC Universal riscv64", + "os": "ubuntu-22.04", + "simple_name": "linux", + "archive_ext": "tar.gz" + }, + "binaries": "riscv64-universal" + }, { "config": { "name": "Windows 11 Clang Universal arm64", diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b09ec5662..b502b82ec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,15 +39,6 @@ jobs: comp: ndk run_armv7_tests: true 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++ @@ -371,15 +362,6 @@ jobs: make -j4 ARCH=armv7-neon build ../tests/signature.sh $benchref - # riscv64 tests - - - name: Test riscv64 build - if: matrix.config.run_riscv64_tests - run: | - echo "cd src && export LDFLAGS='-static' && make clean && make -j4 ARCH=riscv64 build" > script.sh - docker run --rm --platform ${{ matrix.config.platform }} -v ${{ github.workspace }}:/app sf_builder - ../tests/signature.sh $benchref - # ppc64 tests - name: Test ppc64 build diff --git a/.github/workflows/universal_compilation.yml b/.github/workflows/universal_compilation.yml index 687be4946..1fc5476aa 100644 --- a/.github/workflows/universal_compilation.yml +++ b/.github/workflows/universal_compilation.yml @@ -254,3 +254,65 @@ jobs: . !.git !.output + + UniversalLinuxRISCV64: + name: Linux riscv64 + runs-on: ubuntu-22.04 + defaults: + run: + shell: bash + env: + QEMU_DEB: https://launchpad.net/ubuntu/+archive/primary/+files/qemu-user_9.2.1+ds-1ubuntu5_amd64.deb + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Install clang and the riscv cross runtime + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc >/dev/null + echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null + sudo apt-get update + sudo apt-get install -y clang-20 lld-20 llvm-20 g++-riscv64-linux-gnu + echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH" + - name: Install qemu + run: | + curl -sL -o /tmp/qemu.deb "$QEMU_DEB" + dpkg -x /tmp/qemu.deb /tmp/qemu + /tmp/qemu/usr/bin/qemu-riscv64 --version + echo "/tmp/qemu/usr/bin" >> "$GITHUB_PATH" + - name: Build universal binary + run: | + make -j4 -C src ARCH=riscv64-universal build \ + COMP=clang COMPCXX=clang++ \ + EXTRACXXFLAGS="--target=riscv64-linux-gnu" \ + EXTRALDFLAGS="--target=riscv64-linux-gnu -fuse-ld=lld" \ + OBJCOPY=llvm-objcopy + - name: Strip binary + run: llvm-strip src/stockfish + + - name: Rename binary + run: mv src/stockfish stockfish-linux-riscv64-universal + + - name: Extract the bench number from the commit history + run: | + for hash in $(git rev-list -100 HEAD); do + benchref=$(git show -s $hash | tac | grep -m 1 -o -x '[[:space:]]*\b[Bb]ench[ :]\+[1-9][0-9]\{5,7\}\b[[:space:]]*' | sed 's/[^0-9]//g') && break || true + done + [[ -n "$benchref" ]] && echo "benchref=$benchref" >> $GITHUB_ENV && echo "From commit: $hash" && echo "Reference bench: $benchref" || echo "No bench found" + - name: Check arch selection under qemu + run: | + QEMU=qemu-riscv64 QEMU_LD_PREFIX=/usr/riscv64-linux-gnu \ + ./scripts/check_universal_riscv.sh ./stockfish-linux-riscv64-universal "$benchref" + - name: Remove non-src files + run: git clean -fxd -- src + + - name: Upload artifact for (pre)-release + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: linux riscv64-universal + path: | + . + !.git + !.output diff --git a/scripts/check_universal_riscv.sh b/scripts/check_universal_riscv.sh new file mode 100755 index 000000000..d55f756f6 --- /dev/null +++ b/scripts/check_universal_riscv.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +# Verify that the riscv64 universal binary selects correct build +# and benches correctly under qemu-riscv64 with various vlens +# +# Usage: check_universal_riscv.sh STOCKFISH_EXE EXPECTED_BENCH + +set -eu + +if [ $# -ne 2 ]; then + echo "Usage: $0 STOCKFISH_EXE EXPECTED_BENCH" >&2 + exit 2 +fi + +STOCKFISH_EXE=$1 +EXPECTED_BENCH=$2 +QEMU=${QEMU:-qemu-riscv64} +export QEMU_LD_PREFIX=${QEMU_LD_PREFIX:-/usr/riscv64-linux-gnu} + +EXTS=zba=true,zbb=true,zbs=true,zicond=true +PAIRS=" +rv64:riscv64 +rv64,v=true,$EXTS,vlen=128:riscv64-rva23 +rv64,v=true,$EXTS,vlen=256:riscv64-rva23 +rv64,v=true,$EXTS,vlen=512:riscv64-rva23 +rv64,v=true,$EXTS,vlen=1024:riscv64-rva23 +" + +BINARY_SIZE=$(wc -c < "$STOCKFISH_EXE") +MAX_SIZE=$((150 * 1024 * 1024)) +if [ "$BINARY_SIZE" -gt "$MAX_SIZE" ]; then + printf 'check_universal_riscv.sh: binary size %d bytes exceeds 150 MB limit\n' "$BINARY_SIZE" >&2 + exit 1 +fi + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +idx=0 +for pair in $PAIRS; do + idx=$((idx + 1)) + cpu=${pair%%:*} + ( + comp=$($QEMU -cpu "$cpu" "$STOCKFISH_EXE" compiler 2>&1 | awk -F: '/Compilation architecture/ { + sub(/^[[:space:]]+/, "", $2); sub(/[[:space:]]+$/, "", $2); print $2; exit }') + bench=$($QEMU -cpu "$cpu" "$STOCKFISH_EXE" bench 2>&1 | awk -F: '/Nodes searched/ { + gsub(/[^0-9]/, "", $2); print $2; exit }') + printf '%s|%s|%s\n' "$cpu" "$comp" "$bench" > "$tmp/$idx" + ) & +done +wait + +FAIL=0 +idx=0 +for pair in $PAIRS; do + idx=$((idx + 1)) + expected=${pair##*:} + IFS='|' read -r cpu comp bench < "$tmp/$idx" + if [ "$comp" = "$expected" ] && [ "$bench" = "$EXPECTED_BENCH" ]; then + printf 'CPU %-26s ok (%s, bench %s)\n' "$cpu" "$comp" "$bench" >&2 + else + printf 'CPU %-26s FAIL: expected %s/%s, got %s/%s\n' \ + "$cpu" "$expected" "$EXPECTED_BENCH" "${comp:--}" "${bench:--}" >&2 + FAIL=1 + fi +done + +if [ "$FAIL" != 0 ]; then + echo "check_universal_riscv.sh: failed" + exit 1 +fi + +echo "check_universal_riscv.sh: Good! Universal binary has correct hardware detection." diff --git a/scripts/get_native_properties.sh b/scripts/get_native_properties.sh index 84e58498a..e92b11c17 100755 --- a/scripts/get_native_properties.sh +++ b/scripts/get_native_properties.sh @@ -53,6 +53,26 @@ get_flags() { flags=$(normalize_ws "$flags") } +# Populate $flags from the RISC-V isa string; split the single-letter +# base (e.g., rv64imafdcv), and keep multi-letter extensions. +get_riscv_flags() { + if [ -r "$cpuinfo_path" ]; then + isa=$(awk -F: '/^isa[ \t]*:/{print $2; exit}' "$cpuinfo_path" 2>/dev/null) + else + isa='' + fi + isa=$(printf '%s\n' "$isa" | tr '[:upper:]' '[:lower:]') + flags=$(printf '%s\n' "$isa" | awk '{ + gsub(/^[ \t]*rv(32|64)/, ""); + n = split($0, ext, "_"); + out = ""; + for (i = 1; i <= length(ext[1]); i++) out = out " " substr(ext[1], i, 1); + for (i = 2; i <= n; i++) out = out " " ext[i]; + print out; + }') + flags=$(normalize_ws "$flags") +} + # Populate $flags from sysctl on Darwin x86_64. get_sysctl_flags() { if [ -n "${GP_SYSCTL_FEATURES:-}" ]; then @@ -193,6 +213,16 @@ EOF ) } +set_arch_riscv64() { + get_riscv_flags + true_arch=$( + select_arch_from_table <<'EOF' +riscv64-rva23|match_flags|v zba zbb zbs zicond +riscv64|match_true| +EOF + ) +} + set_arch_x86_64() { true_arch=$( select_arch_from_table <<'EOF' @@ -324,7 +354,7 @@ case $uname_s in set_arch_loongarch64 ;; riscv64) - true_arch='riscv64' + set_arch_riscv64 ;; e2k*) true_arch='e2k' diff --git a/src/Makefile b/src/Makefile index 0970362dd..247768511 100644 --- a/src/Makefile +++ b/src/Makefile @@ -83,7 +83,7 @@ SRCS = attacks.cpp benchmark.cpp bitboard.cpp evaluate.cpp main.cpp \ nnue/features/half_ka_v2_hm.cpp nnue/features/full_threats.cpp \ engine.cpp score.cpp memory.cpp -OTHER_SRCS = universal/entry_x86.cpp universal/entry_arm64.cpp universal/nnue_embed.cpp +OTHER_SRCS = universal/entry_x86.cpp universal/entry_arm64.cpp universal/entry_riscv64.cpp universal/nnue_embed.cpp HEADERS = attacks.h benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h history.h \ nnue/nnue_misc.h nnue/features/half_ka_v2_hm.h nnue/features/full_threats.h \ @@ -160,13 +160,13 @@ ifeq ($(ARCH), $(filter $(ARCH), \ 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 arm64-universal apple-silicon general-64 general-32 riscv64 \ - loongarch64 loongarch64-lsx loongarch64-lasx wasm32 wasm32-relaxed-simd)) + riscv64-rva23 riscv64-universal loongarch64 loongarch64-lsx loongarch64-lasx wasm32 wasm32-relaxed-simd)) SUPPORTED_ARCH=true else SUPPORTED_ARCH=false endif -ifneq (,$(filter $(ARCH),x86-64-universal arm64-universal)) +ifneq (,$(filter $(ARCH),x86-64-universal arm64-universal riscv64-universal)) UNIVERSAL_BUILD=true else UNIVERSAL_BUILD=false @@ -195,6 +195,7 @@ vsx = no neon = no dotprod = no arm_version = 0 +rva23 = no lsx = no lasx = no relaxedsimd = no @@ -459,10 +460,16 @@ ifeq ($(findstring e2k,$(ARCH)),e2k) popcnt = yes endif -ifeq ($(ARCH),riscv64) +ifeq ($(findstring riscv64,$(ARCH)),riscv64) arch = riscv64 endif +ifeq ($(ARCH),riscv64-rva23) + rva23 = yes + popcnt = yes + prefetch = yes +endif + ifeq ($(findstring loongarch64,$(ARCH)),loongarch64) arch = loongarch64 prefetch = yes @@ -509,9 +516,6 @@ ifeq ($(COMP),gcc) CXXFLAGS += -m$(bits) LDFLAGS += -m$(bits) endif - ifeq ($(ARCH),riscv64) - CXXFLAGS += -latomic - endif else ifeq ($(arch),loongarch64) CXXFLAGS += -latomic else ifneq ($(arch),wasm32) @@ -583,9 +587,6 @@ ifeq ($(COMP),clang) CXXFLAGS += -m$(bits) LDFLAGS += -m$(bits) endif - ifeq ($(ARCH),riscv64) - CXXFLAGS += -latomic - endif else ifeq ($(arch),loongarch64) CXXFLAGS += -latomic else @@ -771,7 +772,7 @@ else endif ifeq ($(popcnt),yes) - ifeq ($(arch),$(filter $(arch),ppc64 ppc64-altivec ppc64-vsx armv7 armv8 arm64)) + ifeq ($(arch),$(filter $(arch),ppc64 ppc64-altivec ppc64-vsx armv7 armv8 arm64 riscv64)) CXXFLAGS += -DUSE_POPCNT else CXXFLAGS += -msse3 -mpopcnt -DUSE_POPCNT @@ -870,6 +871,10 @@ ifeq ($(dotprod),yes) CXXFLAGS += -march=armv8.2-a+dotprod -DUSE_NEON_DOTPROD endif +ifeq ($(rva23),yes) + CXXFLAGS += -march=rv64gcv1p0_zba_zbb_zbs_zicbop_zicond -DUSE_RVV +endif + ifeq ($(lasx),yes) CXXFLAGS += -DUSE_LASX ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) @@ -1047,6 +1052,8 @@ help: echo "general-64 > unspecified 64-bit" && \ echo "general-32 > unspecified 32-bit" && \ echo "riscv64 > RISC-V 64-bit" && \ + echo "riscv64-rva23 > RISC-V 64-bit RVA23 profile" && \ + echo "riscv64-universal > RISC-V 64-bit with automatic runtime selection of the best architecture" && \ echo "loongarch64 > LoongArch 64-bit" && \ echo "loongarch64-lsx > LoongArch 64-bit with SIMD eXtension" && \ echo "loongarch64-lasx > LoongArch 64-bit with Advanced SIMD eXtension" && \ @@ -1383,6 +1390,11 @@ ifeq ($(arch),x86_64) UNIVERSAL_ENTRY_OBJ := $(TEMP_DIR)/entry_x86.o UNIVERSAL_ENTRY_SRC := $(UNIVERSAL_SRC_DIR)/entry_x86.cpp BASELINE_ARCH := x86-64 +else ifeq ($(arch),riscv64) + UNIVERSAL_ARCHES := riscv64 riscv64-rva23 + UNIVERSAL_ENTRY_OBJ := $(TEMP_DIR)/entry_riscv64.o + UNIVERSAL_ENTRY_SRC := $(UNIVERSAL_SRC_DIR)/entry_riscv64.cpp + BASELINE_ARCH := riscv64 else UNIVERSAL_ARCHES := armv8 armv8-dotprod UNIVERSAL_ENTRY_OBJ := $(TEMP_DIR)/entry_arm64.o @@ -1452,7 +1464,11 @@ arch-cxxflags = $(EMBED_DIR) -DStockfish=$(call arch-namespace,$(1)) -DUNIVERS $(call arch-defmain,$(1)) -Wno-c++26-extensions -Wno-c23-extensions UOBJ_TGT ?= universal-object-pgo -OBJCOPY ?= objcopy +ifeq ($(COMP),clang) + OBJCOPY ?= llvm-objcopy +else + OBJCOPY ?= objcopy +endif .PHONY: build profile-build universal-clean build: UOBJ_TGT := universal-object-nopgo @@ -1469,10 +1485,10 @@ $(NET_SENTINEL): | $(TEMP_DIR) @touch $@ $(NNUE_EMBED_OBJ): $(UNIVERSAL_SRC_DIR)/nnue_embed.cpp $(NET_SENTINEL) | $(TEMP_DIR) - $(CXX) -O2 -std=c++20 -Wno-c++26-extensions $(SLICE_DEF) $(mac_target_flags) $(EMBED_DIR) -c $< -o $@ + $(CXX) -O2 -std=c++20 -Wno-c++26-extensions $(SLICE_DEF) $(mac_target_flags) $(EXTRACXXFLAGS) $(EMBED_DIR) -c $< -o $@ $(UNIVERSAL_ENTRY_OBJ): $(UNIVERSAL_ENTRY_SRC) | $(TEMP_DIR) - $(CXX) -O2 -std=c++20 $(mac_target_flags) -c $< -o $@ + $(CXX) -O2 -std=c++20 $(mac_target_flags) $(EXTRACXXFLAGS) -c $< -o $@ # Symlink tracked top-level items from src/ into temp_builds//. $(TEMP_DIR)/%/.setup: | $(TEMP_DIR) @@ -1510,7 +1526,7 @@ endif endif $(UNIVERSAL_EXE): $(UNIVERSAL_ENTRY_OBJ) $(NNUE_EMBED_OBJ) $(ARCH_OBJS) - $(CXX) -o $@ $(UNIVERSAL_ENTRY_OBJ) $(NNUE_EMBED_OBJ) $(ARCH_OBJS) $(UNIVERSAL_FINAL_FLAGS) + $(CXX) -o $@ $(UNIVERSAL_ENTRY_OBJ) $(NNUE_EMBED_OBJ) $(ARCH_OBJS) $(UNIVERSAL_FINAL_FLAGS) $(EXTRALDFLAGS) @echo "Universal binary built: $@" endif # UNIVERSAL_BUILD diff --git a/src/memory.h b/src/memory.h index d5bccf34e..95ba8fb59 100644 --- a/src/memory.h +++ b/src/memory.h @@ -20,6 +20,7 @@ #define MEMORY_H_INCLUDED #include +#include #include #include #include @@ -326,6 +327,14 @@ T load_as(const ByteT* buffer) { static_assert(std::is_trivially_copyable::value, "Type must be trivially copyable"); static_assert(sizeof(ByteT) == 1); + if (reinterpret_cast(buffer) % alignof(T) != 0) + { + assert(false); +#ifdef __GNUC__ + __builtin_unreachable(); +#endif + } + T value; std::memcpy(&value, buffer, sizeof(T)); diff --git a/src/nnue/layers/affine_transform.h b/src/nnue/layers/affine_transform.h index f92ccc70f..bdf6ff281 100644 --- a/src/nnue/layers/affine_transform.h +++ b/src/nnue/layers/affine_transform.h @@ -105,6 +105,26 @@ affine_transform_non_ssse3(i32* output, const i8* weights, const i32* biases, co #endif } + #elif defined(USE_RVV) + for (IndexType i = 0; i < OutputDimensions; ++i) + { + const i8* row = &weights[i * PaddedInputDimensions]; + vint32m1_t vsum = __riscv_vmv_v_x_i32m1(0, __riscv_vsetvlmax_e32m1()); + + for (usize j = 0; j < InputDimensions;) + { + usize vl = __riscv_vsetvl_e8m4(InputDimensions - j); + + vint8m4_t w = __riscv_vle8_v_i8m4(&row[j], vl); + vuint8m4_t x = __riscv_vle8_v_u8m4(&input[j], vl); + vint16m8_t prod = __riscv_vwmulsu_vv_i16m8(w, x, vl); + + vsum = __riscv_vwredsum_vs_i16m8_i32m1(prod, vsum, vl); + j += vl; + } + + output[i] = biases[i] + __riscv_vmv_x_s_i32m1_i32(vsum); + } #else std::memcpy(output, biases, sizeof(i32) * OutputDimensions); diff --git a/src/nnue/layers/affine_transform_sparse_input.h b/src/nnue/layers/affine_transform_sparse_input.h index 4ae2a70b7..bdce63918 100644 --- a/src/nnue/layers/affine_transform_sparse_input.h +++ b/src/nnue/layers/affine_transform_sparse_input.h @@ -59,7 +59,8 @@ class AffineTransformSparseInput { static constexpr IndexType PaddedOutputDimensions = ceil_to_multiple(OutputDimensions, MaxSimdWidth); -#if (defined(USE_SSSE3) || defined(USE_LSX) || defined(USE_LASX) || (USE_NEON >= 8)) +#if (defined(USE_SSSE3) || defined(USE_LSX) || defined(USE_LASX) || (USE_NEON >= 8) \ + || defined(USE_RVV)) static constexpr IndexType ChunkSize = 4; #else static constexpr IndexType ChunkSize = 1; @@ -82,7 +83,8 @@ class AffineTransformSparseInput { } static constexpr IndexType get_weight_index(IndexType i) { -#if (defined(USE_SSSE3) || defined(USE_LSX) || defined(USE_LASX) || (USE_NEON >= 8)) +#if (defined(USE_SSSE3) || defined(USE_LSX) || defined(USE_LASX) || (USE_NEON >= 8) \ + || defined(USE_RVV)) return get_weight_index_scrambled(i); #else return i; @@ -275,6 +277,51 @@ class AffineTransformSparseInput { #ifdef vec_add_32 #undef vec_add_32 #endif +#elif defined(USE_RVV) + static_assert(InputDimensions % 256 == 0); + + const i8* weights_cp = weights; + + #define RVV_SPARSE_PROPAGATE(LMUL) \ + do \ + { \ + const usize blk = __riscv_vsetvlmax_e32m##LMUL(); \ + for (IndexType ob = 0; ob < OutputDimensions; ob += blk) \ + { \ + const usize vl = __riscv_vsetvl_e32m##LMUL(OutputDimensions - ob); \ + vint32m##LMUL##_t acc = __riscv_vle32_v_i32m##LMUL(biases + ob, vl); \ + for (IndexType k = 0; k < InputDimensions / 256; ++k) \ + { \ + u64 bits = load_as(nnzInfo.bitset + k * 8); \ + isize base = k * 64; \ + auto* base_addr = input + base * sizeof(i32); \ + auto* weights_base = &weights_cp[base * OutputDimensions * ChunkSize]; \ + while (bits) \ + { \ + isize i = pop_lsb(bits); \ + vuint8m##LMUL##_t a = __riscv_vreinterpret_v_u32m##LMUL##_u8m##LMUL( \ + __riscv_vmv_v_x_u32m##LMUL(load_as(base_addr + i * sizeof(i32)), \ + vl)); \ + vint8m##LMUL##_t b = __riscv_vle8_v_i8m##LMUL( \ + &weights_base[i * OutputDimensions * ChunkSize + ob * ChunkSize], \ + vl * ChunkSize); \ + acc = \ + __riscv_vadd_vv_i32m##LMUL(acc, SIMD::rvv_dpbusd_m##LMUL(a, b, vl), vl); \ + } \ + } \ + __riscv_vse32_v_i32m##LMUL(output + ob, acc, vl); \ + } \ + } while (0) + + // Select LMUL + if (__riscv_vsetvlmax_e32m1() >= OutputDimensions) + RVV_SPARSE_PROPAGATE(1); + else if (__riscv_vsetvlmax_e32m2() >= OutputDimensions) + RVV_SPARSE_PROPAGATE(2); + else + RVV_SPARSE_PROPAGATE(4); + + #undef RVV_SPARSE_PROPAGATE #else // Use dense implementation for the other architectures. affine_transform_non_ssse3( diff --git a/src/nnue/layers/clipped_relu.h b/src/nnue/layers/clipped_relu.h index 5809ae6ed..2dd5319ea 100644 --- a/src/nnue/layers/clipped_relu.h +++ b/src/nnue/layers/clipped_relu.h @@ -147,6 +147,24 @@ class ClippedReLU { } constexpr IndexType Start = NumChunks * 16; +#elif defined(USE_RVV) + + for (usize j = 0; j < InputDimensions;) + { + usize vl = __riscv_vsetvl_e32m4(InputDimensions - j); + + vint32m4_t in = __riscv_vle32_v_i32m4(&input[j], vl); + in = __riscv_vmax_vx_i32m4(in, 0, vl); + + vint16m2_t words = + __riscv_vnclip_wx_i16m2(in, WeightScaleBitsLocal, __RISCV_VXRM_RDN, vl); + vint8m1_t narrowed = __riscv_vnclip_wx_i8m1(words, 0, __RISCV_VXRM_RDN, vl); + + __riscv_vse8_v_u8m1(&output[j], __riscv_vreinterpret_v_i8m1_u8m1(narrowed), vl); + j += vl; + } + constexpr IndexType Start = InputDimensions; + #else constexpr IndexType Start = 0; #endif diff --git a/src/nnue/layers/sqr_clipped_relu.h b/src/nnue/layers/sqr_clipped_relu.h index a0886c270..5da8e183e 100644 --- a/src/nnue/layers/sqr_clipped_relu.h +++ b/src/nnue/layers/sqr_clipped_relu.h @@ -143,6 +143,22 @@ class SqrClippedReLU { } constexpr IndexType Start = NumChunks * 16; +#elif defined(USE_RVV) + + for (usize j = 0; j < InputDimensions;) + { + usize vl = __riscv_vsetvl_e32m4(InputDimensions - j); + vint32m4_t in = __riscv_vle32_v_i32m4(&input[j], vl); + + vint16m2_t words = __riscv_vnclip_wx_i16m2(in, 0, __RISCV_VXRM_RDN, vl); + vint16m2_t sqr = __riscv_vmulh_vv_i16m2(words, words, vl); + vint8m1_t narrowed = __riscv_vnclip_wx_i8m1(sqr, SimdShiftAmount, __RISCV_VXRM_RDN, vl); + + __riscv_vse8_v_u8m1(&output[j], __riscv_vreinterpret_v_i8m1_u8m1(narrowed), vl); + j += vl; + } + constexpr IndexType Start = InputDimensions; + #else constexpr IndexType Start = 0; #endif diff --git a/src/nnue/nnue_accumulator.cpp b/src/nnue/nnue_accumulator.cpp index a65c3a9a2..ec3f2022e 100644 --- a/src/nnue/nnue_accumulator.cpp +++ b/src/nnue/nnue_accumulator.cpp @@ -288,6 +288,64 @@ void apply_combined(Color perspective, vec_store_psqt(&toTilePsqt[k], psqt[k]); } +#elif defined(USE_RVV) + + usize tileOffset = 0; + + const auto* psqWeights = &featureTransformer.weights[0]; + const auto* threatWeights = &featureTransformer.threatWeights[0]; + const auto* psqtWeights = &featureTransformer.psqtWeights[0]; + const auto* threatPsqtWeights = &featureTransformer.threatPsqtWeights[0]; + + while (tileOffset < Dimensions) + { + usize vl = __riscv_vsetvl_e16m8(Dimensions - tileOffset); + + vint16m8_t accum = __riscv_vle16_v_i16m8(&fromAcc[tileOffset], vl); + for (int i : psqRemoved) + accum = __riscv_vsub_vv_i16m8( + accum, __riscv_vle16_v_i16m8(&psqWeights[i * Dimensions + tileOffset], vl), vl); + for (int i : psqAdded) + accum = __riscv_vadd_vv_i16m8( + accum, __riscv_vle16_v_i16m8(&psqWeights[i * Dimensions + tileOffset], vl), vl); + for (int i : thrRemoved) + accum = __riscv_vwsub_wv_i16m8( + accum, __riscv_vle8_v_i8m4(&threatWeights[i * Dimensions + tileOffset], vl), vl); + for (int i : thrAdded) + accum = __riscv_vwadd_wv_i16m8( + accum, __riscv_vle8_v_i8m4(&threatWeights[i * Dimensions + tileOffset], vl), vl); + __riscv_vse16_v_i16m8(&toAcc[tileOffset], accum, vl); + + tileOffset += vl; + } + + tileOffset = 0; + + while (tileOffset < PSQTBuckets) + { + usize vl = __riscv_vsetvl_e32m1(PSQTBuckets - tileOffset); + + vint32m1_t accum = __riscv_vle32_v_i32m1(&fromPsqtAcc[tileOffset], vl); + for (int i : psqRemoved) + accum = __riscv_vsub_vv_i32m1( + accum, __riscv_vle32_v_i32m1(&psqtWeights[i * PSQTBuckets + tileOffset], vl), vl); + for (int i : psqAdded) + accum = __riscv_vadd_vv_i32m1( + accum, __riscv_vle32_v_i32m1(&psqtWeights[i * PSQTBuckets + tileOffset], vl), vl); + for (int i : thrRemoved) + accum = __riscv_vsub_vv_i32m1( + accum, __riscv_vle32_v_i32m1(&threatPsqtWeights[i * PSQTBuckets + tileOffset], vl), + vl); + for (int i : thrAdded) + accum = __riscv_vadd_vv_i32m1( + accum, __riscv_vle32_v_i32m1(&threatPsqtWeights[i * PSQTBuckets + tileOffset], vl), + vl); + + __riscv_vse32_v_i32m1(&toPsqtAcc[tileOffset], accum, vl); + + tileOffset += vl; + } + #else toAcc = fromAcc; @@ -448,6 +506,25 @@ Bitboard get_changed_pieces(const std::array& oldPieces, } return ~sameBB; +#elif defined(USE_RVV) + + #define IMPL(mx, bx) \ + return __riscv_vmv_x_s_u64m1_u64(__riscv_vreinterpret_v_u8m1_u64m1( \ + __riscv_vreinterpret_v_b##bx##_u8m1(__riscv_vmsne_vv_i8m##mx##_b##bx( \ + __riscv_vle8_v_i8m##mx(reinterpret_cast(oldPieces.data()), 64), \ + __riscv_vle8_v_i8m##mx(reinterpret_cast(newPieces.data()), 64), 64)))) + + + usize vl = __riscv_vsetvlmax_e8m1(); + if (vl >= 64) + IMPL(1, 8); + else if (vl == 32) + IMPL(2, 4); + else + IMPL(4, 2); + + #undef IMPL + #else Bitboard changed = 0; @@ -596,6 +673,64 @@ void update_accumulator_refresh_cache(Color perspective, vec_store_psqt(&accTilePsqt[k], psqt[k]); } +#elif defined(USE_RVV) + + const auto* weights = &featureTransformer.weights[0]; + const auto* threatWeights = &featureTransformer.threatWeights[0]; + const auto* psqtWeights = &featureTransformer.psqtWeights[0]; + const auto* threatPsqtWeights = &featureTransformer.threatPsqtWeights[0]; + + usize tileOffset = 0; + + while (tileOffset < Dimensions) + { + usize vl = __riscv_vsetvl_e16m8(Dimensions - tileOffset); + + vint16m8_t accum = __riscv_vle16_v_i16m8(&entry.accumulation[tileOffset], vl); + for (int i : removed) + accum = __riscv_vsub_vv_i16m8( + accum, __riscv_vle16_v_i16m8(&weights[i * Dimensions + tileOffset], vl), vl); + for (int i : added) + accum = __riscv_vadd_vv_i16m8( + accum, __riscv_vle16_v_i16m8(&weights[i * Dimensions + tileOffset], vl), vl); + + __riscv_vse16_v_i16m8(&entry.accumulation[tileOffset], accum, vl); + + for (int i : active) + accum = __riscv_vwadd_wv_i16m8( + accum, __riscv_vle8_v_i8m4(&threatWeights[i * Dimensions + tileOffset], vl), vl); + + __riscv_vse16_v_i16m8(&accumulator.accumulation[perspective][tileOffset], accum, vl); + + tileOffset += vl; + } + + tileOffset = 0; + + while (tileOffset < PSQTBuckets) + { + usize vl = __riscv_vsetvl_e32m1(PSQTBuckets - tileOffset); + + vint32m1_t accum = __riscv_vle32_v_i32m1(&entry.psqtAccumulation[tileOffset], vl); + for (int i : removed) + accum = __riscv_vsub_vv_i32m1( + accum, __riscv_vle32_v_i32m1(&psqtWeights[i * PSQTBuckets + tileOffset], vl), vl); + for (int i : added) + accum = __riscv_vadd_vv_i32m1( + accum, __riscv_vle32_v_i32m1(&psqtWeights[i * PSQTBuckets + tileOffset], vl), vl); + + __riscv_vse32_v_i32m1(&entry.psqtAccumulation[tileOffset], accum, vl); + + for (int i : active) + accum = __riscv_vadd_vv_i32m1( + accum, __riscv_vle32_v_i32m1(&threatPsqtWeights[i * PSQTBuckets + tileOffset], vl), + vl); + + __riscv_vse32_v_i32m1(&accumulator.psqtAccumulation[perspective][tileOffset], accum, vl); + + tileOffset += vl; + } + #else for (const auto index : removed) diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h index 53504632f..9e2c61d25 100644 --- a/src/nnue/nnue_feature_transformer.h +++ b/src/nnue/nnue_feature_transformer.h @@ -342,6 +342,40 @@ class FeatureTransformer { cursor.record2(packed[0], packed[1]); } +#elif defined(USE_RVV) + + usize j = 0; + + while (j < HalfDimensions / 2) + { + usize vl = __riscv_vsetvl_e16m8(HalfDimensions / 2 - j); + + vint16m8_t acc0 = __riscv_vle16_v_i16m8(&accumulation[perspectives[p]][j], vl); + vint16m8_t acc1 = + __riscv_vle16_v_i16m8(&accumulation[perspectives[p]][j + HalfDimensions / 2], vl); + + acc0 = __riscv_vmax_vx_i16m8(acc0, 0, vl); + acc1 = __riscv_vmax_vx_i16m8(acc1, 0, vl); + + vuint8m4_t pa = __riscv_vnclipu_wx_u8m4(__riscv_vreinterpret_v_i16m8_u16m8(acc0), 0, + __RISCV_VXRM_RDN, vl); + vuint8m4_t pb = __riscv_vnclipu_wx_u8m4(__riscv_vreinterpret_v_i16m8_u16m8(acc1), 0, + __RISCV_VXRM_RDN, vl); + + vuint8m4_t hi = __riscv_vmulhu_vv_u8m4(pa, pb, vl); + vuint8m4_t result = __riscv_vsrl_vx_u8m4(hi, 1, vl); + + __riscv_vse8_v_u8m4(&output[offset + j], result, vl); + j += vl; + + // Record NNZ + usize vl32 = vl / 4; + vbool8_t nnzMask = + __riscv_vmsne_vx_u32m4_b8(__riscv_vreinterpret_v_u8m4_u32m4(result), 0, vl32); + __riscv_vsm_v_b8(cursor.out, nnzMask, vl32); + cursor.out += vl32 / 8; + } + #else for (IndexType j = 0; j < HalfDimensions / 2; ++j) diff --git a/src/nnue/nnz_helper.h b/src/nnue/nnz_helper.h index 00202f524..7033ad54e 100644 --- a/src/nnue/nnz_helper.h +++ b/src/nnue/nnz_helper.h @@ -105,7 +105,7 @@ struct NNZInfo { NNZCursor make_cursor(bool perspective) { return {*this, perspective, count}; } #else // Each 8-bit chunk - u8 bitset[(Dimensions + 31) / 32]; + alignas(8) u8 bitset[(Dimensions + 31) / 32]; struct NNZCursor { u8* out; diff --git a/src/nnue/simd.h b/src/nnue/simd.h index d14a7454c..39e102fd3 100644 --- a/src/nnue/simd.h +++ b/src/nnue/simd.h @@ -40,6 +40,10 @@ #elif defined(USE_LSX) #include + +#elif defined(USE_RVV) + #include + #endif #include "../types.h" @@ -635,6 +639,35 @@ class SIMDTiling { static_assert(PSQTBuckets % PsqtTileHeight == 0, "PsqtTileHeight must divide PSQTBuckets"); #endif }; + +#if defined(USE_RVV) + + #define RVV_DEFINE_DPBUSD(A, W, H) \ + inline vint32##A##_t rvv_dpbusd_##A(vuint8##A##_t a, vint8##A##_t b, usize n) { \ + vint16##W##_t prod = __riscv_vwmulsu_vv_i16##W(b, a, 4 * n); \ + vuint32##W##_t prod32 = __riscv_vreinterpret_v_u16##W##_u32##W( \ + __riscv_vreinterpret_v_i16##W##_u16##W(prod)); \ + vint16##A##_t even = \ + __riscv_vreinterpret_v_u16##A##_i16##A(__riscv_vnsrl_wx_u16##A(prod32, 0, 2 * n)); \ + vint16##A##_t odd = \ + __riscv_vreinterpret_v_u16##A##_i16##A(__riscv_vnsrl_wx_u16##A(prod32, 16, 2 * n)); \ + vuint32##A##_t pairs = __riscv_vreinterpret_v_u16##A##_u32##A( \ + __riscv_vreinterpret_v_i16##A##_u16##A(__riscv_vadd_vv_i16##A(even, odd, 2 * n))); \ + vint16##H##_t lo = \ + __riscv_vreinterpret_v_u16##H##_i16##H(__riscv_vnsrl_wx_u16##H(pairs, 0, n)); \ + vint16##H##_t hi = \ + __riscv_vreinterpret_v_u16##H##_i16##H(__riscv_vnsrl_wx_u16##H(pairs, 16, n)); \ + return __riscv_vwadd_vv_i32##A(lo, hi, n); \ + } + +RVV_DEFINE_DPBUSD(m1, m2, mf2) +RVV_DEFINE_DPBUSD(m2, m4, m1) +RVV_DEFINE_DPBUSD(m4, m8, m2) + + #undef RVV_DEFINE_DPBUSD + +#endif + } #endif diff --git a/src/universal/entry_riscv64.cpp b/src/universal/entry_riscv64.cpp new file mode 100644 index 000000000..42d8e375a --- /dev/null +++ b/src/universal/entry_riscv64.cpp @@ -0,0 +1,96 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2026 The Stockfish developers (see AUTHORS file) + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include + +#ifdef __linux__ + #include + #include + #include +#else + #error This file is only supported on Linux +#endif + +#define DEFINE_BUILD(x) \ + namespace Stockfish_##x { \ + extern int main(int argc, char* argv[]); \ + } \ + extern "C" void (*__start_##x##_init[])(void); \ + extern "C" void (*__stop_##x##_init[])(void); \ + int entry_##x(int argc, char* argv[]) { \ + unsigned count = __stop_##x##_init - __start_##x##_init; \ + for (unsigned i = 0; i < count; i++) \ + __start_##x##_init[i](); \ + return Stockfish_##x::main(argc, argv); \ + } + +DEFINE_BUILD(riscv64) +DEFINE_BUILD(riscv64_rva23) + +struct CpuFeatures { + bool gcv; // GCV + bool zbitmanip; // Zba + Zbb + Zbs + bool zicond; // Zicond + int vlen; // vector length in bits (valid only if gcv is true), always a power of two +}; + +static CpuFeatures query_cpu_features() { + unsigned long hwcap = getauxval(AT_HWCAP); + constexpr long MASK_GCV = 0x20112d; + + bool gcv = (hwcap & MASK_GCV) == MASK_GCV; + int vlen = 0; + if (gcv) + { + unsigned long vlenb; + asm volatile("csrr %0, 0xc22" : "=r"(vlenb)); // 0xc22 = vlenb + vlen = int(vlenb * 8); + } + + constexpr long NR_riscv_hwprobe = 258; + constexpr int64_t KEY_IMA_EXT_0 = 4; + constexpr uint64_t EXT_ZB = 1 << 3 | 1 << 4 | 1 << 5; + constexpr uint64_t EXT_ZICOND = uint64_t(1) << 35; + + struct { + int64_t key; + uint64_t value; + } pair = {KEY_IMA_EXT_0, 0}; + + long rc = syscall(NR_riscv_hwprobe, &pair, 1UL, 0UL, nullptr, 0U); + bool zbitmanip = false, zicond = false; + + if (rc == 0 && pair.key == KEY_IMA_EXT_0) + { + zbitmanip = (pair.value & EXT_ZB) == EXT_ZB; + zicond = (pair.value & EXT_ZICOND) == EXT_ZICOND; + } + + return CpuFeatures{.gcv = gcv, .zbitmanip = zbitmanip, .zicond = zicond, .vlen = vlen}; +} + + +// Selects the most capable ISA variant supported by this CPU +static int dispatch(const CpuFeatures& f, int argc, char* argv[]) { + if (!f.gcv || !f.zbitmanip || !f.zicond || f.vlen < 128) + return entry_riscv64(argc, argv); + + return entry_riscv64_rva23(argc, argv); +} + +int main(int argc, char* argv[]) { + CpuFeatures f = query_cpu_features(); + return dispatch(f, argc, argv); +}