diff --git a/CMakeLists.txt b/CMakeLists.txt index 9daec5f1e6..dab5632bc9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,8 @@ option(DYNAMIC_OLDER "Include specific support for older x86 cpu models (Penryn, option(BUILD_RELAPACK "Build with ReLAPACK (recursive implementation of several LAPACK functions on top of standard LAPACK)" OFF) +option(WASM_RELAXED_SIMD "WASM: emit relaxed SIMD opcodes (not portable to engines without the feature, including shipping Safari)" OFF) + option(USE_LOCKING "Use locks even in single-threaded builds to make them callable from multiple threads" OFF) option(USE_PERL "Use the older PERL scripts for build preparation instead of universal shell scripts" OFF) diff --git a/Makefile.rule b/Makefile.rule index 664a3d8036..5c843cfedd 100644 --- a/Makefile.rule +++ b/Makefile.rule @@ -311,6 +311,14 @@ COMMON_PROF = -pg # If you want to enable the experimental HFLOAT16 support # BUILD_HFLOAT16 = 1 +# WASM only: emit WebAssembly relaxed SIMD (for example f32x4.relaxed_madd) +# in kernels that support it. Off by default because a module that contains +# those opcodes will not instantiate on engines without the relaxed-simd +# feature, including shipping Safari. See docs/install.md#webassembly and +# https://webassembly.org/features/ +# WASM_RELAXED_SIMD = 1 + + # Set the thread number threshold beyond which the job array for the threaded level3 BLAS # will be allocated on the heap rather than the stack. (This array alone requires # NUM_THREADS*NUM_THREADS*128 bytes of memory so should not pose a problem at low cpu diff --git a/Makefile.wasm b/Makefile.wasm index feca75ed4c..fa144e8a37 100644 --- a/Makefile.wasm +++ b/Makefile.wasm @@ -1 +1,9 @@ -CCOMMON_OPT += -msimd128 -mrelaxed-simd +# Portable WASM SIMD128. Do not pass -mrelaxed-simd by default: the resulting +# module contains opcodes that engines without the relaxed-simd feature reject +# at instantiate time (shipping Safari / JavaScriptCore among them). Engine +# support is tracked at https://webassembly.org/features/ +# Opt in with WASM_RELAXED_SIMD=1 (see Makefile.rule and docs/install.md). +CCOMMON_OPT += -msimd128 +ifeq ($(WASM_RELAXED_SIMD), 1) +CCOMMON_OPT += -mrelaxed-simd +endif diff --git a/README.md b/README.md index 31c2bd0b4f..d651c42b36 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,9 @@ e.g.: - **WASM128_GENERIC**: Optimized SGEMM,DGEMM, DAXPY, SSUM/DSUM, SDOT/DDOT and SROT/DROT + Builds target [WASM SIMD128](https://github.com/WebAssembly/simd) (`-msimd128`) by default. Relaxed SIMD is **not** enabled globally: a module that contains those opcodes fails to instantiate on engines that do not implement the proposal (notably shipping Safari / JavaScriptCore). Engine support is listed at [webassembly.org/features](https://webassembly.org/features/). Pass `WASM_RELAXED_SIMD=1` (or `-DWASM_RELAXED_SIMD=ON` with CMake) to opt in. Details: [docs/install.md](docs/install.md#webassembly). + + ### Support for multiple targets in a single library diff --git a/cmake/arch.cmake b/cmake/arch.cmake index b902937a05..12d7de54e7 100644 --- a/cmake/arch.cmake +++ b/cmake/arch.cmake @@ -175,3 +175,11 @@ if (RISCV64) set(BINARY_DEFINED 1) endif () +if (WASM) + set(CCOMMON_OPT "${CCOMMON_OPT} -msimd128") + if (WASM_RELAXED_SIMD) + set(CCOMMON_OPT "${CCOMMON_OPT} -mrelaxed-simd") + endif () +endif () + + diff --git a/docs/build_system.md b/docs/build_system.md index ad0c77e89c..9b95a5e678 100644 --- a/docs/build_system.md +++ b/docs/build_system.md @@ -73,6 +73,10 @@ though - please read the linked Makefiles if you want to see all variables. - `FCOMMON_OPT`: flags to add to all invocations of the target Fortran compiler (overrides `FFLAGS`) - `LDFLAGS`: flags to add to all target linker invocations +- `WASM_RELAXED_SIMD`: WASM only; if set to `1`, pass `-mrelaxed-simd` so + kernels can emit relaxed SIMD FMA. Off by default because the binary will + not load on engines without the feature (see + [install.md](install.md#webassembly)). - `AR`, `AS`, `LD`, `RANLIB`: `TARGET` toolchain helpers used for compilation (can be cross-toolchains). - `HOSTCC`: compiler of build machine, needed to create proper config files for diff --git a/docs/install.md b/docs/install.md index b842595494..edd539918f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -847,6 +847,62 @@ riscv64-linux-gnu-objdump -d libopenblas*.a | \ returns approximately 12,000-14,000 (GCC 14: ~12,691; GCC 15: ~14,355). +### WebAssembly + +OpenBLAS can be cross-compiled with [Emscripten](https://emscripten.org/) +(`TARGET=WASM128_GENERIC`). `Makefile.wasm` always passes `-msimd128` so +kernels can use the portable [WASM SIMD128](https://github.com/WebAssembly/simd) +instruction set (128-bit `v128` vectors). + +#### Relaxed SIMD + +[Relaxed SIMD](https://github.com/WebAssembly/relaxed-simd) is a follow-on +proposal. It keeps the same 128-bit vectors but allows a few operations — +in particular fused multiply-add (`f32x4.relaxed_madd` / +`f64x2.relaxed_madd`) — to map to native hardware FMA. The result may use +one rounding or two, depending on the CPU, so it is not bit-identical across +engines. + +OpenBLAS kernels already select `relaxed_madd` when the compiler defines +`__wasm_relaxed_simd__` (from `-mrelaxed-simd`), and fall back to a separate +multiply then add otherwise. That compile-time split is not enough for a +single portable binary: WebAssembly validates the whole module, so any +relaxed-SIMD opcode causes instantiate to fail on an engine that does not +implement the feature. There is no in-module runtime dispatch. + +Engine support is tracked at [webassembly.org/features](https://webassembly.org/features/). +As of 2026, shipping Safari / JavaScriptCore still lacks relaxed SIMD +(Safari Technology Preview has it; a page cannot enable the JSC flag +`useWasmRelaxedSIMD` itself). Chrome/V8 and current Firefox do support it. + +For a library that must load in every current browser, leave relaxed SIMD +**off** (the default). Opt in only when every target engine is known to +support it, or when you ship a second binary and select it from JavaScript +with [wasm-feature-detect](https://github.com/GoogleChromeLabs/wasm-feature-detect). + +```bash +# Portable default (SIMD128 only) +make TARGET=WASM128_GENERIC \ + HOSTCC=gcc CC=emcc AR=emar RANLIB=emranlib \ + USE_THREAD=0 NOFORTRAN=1 + +# Relaxed SIMD (FMA); will not instantiate on engines without the feature +make TARGET=WASM128_GENERIC WASM_RELAXED_SIMD=1 \ + HOSTCC=gcc CC=emcc AR=emar RANLIB=emranlib \ + USE_THREAD=0 NOFORTRAN=1 +``` + +With CMake, pass `-DWASM_RELAXED_SIMD=ON`. + +After the tree is configured for WASM (`ARCH=wasm` in `Makefile.conf`), check +that the default build stays SIMD128-only and that the opt-in path emits +`relaxed_madd`: + +```bash +./kernel/wasm/test_relaxed_simd.sh +``` + + ### FreeBSD You will need to install the following tools from the FreeBSD ports tree: diff --git a/kernel/wasm/test_relaxed_simd.sh b/kernel/wasm/test_relaxed_simd.sh new file mode 100755 index 0000000000..41fb732b3e --- /dev/null +++ b/kernel/wasm/test_relaxed_simd.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env bash +# Verify WASM relaxed SIMD is opt-in: +# * the default compile does not pass -mrelaxed-simd and emits no relaxed_madd +# * WASM_RELAXED_SIMD=1 passes the flag and kernels use relaxed_madd +# +# Requires a WASM-configured tree (ARCH=wasm in Makefile.conf), emcc, and +# wasm-dis (Binaryen, shipped with Emscripten). Node is used to instantiate +# the default module when available. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}" +FAILS=0 + +activate_emscripten() { + if command -v emcc >/dev/null 2>&1; then + return 0 + fi + local prefix="${OPENBLAS_EM_PREFIX:-}" + if [[ -z "$prefix" && -n "${CONDA_PREFIX:-}" && -f "${CONDA_PREFIX}/bin/activate_emscripten.sh" ]]; then + prefix="$CONDA_PREFIX" + fi + if [[ -z "$prefix" && -n "${EMSDK:-}" && -x "${EMSDK}/upstream/emscripten/emcc" ]]; then + export PATH="${EMSDK}/upstream/emscripten:${EMSDK}/upstream/bin:${PATH}" + return 0 + fi + if [[ -z "$prefix" && -f "$ROOT/.em-prefix/bin/activate_emscripten.sh" ]]; then + prefix="$ROOT/.em-prefix" + fi + if [[ -z "$prefix" || ! -f "$prefix/bin/activate_emscripten.sh" ]]; then + echo "emcc not found. Set OPENBLAS_EM_PREFIX to an Emscripten prefix." >&2 + exit 1 + fi + export CONDA_PREFIX="$prefix" + export PREFIX="${PREFIX:-$prefix}" + export PATH="$prefix/bin:$prefix/opt/emsdk/upstream/emscripten:$prefix/opt/emsdk/upstream/bin:$PATH" + # shellcheck disable=SC1091 + source "$prefix/bin/activate_emscripten.sh" +} + +count_op() { + # wasm-dis prints a warning on relocatable objects; keep stdout only. + local n + n="$(wasm-dis "$1" 2>/dev/null | grep -c "$2" || true)" + echo "${n:-0}" +} + +expect() { + local name="$1" got="$2" pred="$3" want="$4" + if [[ "$pred" == "eq" && "$got" == "$want" ]]; then + echo "PASS $name ($got)" + return 0 + fi + if [[ "$pred" == "gt" && "$got" -gt "$want" ]]; then + echo "PASS $name ($got > $want)" + return 0 + fi + echo "FAIL $name (got $got, expected $pred $want)" >&2 + FAILS=$((FAILS + 1)) +} + +activate_emscripten +cd "$ROOT" + +if ! command -v wasm-dis >/dev/null 2>&1; then + echo "wasm-dis not found (Binaryen). It is usually on PATH after activating Emscripten." >&2 + exit 1 +fi + +if [[ ! -f Makefile.conf ]] || ! grep -q '^ARCH=wasm$' Makefile.conf; then + echo "Makefile.conf is not a WASM config (need ARCH=wasm)." >&2 + echo "Configure first, for example:" >&2 + echo " make TARGET=WASM128_GENERIC HOSTCC=gcc CC=emcc AR=emar RANLIB=emranlib USE_THREAD=0 NOFORTRAN=1 NO_LAPACK=1" >&2 + exit 1 +fi + +MAKE_COMMON=( + HOSTCC=gcc + CC=emcc + AR=emar + RANLIB=emranlib + TARGET=WASM128_GENERIC + USE_THREAD=0 + NOFORTRAN=1 + NO_LAPACK=1 + COLORCODE=0 +) + +# Kernels that emit madd when __wasm_relaxed_simd__ is set. +OBJS=( + sgemm_kernel.o + dgemm_kernel.o + cgemm_kernel_n.o + zgemm_kernel_n.o + sdot_k.o + ddot_k.o + dsdot_k.o + srot_k.o + dtrmm_kernel_LN.o +) + +EXPORTS=_sgemm_kernel,_dgemm_kernel,_cgemm_kernel_n,_zgemm_kernel_n,_sdot_k,_ddot_k,_dsdot_k,_srot_k,_dtrmm_kernel_LN + +MAKE_BIN="${MAKE:-make}" + +sgemm_line() { + local extra=("$@") + rm -f kernel/sgemm_kernel.o + # GNU make -n prints the compile line only when the target is out of date. + "$MAKE_BIN" -C kernel -n sgemm_kernel.o "${MAKE_COMMON[@]}" "${extra[@]}" 2>/dev/null \ + | grep -E '(^|[[:space:]])emcc[[:space:]]' | tail -1 || true +} + +echo "==> compile-line flags" +DEFAULT_LINE="$(sgemm_line)" +RELAXED_LINE="$(sgemm_line WASM_RELAXED_SIMD=1)" +echo " default: $DEFAULT_LINE" +echo " relaxed: $RELAXED_LINE" +if [[ -z "$DEFAULT_LINE" || -z "$RELAXED_LINE" ]]; then + echo "FAIL could not extract an emcc compile line from make -n" >&2 + exit 1 +fi + +case "$DEFAULT_LINE" in + *-msimd128*) expect "default passes -msimd128" 1 eq 1 ;; + *) expect "default passes -msimd128" 0 eq 1 ;; +esac +case "$DEFAULT_LINE" in + *-mrelaxed-simd*) expect "default omits -mrelaxed-simd" 1 eq 0 ;; + *) expect "default omits -mrelaxed-simd" 0 eq 0 ;; +esac +case "$RELAXED_LINE" in + *-mrelaxed-simd*) expect "WASM_RELAXED_SIMD=1 passes -mrelaxed-simd" 1 eq 1 ;; + *) expect "WASM_RELAXED_SIMD=1 passes -mrelaxed-simd" 0 eq 1 ;; +esac + +OUT="$(mktemp -d "${TMPDIR:-/tmp}/openblas-relaxed-simd.XXXXXX")" +trap 'rm -rf "$OUT"' EXIT + +build_variant() { + local dest="$1" + shift + rm -f "${OBJS[@]/#/kernel/}" + "$MAKE_BIN" -C kernel -j"$JOBS" "${OBJS[@]}" "${MAKE_COMMON[@]}" "$@" + mkdir -p "$dest" + local o + for o in "${OBJS[@]}"; do + cp "kernel/$o" "$dest/" + done +} + +echo "==> compile default kernels" +build_variant "$OUT/default" + +echo "==> compile WASM_RELAXED_SIMD=1 kernels" +build_variant "$OUT/relaxed" WASM_RELAXED_SIMD=1 + +sum_op() { + local dir="$1" op="$2" total=0 n + local o + for o in "$dir"/*.o; do + n="$(count_op "$o" "$op")" + total=$((total + n)) + done + echo "$total" +} + +DEFAULT_MADD="$(sum_op "$OUT/default" 'relaxed_madd')" +RELAXED_MADD="$(sum_op "$OUT/relaxed" 'relaxed_madd')" +DEFAULT_MUL="$(sum_op "$OUT/default" 'f32x4.mul')" +RELAXED_MUL="$(sum_op "$OUT/relaxed" 'f32x4.mul')" + +echo "==> opcode counts (wasm-dis)" +echo " default relaxed_madd=$DEFAULT_MADD f32x4.mul=$DEFAULT_MUL" +echo " relaxed relaxed_madd=$RELAXED_MADD f32x4.mul=$RELAXED_MUL" + +expect "default objects have no relaxed_madd" "$DEFAULT_MADD" eq 0 +expect "WASM_RELAXED_SIMD=1 objects use relaxed_madd" "$RELAXED_MADD" gt 0 +expect "default objects still use f32x4.mul" "$DEFAULT_MUL" gt 0 + +link_wasm() { + local dir="$1" + local extra=() + if [[ "${2:-}" == "relaxed" ]]; then + extra+=(-mrelaxed-simd) + fi + emcc -O2 -msimd128 "${extra[@]}" --no-entry \ + -sEXPORTED_FUNCTIONS="$EXPORTS" \ + -sERROR_ON_UNDEFINED_SYMBOLS=0 \ + -sSTANDALONE_WASM=1 \ + "$dir"/*.o -o "$dir/kernels.wasm" +} + +echo "==> link and instantiate" +link_wasm "$OUT/default" +link_wasm "$OUT/relaxed" relaxed + +LINKED_DEFAULT_MADD="$(count_op "$OUT/default/kernels.wasm" 'relaxed_madd')" +LINKED_RELAXED_MADD="$(count_op "$OUT/relaxed/kernels.wasm" 'relaxed_madd')" +expect "linked default module has no relaxed_madd" "$LINKED_DEFAULT_MADD" eq 0 +expect "linked WASM_RELAXED_SIMD=1 module has relaxed_madd" "$LINKED_RELAXED_MADD" gt 0 + +if command -v node >/dev/null 2>&1; then + node -e ' +const fs = require("fs"); +const defb = fs.readFileSync(process.argv[1]); +const relb = fs.readFileSync(process.argv[2]); +if (!WebAssembly.validate(defb)) { + console.error("FAIL default module failed WebAssembly.validate"); + process.exit(1); +} +WebAssembly.instantiate(defb).then(function () { + console.log("PASS default module instantiates (SIMD128 only)"); + if (!WebAssembly.validate(relb)) { + console.log("SKIP WASM_RELAXED_SIMD=1 module does not validate on this engine (expected without relaxed SIMD)"); + return; + } + return WebAssembly.instantiate(relb).then(function () { + console.log("PASS WASM_RELAXED_SIMD=1 module instantiates on this engine"); + }); +}).catch(function (err) { + console.error(err); + process.exit(1); +}); +' "$OUT/default/kernels.wasm" "$OUT/relaxed/kernels.wasm" +else + echo "SKIP node not found; opcode checks still apply" +fi + +# Rebuild default objects so a following 'make libs' is not mixed. +rm -f "${OBJS[@]/#/kernel/}" +"$MAKE_BIN" -C kernel -j"$JOBS" "${OBJS[@]}" "${MAKE_COMMON[@]}" + +if [[ "$FAILS" -ne 0 ]]; then + echo "$FAILS check(s) failed" >&2 + exit 1 +fi +echo "relaxed-SIMD opt-in checks passed"