Skip to content

RISC-V: cache-aware GEMM blocking (get_L2_size + blas_set_parameter)#5909

Merged
martin-frbg merged 1 commit into
OpenMathLib:developfrom
hmeiland:riscv64-blas-set-parameter
Jul 14, 2026
Merged

RISC-V: cache-aware GEMM blocking (get_L2_size + blas_set_parameter)#5909
martin-frbg merged 1 commit into
OpenMathLib:developfrom
hmeiland:riscv64-blas-set-parameter

Conversation

@hmeiland

Copy link
Copy Markdown
Contributor

RISC-V: cache-aware GEMM blocking (get_L2_size + blas_set_parameter)

Summary

RISC-V is currently the only major architecture without a get_L2_size() / blas_set_parameter() implementation, so the GEMM cache-blocking parameters (P/Q/R) are fixed at compile time regardless of the actual L2 cache. This PR adds cache-aware blocking for RISC-V, mirroring the existing x86 / LoongArch mechanism.

Because the blocking is now derived from the L2 cache detected at runtime rather than a fixed compile-time constant, future RISC-V cores will get more optimal blocking automatically. The RISC-V hardware landscape is moving quickly — new cores are arriving with progressively larger and more varied L2 caches — and this lets a single library binary adapt its GEMM blocking to each part instead of shipping one hand-picked value per target (or, as today, one value for all). It also gives the RISC-V port the same runtime-tuning hook x86/LoongArch already use, so future per-core tuning has somewhere to live.

What it does

  • get_L2_size() (ARCH_RISCV64) — reads the level-2 (unified) cache size from Linux sysfs (/sys/devices/system/cpu/cpu0/cache/index*/{level,size}). RISC-V has no architectural cache-size query (x86 CPUID / LoongArch CPUCFG), so sysfs is the portable source; falls back to 512 KB when unavailable.
  • blas_set_parameter() (ARCH_RISCV64) — scales the packed-A dimension P from the detected L2, relative to a per-core reference cache size. The base P values and the reference (*_DEFAULT_P_BASE, RISCV_L2_REFERENCE_KB) live in each core's param.h block, so the function carries no hard-coded, core-specific numbers and is a no-op for cores that don't declare a base; Q and R keep their param.h defaults.
  • Wires ARCH_RISCV64 into the blas_set_parameter() call sites (driver/others/memory.c) and the runtime-parameter extern declarations (common_macro.h).
  • param.h RISCV64_ZVL256B: declares the per-core base blocking + reference and maps the *_DEFAULT_P macros to the runtime variables for static builds; DYNAMIC_ARCH keeps the literals (see below). Q and R are unchanged.

DYNAMIC_ARCH

kernel/setparam-ref.c's RISC-V init_parameter() initialises the gotoblas table with TABLE_NAME.sgemm_p = SGEMM_DEFAULT_P, and blas_set_parameter() is not called on the dynamic path. So the param.h change is gated: #if defined(DYNAMIC_ARCH) keeps the integer literals (dynamic builds behave exactly as before), #else uses the runtime variables (static builds get the cache-aware path).

Calibration / scope

blas_set_parameter() carries no core-specific constants: it scales each core's own *_DEFAULT_P_BASE by L2 / RISCV_L2_REFERENCE_KB, both declared in that core's param.h block. Only RISCV64_ZVL256B opts in so far, with a base + reference tuned on the SpaceMiT X60 — an empirical P/Q sweep showed its stock 128/128 blocking is already optimal for the 512 KB shared L2, so at 512 KB this reproduces the stock blocking exactly (performance-neutral by construction). The payoff is forward-looking: a part with a larger L2 gets a proportionally larger panel automatically, and another core opts in simply by declaring its own base + reference — no change to the arch-wide function. Every other RISC-V core is untouched (their param.h literals stand, and blas_set_parameter() is a no-op for them). Happy to adjust the scaling policy (e.g. discrete cache-size tiers like the LoongArch tables) if you'd prefer.

