Skip to content

RISC-V Vector (RVV 1.0) SIMD support#100

Open
mikey wants to merge 2 commits into
marton78:masterfrom
mikey:riscv-vector
Open

RISC-V Vector (RVV 1.0) SIMD support#100
mikey wants to merge 2 commits into
marton78:masterfrom
mikey:riscv-vector

Conversation

@mikey

@mikey mikey commented May 26, 2026

Copy link
Copy Markdown

Adds a RISC-V Vector backend alongside the existing SSE/AVX/NEON/Altivec
paths, gated entirely on __riscv_vector + a new PFFFT_ENABLE_RVV
build define so the change is a no-op for every other target.

Two commits:

  1. Add RISC-V Vector (RVV 1.0) SIMD backend — new src/simd/pf_rvv_float.h
    and pf_rvv_double.h, plus the matching include lines in
    pf_float.h / pf_double.h, the SIMD_*_HDRS listing in
    CMakeLists.txt, and a short README mention.
  2. cmake: detect riscv64 and auto-enable RVV (+ CI job) — mirrors the
    aarch64/NEON auto-enable in cmake/target_optimizations.cmake, plus a
    new build_riscv64_qemu workflow that exercises the headers under
    qemu-user-static.

Design

  • v4sf is a GCC vector_size typedef (16 bytes for float, 32 for
    double), which the compiler lowers to RVV instructions (vfadd.vv
    / vfmul.vv / vfmadd.vv / vrgather.vv / …).
  • For float, SIMD_SZ=4 → 128-bit vector, works on any VLEN ≥ 128.
  • For double, SIMD_SZ=4 → 256-bit vector, matching the AVX double
    layout. On hardware with VLEN ≥ 256 this is a single instruction;
    otherwise the compiler splits it into two ops. The build detects
    the host VLEN via a tiny try_run probe and appends a zvlNNNb
    token so the single-instruction form is used when available.
  • Shuffles route through a PF_RVV_SHUF4 helper that picks
    __builtin_shufflevector (clang, GCC 12+) or __builtin_shuffle
    (older GCC), matching the same pattern used in src/sse2neon.h.
  • The auto-detect also probes for the V extension itself: if it isn't
    present, the build falls back to plain rv64gc (no
    PFFFT_ENABLE_RVV, scalar path) instead of producing a binary that
    SIGILLs.

Verification

Tested on a SpaceMIT X100 (8 cores, VLEN=256, Ubuntu 26.04):

  • gcc 15.2.0: ctest 13/13 pass; both test_pffft_{float,double} --test-simd exit clean (the shuffle/transpose self-tests).
  • clang 21.1.8: same — ctest 13/13, both --test-simd pass.
    RVV throughput trails gcc by ~10–25 % (clang's shuffle lowering
    is less tight) but stays solidly above the scalar baseline.
  • Auto-detect picks -march=rv64gcv_zvl256b and defines
    PFFFT_ENABLE_RVV; runtime pffft_simd_arch() reports "RVV",
    pffft_simd_size() reports 4.
  • Explicit TARGET_C_ARCH=rv64gc builds cleanly and runs the scalar
    path — no spurious RVV define.

Performance vs the pure-scalar build
(-DPFFFT_USE_SIMD=OFF -DPFFFT_USE_SCALAR_VECT=OFF), real FFT,
PFFFT-U column, gcc 15.2, single core (taskset -c 0):

size float speedup double speedup
128 2.96× 2.05×
512 2.67× 2.32×
1024 2.78× 2.17×
4096 2.95× 2.20×
16384 2.62× 2.09×
65536 2.60× 2.04×

Complex FFTs see roughly 1.3–1.4× as expected
(memory-bandwidth bound).

Deliberately not done

  • SIMD_SZ=4 is hard-coded throughout pffft's pass functions, so
    this backend stays at 4 lanes rather than scaling with VLEN.
    Using LMUL > 1 or a VLEN-agnostic refactor would need changes
    well outside the SIMD headers.
  • No runtime CPU detection — PFFFT_ENABLE_RVV is decided at
    compile time, the same way PFFFT_ENABLE_NEON is.

What's tested vs not

  • ✅ Native gcc 15.2 and clang 21.1.8 builds on real RVV 1.0
    hardware (correctness, --test-simd, performance numbers above).
  • ✅ Explicit TARGET_C_ARCH=rv64gc (no-V) fallback path on the
    same hardware.
  • ⚠️ The new build_riscv64_qemu CI job has not yet run on
    GitHub Actions; the first push may need toolchain/QEMU
    adjustments.

