[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
This commit is contained in:
anematode
2026-07-03 20:29:50 +02:00
committed by Joost VandeVondele
parent 99489f57dd
commit d5bbc6b67d
16 changed files with 617 additions and 37 deletions
+31 -15
View File
@@ -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/<arch>/.
$(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