Testing

  • SpaceMiT X60 (RISCV64_ZVL256B, RVV), static buildget_L2_size() returns 512; blas_set_parameter() scales the ZVL256B base by 1 → effective blocking = stock (SGEMM 128/128/16384, DGEMM 64/128/8192); SGEMM throughput unchanged (no regression).
  • X60, DYNAMIC_ARCH build — the param.h P macros resolve to the same literals as before (checked by preprocessing), so kernel/setparam-ref.c's table init is unchanged; a dynamic build compiles cleanly (per-core setparam_RISCV64_*.o objects build with no errors).
  • SiFive U74 / VisionFive 2 (rv64gc scalar, GCC 13.3) — the change compiles; get_L2_size() reads 2048 (the JH7110's 2 MB L2), confirming the sysfs detection generalizes. As a generality check, opting a second core in (the generic core) confirmed blas_set_parameter() scales that core's own base — dgemm_p 128→512 from the generic base, not the X60's 64 — i.e. the function is genuinely per-core. That test also showed the plain total-L2 scale can overshoot a large shared cache (sa≈480 KiB vs the 512 KiB/core share → 4-core DGEMM 3.12 vs 3.43 GF), which is why only ZVL256B opts in and why the scaling policy is called out as adjustable above.

Non-RISC-V builds are unaffected — every change is guarded by ARCH_RISCV64.

…MM blocking

RISC-V was the only major architecture without a get_L2_size() /
blas_set_parameter() implementation, so the GEMM cache-blocking parameters
(P/Q/R) were fixed at compile time regardless of the actual L2 cache size.

Because the blocking is now derived from the L2 cache detected at runtime
rather than a fixed compile-time constant, future RISC-V cores - which are
arriving with progressively larger and more varied L2 caches - get more
optimal blocking automatically, and the port gains the same runtime-tuning
hook x86 and LoongArch already use.

This adds, under ARCH_RISCV64:

- get_L2_size(): reads the level-2 (unified) cache size from Linux sysfs
  (/sys/devices/system/cpu/cpu0/cache/index*/{level,size}); RISC-V has no
  architectural cache-size query like x86 CPUID or LoongArch CPUCFG. Falls
  back to 512 KB when sysfs is unavailable.

- blas_set_parameter(): scales each precision's packed-A dimension P from the
  detected L2. The base blocking and the reference cache size come from the
  active core's own param.h block (*_DEFAULT_P_BASE, RISCV_L2_REFERENCE_KB),
  so the function carries no core-specific constants and is a no-op for cores
  that do not opt in. Q and R keep their param.h defaults.

- driver/others/memory.c and common_macro.h: add ARCH_RISCV64 to the existing
  architecture lists that call blas_set_parameter() and declare the runtime
  parameter variables (sgemm_p, dgemm_p, ...).

- param.h RISCV64_ZVL256B: declares the per-core base blocking + reference and
  maps SGEMM/DGEMM/CGEMM/ZGEMM DEFAULT_P to the runtime variables for static
  builds; DYNAMIC_ARCH keeps the literals, since kernel/setparam-ref.c
  init_parameter() initialises the gotoblas table from these macros and
  blas_set_parameter() is not called on the dynamic path.

Only RISCV64_ZVL256B opts in so far; its base + reference are tuned on the
SpaceMiT X60, where a 512 KB L2 reproduces the stock blocking, so this is
performance-neutral on current hardware. Verified: a static RISCV64_ZVL256B
build reproduces the stock 128/128/16384 (SGEMM) and 64/128/8192 (DGEMM)
blocking; a DYNAMIC_ARCH build compiles cleanly (per-core setparam-ref objects
build without error); and get_L2_size() reads the correct size on both a
SpaceMiT X60 (512 KB L2) and a SiFive U74 / VisionFive 2 (2 MB L2).
@martin-frbg martin-frbg added this to the 0.3.34 milestone Jul 14, 2026
@martin-frbg martin-frbg merged commit a5dcb2a into OpenMathLib:develop Jul 14, 2026
101 of 106 checks passed
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.

2 participants