Happy to iterate on any of this — particularly the CMake glue and
the CI job — if there's a different shape you'd prefer.

Implementation done with Claude Code; see Assisted-by: trailers on the commits.

mikey added 2 commits May 26, 2026 13:49
Add pf_rvv_float.h and pf_rvv_double.h, providing the v4sf SIMD
contract (VADD/VMUL/VMADD, INTERLEAVE2/UNINTERLEAVE2, VTRANSPOSE4,
VSWAPHL, VREV_S/VREV_C, etc.) on top of GCC vector_size types so
that the compiler lowers them to RVV instructions
(vfadd.vv/vfmul.vv/vfmadd.vv/vrgather.vv/...).

The headers are gated on __riscv_vector together with the new
PFFFT_ENABLE_RVV define (added in a follow-up cmake change), so a
plain rv64gc build still falls back to the scalar implementation.

For float, SIMD_SZ=4 uses a 128-bit vector and works for any
VLEN >= 128. For double, SIMD_SZ=4 uses a 256-bit vector, matching
the existing AVX/SSE2 double layout; on hardware with VLEN >= 256
this is a single vfadd.vv per op, otherwise the compiler splits it.

Shuffles are routed through a small PF_RVV_SHUF4 helper that uses
__builtin_shufflevector when available (clang, GCC 12+) and falls
back to __builtin_shuffle on older GCC, so the headers compile
with both compilers. VALIGNED reports v4sf-sized alignment so the
existing test_pffft_{float,double} --test-simd self-test sees a
mix of aligned and unaligned addresses and passes cleanly.

README.md picks up RVV in the supported-SIMD list and a short
"Building on RISC-V" subsection describing the auto-enable.

Verified correctness with the existing test_pffft_float /
test_pffft_double suites (sizes 32..65536) and with --test-simd
on a SpaceMIT X100 (VLEN=256, gcc 15.2). Performance vs the
pure-scalar build (PFFFT_USE_SIMD=OFF, PFFFT_USE_SCALAR_VECT=OFF)
on real FFTs, PFFFT-U column, single core:

    size   float speedup   double speedup
     128       2.96x           2.05x
     512       2.67x           2.32x
    1024       2.78x           2.17x
    4096       2.95x           2.20x
   16384       2.62x           2.09x
   65536       2.60x           2.04x

Assisted-by: Claude Code (Opus 4.7) <noreply@anthropic.com>
Mirror the aarch64/NEON auto-enable path for RISC-V: when the host
is riscv64 and the user has not pinned TARGET_C_ARCH/TARGET_CXX_ARCH,
pick an appropriate -march and define PFFFT_ENABLE_RVV so the new
RVV SIMD headers activate. When the user does set an explicit RISC-V
march that includes the V extension (matched via the regex
"rv[0-9]+[a-z_]*v($|_)" so plain rv64gc does not falsely match) we
leave the march alone and just define the macro.

The default march is decided by a single probe that compiles and
runs a tiny program against -march=rv64gcv:

  - If the probe runs and prints a VLEN, the target supports V. The
    default is rv64gcv, and if VLEN >= 256 the matching zvlNNNb is
    appended so the compiler emits single-instruction 4-wide double
    ops instead of splitting them into VLEN_min=128 pieces.

  - If the probe fails to compile or fails to run, the target lacks
    V. The default falls back to plain rv64gc and PFFFT_ENABLE_RVV
    is not defined, so the build produces a working scalar binary
    instead of one that SIGILLs on the first vector instruction.

The probe runs natively or under CMAKE_CROSSCOMPILING_EMULATOR
(e.g. qemu-riscv64-static); a bare cross-compile with no emulator
optimistically assumes V and falls back to rv64gcv, so users
targeting non-V cores should pass TARGET_C_ARCH=rv64gc explicitly.

Also add rv64gc/rv64gcv to the TARGET_C_ARCH cache value list so
the option shows sensible choices on RISC-V hosts in cmake-gui /
ccmake.

Add a build_riscv64_qemu CI job that cross-builds with the Ubuntu
gcc-riscv64-linux-gnu toolchain and runs ctest under
qemu-riscv64-static, mirroring the build_emscripten job for WASM.
This exercises the RVV headers, the cmake auto-detect path, and
the --test-simd shuffle/transpose validator on every PR without
requiring the maintainer to have RISC-V hardware.

Assisted-by: Claude Code (Opus 4.7) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant