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); +}