diff --git a/.github/workflows/stockfish.yml b/.github/workflows/stockfish.yml index 0606d685c..2608f0bf6 100644 --- a/.github/workflows/stockfish.yml +++ b/.github/workflows/stockfish.yml @@ -150,6 +150,10 @@ jobs: name: Compiler check if: ${{ always() }} uses: ./.github/workflows/avx2_compilers.yml + WASMCompilation: + name: WASM compilation + if: ${{ always() }} + uses: ./.github/workflows/wasm_compilation.yml # Release Jobs ARMCompilation: name: Android builds diff --git a/.github/workflows/wasm_compilation.yml b/.github/workflows/wasm_compilation.yml new file mode 100644 index 000000000..12f23c683 --- /dev/null +++ b/.github/workflows/wasm_compilation.yml @@ -0,0 +1,51 @@ +name: WASM Compilation + +on: + workflow_call: + +jobs: + wasm: + name: wasm (${{ matrix.arch }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + arch: [wasm32, wasm32-relaxed-simd] + defaults: + run: + working-directory: src + shell: bash + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install emsdk + working-directory: . + run: | + git clone --depth 1 https://github.com/emscripten-core/emsdk.git + cd emsdk + ./emsdk install latest + ./emsdk activate latest + + - 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: Download the used network from the fishtest framework + run: make net + + - name: Build ${{ matrix.arch }} build + run: | + source $GITHUB_WORKSPACE/emsdk/emsdk_env.sh + em++ --version + net=$(grep -oE 'nn-[0-9a-f]+\.nnue' evaluate.h | head -1) + make clean + COMPCXX=em++ make -j build ARCH=${{ matrix.arch }} EXTRALDFLAGS="--preload-file $net" + + - name: Verify bench under node + run: EXE=./stockfish.js RUN_PREFIX=node ../tests/signature.sh $benchref diff --git a/src/Makefile b/src/Makefile index ab91d1d0f..cdf0ee1a8 100644 --- a/src/Makefile +++ b/src/Makefile @@ -55,6 +55,8 @@ endif ### Executable name ifeq ($(target_windows),yes) EXE = stockfish.exe +else ifneq (,$(findstring wasm,$(ARCH))) + EXE = stockfish.js else EXE = stockfish endif @@ -122,7 +124,9 @@ VPATH = syzygy:nnue:nnue/features # neon = yes/no --- -DUSE_NEON --- Use ARM SIMD architecture # dotprod = yes/no --- -DUSE_NEON_DOTPROD --- Use ARM advanced SIMD Int8 dot product instructions # lsx = yes/no --- -mlsx --- Use Loongson SIMD eXtension -# lasx = yes/no --- -mlasx --- use Loongson Advanced SIMD eXtension +# lasx = yes/no --- -mlasx --- Use Loongson Advanced SIMD eXtension +# relaxedsimd = y/n --- -mrelaxed-simd --- Use WebAssembly relaxed SIMD extension +# syzygy = yes/no --- -DNO_TABLEBASES --- Support Syzygy tablebase probing # # Note that Makefile is space sensitive, so when adding new architectures # or modifying existing flags, you have to make sure there are no extra spaces @@ -149,7 +153,7 @@ 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)) + loongarch64 loongarch64-lsx loongarch64-lasx wasm32 wasm32-relaxed-simd)) SUPPORTED_ARCH=true else SUPPORTED_ARCH=false @@ -186,6 +190,8 @@ dotprod = no arm_version = 0 lsx = no lasx = no +relaxedsimd = no +syzygy = yes STRIP = strip ifneq ($(shell which clang-format-20 2> /dev/null),) @@ -389,6 +395,27 @@ ifeq ($(ARCH),apple-silicon) arm_version = 8 endif +# WASM is largely compiled using emcc's x86 compatibility layer, with occasional +# WASM-specific intrinsics +ifeq ($(ARCH),wasm32) + arch = wasm32 + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + syzygy = no +endif + +ifeq ($(ARCH),wasm32-relaxed-simd) + arch = wasm32 + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + relaxedsimd = yes + syzygy = no +endif + ifeq ($(ARCH),ppc-32) arch = ppc bits = 32 @@ -480,7 +507,7 @@ ifeq ($(COMP),gcc) endif else ifeq ($(arch),loongarch64) CXXFLAGS += -latomic - else + else ifneq ($(arch),wasm32) CXXFLAGS += -m$(bits) LDFLAGS += -m$(bits) endif @@ -490,8 +517,10 @@ ifeq ($(COMP),gcc) endif ifneq ($(KERNEL),Darwin) + ifneq ($(arch),wasm32) LDFLAGS += -Wl,--no-as-needed endif + endif endif ifeq ($(target_windows),yes) @@ -654,6 +683,7 @@ ifneq ($(comp),mingw) # Haiku has pthreads in its libroot, so only link it in on other platforms ifneq ($(KERNEL),Haiku) ifneq ($(COMP),ndk) + ifneq ($(arch),wasm32) LDFLAGS += -lpthread add_lrt = yes @@ -669,6 +699,7 @@ ifneq ($(comp),mingw) LDFLAGS += -lrt endif endif + endif endif endif endif @@ -846,6 +877,14 @@ ifeq ($(lsx),yes) endif endif +ifeq ($(arch),wasm32) + CXXFLAGS += -pthread -msimd128 -DUSE_POPCNT -DNNUE_EMBEDDING_OFF + LDFLAGS += -pthread -sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=3MB + ifeq ($(relaxedsimd),yes) + CXXFLAGS += -mrelaxed-simd + endif +endif + ### 3.7 pext ifeq ($(pext),yes) CXXFLAGS += -DUSE_PEXT @@ -854,6 +893,11 @@ ifeq ($(pext),yes) endif endif +### Syzygy tablebases +ifeq ($(syzygy),no) + CXXFLAGS += -DNO_TABLEBASES +endif + ### 3.8.1 Try to include git info for versioning and avoid recompiles if nothing changes BUILD_SHA_FILE := .build_sha.txt BUILD_DATE_FILE := .build_date.txt @@ -885,7 +929,8 @@ endif ### needs access to the optimization flags. ifeq ($(optimize),yes) ifeq ($(debug),no) - ifneq ($(KERNEL),Darwin) + ifeq ($(arch),wasm32) + else ifneq ($(KERNEL),Darwin) LLD_BIN := $(shell command -v ld.lld 2>/dev/null) ifeq ($(LLD_BIN),) LLD_BIN := $(shell command -v lld 2>/dev/null) @@ -1207,6 +1252,7 @@ config-sanity: net echo "arm_version: '$(arm_version)'" && \ echo "lsx: '$(lsx)'" && \ echo "lasx: '$(lasx)'" && \ + echo "syzygy: '$(syzygy)'" && \ echo "target_windows: '$(target_windows)'" && \ echo "" && \ echo "Flags:" && \ @@ -1222,7 +1268,8 @@ config-sanity: net (test "$(arch)" = "any" || test "$(arch)" = "x86_64" || test "$(arch)" = "i386" || \ test "$(arch)" = "ppc64" || test "$(arch)" = "ppc" || test "$(arch)" = "e2k" || \ test "$(arch)" = "armv7" || test "$(arch)" = "armv8" || test "$(arch)" = "arm64" || \ - test "$(arch)" = "riscv64" || test "$(arch)" = "loongarch64") && \ + test "$(arch)" = "riscv64" || test "$(arch)" = "loongarch64" || \ + test "$(arch)" = "wasm32") && \ (test "$(bits)" = "32" || test "$(bits)" = "64") && \ (test "$(prefetch)" = "yes" || test "$(prefetch)" = "no") && \ (test "$(popcnt)" = "yes" || test "$(popcnt)" = "no") && \ @@ -1241,6 +1288,7 @@ config-sanity: net (test "$(neon)" = "yes" || test "$(neon)" = "no") && \ (test "$(lsx)" = "yes" || test "$(lsx)" = "no") && \ (test "$(lasx)" = "yes" || test "$(lasx)" = "no") && \ + (test "$(syzygy)" = "yes" || test "$(syzygy)" = "no") && \ (test "$(comp)" = "gcc" || test "$(comp)" = "icx" || test "$(comp)" = "mingw" || \ test "$(comp)" = "clang" || test "$(comp)" = "armv7a-linux-androideabi16-clang" || \ test "$(comp)" = "aarch64-linux-android21-clang") diff --git a/src/history.h b/src/history.h index 7ba43234d..4e0dbeb03 100644 --- a/src/history.h +++ b/src/history.h @@ -52,11 +52,17 @@ static_assert((CORRHIST_BASE_SIZE & (CORRHIST_BASE_SIZE - 1)) == 0, // the entry. The first template parameter T is the base type of the array, // and the second template parameter D limits the range of updates in [-D, D] // when we update values with the << operator -template +template struct StatsEntry { static_assert(std::is_arithmetic_v, "Not an arithmetic type"); private: + static constexpr bool Atomic = +#ifdef __wasm__ + false; // don't use atomics on WASM as they are always seq_cst +#else + Shared; +#endif std::conditional_t, T> entry; public: diff --git a/src/misc.h b/src/misc.h index cadff28cc..219e75ab8 100644 --- a/src/misc.h +++ b/src/misc.h @@ -384,12 +384,12 @@ class PRNG { } }; -inline u64 mul_hi64(u64 a, u64 b) { -#if defined(__GNUC__) && defined(IS_64BIT) +inline usize mul_hi64(u64 a, usize b) { +#if defined(__GNUC__) && defined(IS_64BIT) && !defined(__wasm__) return (u128(a) * u128(b)) >> 64; #else u64 aL = u32(a), aH = a >> 32; - u64 bL = u32(b), bH = b >> 32; + u64 bL = u32(b), bH = u64(b) >> 32; u64 c1 = (aL * bL) >> 32; u64 c2 = aH * bL + c1; u64 c3 = aL * bH + u32(c2); diff --git a/src/nnue/nnue_accumulator.cpp b/src/nnue/nnue_accumulator.cpp index 35040b2b8..46ebe5db4 100644 --- a/src/nnue/nnue_accumulator.cpp +++ b/src/nnue/nnue_accumulator.cpp @@ -561,6 +561,19 @@ Bitboard get_changed_pieces(const std::array& oldPieces, uint8x8_t sameBB = vshrn_n_u16(vreinterpretq_u16_u8(merged), 4); return ~vget_lane_u64(vreinterpret_u64_u8(sameBB), 0); +#elif defined(USE_SSE2) + Bitboard sameBB = 0; + + for (int i = 0; i < 64; i += 16) + { + const __m128i old_v = _mm_loadu_si128(reinterpret_cast(&oldPieces[i])); + const __m128i new_v = _mm_loadu_si128(reinterpret_cast(&newPieces[i])); + const __m128i same = _mm_cmpeq_epi8(old_v, new_v); + + sameBB |= static_cast(_mm_movemask_epi8(same)) << i; + } + + return ~sameBB; #else Bitboard changed = 0; diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h index c4c92f059..12f620648 100644 --- a/src/nnue/nnue_feature_transformer.h +++ b/src/nnue/nnue_feature_transformer.h @@ -233,11 +233,9 @@ class FeatureTransformer { static_assert((HalfDimensions / 2) % OutputChunkSize == 0); constexpr IndexType NumOutputChunks = HalfDimensions / 2 / OutputChunkSize; - #if !defined(USE_NEON) - const vec_t Zero = vec_zero(); - const vec_t FtMax = vec_set_16(FtMaxVal); - constexpr int shift = 7; - #endif + [[maybe_unused]] const vec_t Zero = vec_zero(); + [[maybe_unused]] const vec_t FtMax = vec_set_16(FtMaxVal); + [[maybe_unused]] constexpr int shift = 7; const vec_t* in0 = reinterpret_cast(&(accumulation[perspectives[p]][0])); const vec_t* in1 = @@ -323,6 +321,19 @@ class FeatureTransformer { vec_t hi = vec_mulhi_8(pa, pb); vec_t result = vec_srli_8(hi, 1); + #elif defined(__wasm__) + // _mm_mulhi_epi16 is lowered to 32-bit multiplies, so we take + // a similar approach as the NEON path. + vec_t mul0 = vec_packus_16(acc0a, acc0b); + vec_t mul1 = vec_packus_16(acc1a, acc1b); + + vec_t low = wasm_u16x8_extmul_low_u8x16(mul0, mul1); + vec_t hi = wasm_u16x8_extmul_high_u8x16(mul0, mul1); + + // equivalent to vuzp2_u8 + vec_t merged = wasm_i8x16_shuffle(low, hi, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, + 21, 23, 25, 27, 29, 31); + vec_t result = wasm_u8x16_shr(merged, 1); #else vec_t sum0a = vec_slli_16(vec_max_16(vec_min_16(acc0a, FtMax), Zero), shift); vec_t sum0b = vec_slli_16(vec_max_16(vec_min_16(acc0b, FtMax), Zero), shift); diff --git a/src/nnue/nnz_helper.h b/src/nnue/nnz_helper.h index 7bbe98437..b7542c7bc 100644 --- a/src/nnue/nnz_helper.h +++ b/src/nnue/nnz_helper.h @@ -132,6 +132,10 @@ struct NNZInfo { const uint16x8_t bits = vandq_u16(packed, vld1q_u16(Mask8)); *out++ = vaddvq_u16(bits); + #elif defined(__wasm__) + __m128i packed = _mm_packus_epi32(neurons1, neurons2); + packed = _mm_packs_epi16(packed, packed); + *out++ = ~_mm_movemask_epi8(_mm_cmpeq_epi8(packed, _mm_setzero_si128())); #else auto m1 = vec_nnz(neurons1); auto m2 = vec_nnz(neurons2); diff --git a/src/nnue/simd.h b/src/nnue/simd.h index fca5e260a..bf6c78732 100644 --- a/src/nnue/simd.h +++ b/src/nnue/simd.h @@ -168,7 +168,12 @@ inline __m128i _mm_cvtsi64_si128(i64 val) { #endif #ifdef USE_SSE41 - #define vec_convert_8_16(a) _mm_cvtepi8_epi16(_mm_cvtsi64_si128(static_cast(a))) + #ifdef __wasm__ + #define vec_convert_8_16(a) wasm_i16x8_load8x8(reinterpret_cast(&a)) + #else + #define vec_convert_8_16(a) \ + _mm_cvtepi8_epi16(_mm_cvtsi64_si128(static_cast(a))) + #endif #else // Credit: Yoshie2000 inline __m128i vec_convert_8_16(u64 x) { @@ -495,10 +500,13 @@ fused(const typename VecWrapper::type& in, const T& operand, const Ts&... operan } [[maybe_unused]] static void m128_add_dpbusd_epi32(__m128i& acc, __m128i a, __m128i b) { - + #if defined(__wasm_relaxed_simd__) + acc = wasm_i32x4_relaxed_dot_i8x16_i7x16_add(b, a, acc); + #else __m128i product0 = _mm_maddubs_epi16(a, b); product0 = _mm_madd_epi16(product0, _mm_set1_epi16(1)); acc = _mm_add_epi32(acc, product0); + #endif } #endif diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index b230ac59b..21d619572 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -59,6 +59,40 @@ #include #endif +#ifdef NO_TABLEBASES + +// Stubbed out impls +namespace Stockfish::Tablebases { + +int MaxCardinality; + +void init(const std::string&) {} + +WDLScore probe_wdl(Position&, ProbeState* result) { + *result = FAIL; + return WDLDraw; +} + +int probe_dtz(Position&, ProbeState* result) { + *result = FAIL; + return 0; +} + +bool root_probe(Position&, Search::RootMoves&, bool, bool, const std::function&) { + return false; +} + +bool root_probe_wdl(Position&, Search::RootMoves&, bool) { return false; } + +Config rank_root_moves( + const OptionsMap&, Position&, Search::RootMoves&, bool, const std::function&) { + return Config{}; +} + +} // namespace Stockfish::Tablebases + +#else + using namespace Stockfish::Tablebases; int Stockfish::Tablebases::MaxCardinality; @@ -206,11 +240,11 @@ class TBFile: public std::ifstream { TBFile(const std::string& f) { -#ifndef _WIN32 + #ifndef _WIN32 constexpr char SepChar = ':'; -#else + #else constexpr char SepChar = ';'; -#endif + #endif std::stringstream ss(Paths); std::string path; @@ -228,7 +262,7 @@ class TBFile: public std::ifstream { if (is_open()) close(); // Need to re-open to get native file descriptor -#ifndef _WIN32 + #ifndef _WIN32 struct stat statbuf; int fd = ::open(fname.c_str(), O_RDONLY); @@ -245,9 +279,9 @@ class TBFile: public std::ifstream { *mapping = statbuf.st_size; *baseAddress = mmap(nullptr, statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0); - #if defined(MADV_RANDOM) + #if defined(MADV_RANDOM) madvise(*baseAddress, statbuf.st_size, MADV_RANDOM); - #endif + #endif ::close(fd); if (*baseAddress == MAP_FAILED) @@ -255,7 +289,7 @@ class TBFile: public std::ifstream { std::cerr << "Could not mmap() " << fname << std::endl; exit(EXIT_FAILURE); } -#else + #else // Note FILE_FLAG_RANDOM_ACCESS is only a hint to Windows and as such may get ignored. HANDLE fd = CreateFileA(fname.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_RANDOM_ACCESS, nullptr); @@ -290,7 +324,7 @@ class TBFile: public std::ifstream { << ", error = " << GetLastError() << std::endl; exit(EXIT_FAILURE); } -#endif + #endif u8* data = (u8*) *baseAddress; constexpr u8 Magics[][4] = {{0xD7, 0x66, 0x0C, 0xA5}, {0x71, 0xE8, 0x23, 0x5D}}; @@ -307,12 +341,12 @@ class TBFile: public std::ifstream { static void unmap(void* baseAddress, u64 mapping) { -#ifndef _WIN32 + #ifndef _WIN32 munmap(baseAddress, mapping); -#else + #else UnmapViewOfFile(baseAddress); CloseHandle((HANDLE) mapping); -#endif + #endif } }; @@ -728,12 +762,12 @@ int map_score(TBTable* entry, File f, int value, WDLScore wdl) { return value + 1; } -// A temporary fix for the compiler bug with vectorization. (#4450) -#if defined(__clang__) && defined(__clang_major__) && __clang_major__ >= 15 - #define DISABLE_CLANG_LOOP_VEC _Pragma("clang loop vectorize(disable)") -#else - #define DISABLE_CLANG_LOOP_VEC -#endif + // A temporary fix for the compiler bug with vectorization. (#4450) + #if defined(__clang__) && defined(__clang_major__) && __clang_major__ >= 15 + #define DISABLE_CLANG_LOOP_VEC _Pragma("clang loop vectorize(disable)") + #else + #define DISABLE_CLANG_LOOP_VEC + #endif // Compute a unique index out of a position and use it to probe the TB file. To // encode k pieces of the same type and color, first sort the pieces by square in @@ -1802,3 +1836,5 @@ Config Tablebases::rank_root_moves(const OptionsMap& options, return config; } } // namespace Stockfish + +#endif // NO_TABLEBASES diff --git a/tests/signature.sh b/tests/signature.sh index ef781a0f4..ad96d2ee3 100755 --- a/tests/signature.sh +++ b/tests/signature.sh @@ -18,7 +18,8 @@ error() trap 'error ${LINENO}' ERR # obtain -eval "$RUN_PREFIX ./stockfish bench" > "$STDOUT_FILE" 2> "$STDERR_FILE" || error ${LINENO} +EXE=${EXE:-./stockfish} +eval "$RUN_PREFIX $EXE bench" > "$STDOUT_FILE" 2> "$STDERR_FILE" || error ${LINENO} signature=$(grep "Nodes searched : " "$STDERR_FILE" | awk '{print $4}') rm -f "$STDOUT_FILE" "$STDERR_FILE"