From 269e1cd505b9bf531a76ffeb0cc7dc753eacb346 Mon Sep 17 00:00:00 2001 From: Hugo Meiland Date: Mon, 13 Jul 2026 11:00:15 +0200 Subject: [PATCH] RISC-V: add get_L2_size() and blas_set_parameter() for cache-aware GEMM 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). --- common_macro.h | 2 +- driver/others/memory.c | 4 ++-- driver/others/parameter.c | 50 +++++++++++++++++++++++++++++++++++++++ param.h | 25 ++++++++++++++++---- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/common_macro.h b/common_macro.h index 4051cb3794..ea2cf5ea15 100644 --- a/common_macro.h +++ b/common_macro.h @@ -2712,7 +2712,7 @@ #ifndef ASSEMBLER #if !defined(DYNAMIC_ARCH) \ && (defined(ARCH_X86) || defined(ARCH_X86_64) || defined(ARCH_IA64) || defined(ARCH_MIPS64) || defined(ARCH_ARM64) \ - || defined(ARCH_LOONGARCH64) || defined(ARCH_E2K) || defined(ARCH_ALPHA)) + || defined(ARCH_LOONGARCH64) || defined(ARCH_E2K) || defined(ARCH_ALPHA) || defined(ARCH_RISCV64)) extern BLASLONG gemm_offset_a; extern BLASLONG gemm_offset_b; extern BLASLONG bgemm_p; diff --git a/driver/others/memory.c b/driver/others/memory.c index d7df981e66..e25dfbae6e 100644 --- a/driver/others/memory.c +++ b/driver/others/memory.c @@ -1220,7 +1220,7 @@ UNLOCK_COMMAND(&alloc_lock); if (!blas_num_threads) blas_cpu_number = blas_get_cpu_number(); #endif -#if defined(ARCH_X86) || defined(ARCH_X86_64) || defined(ARCH_IA64) || defined(ARCH_MIPS64) || defined(ARCH_ARM64) || defined(ARCH_LOONGARCH64) +#if defined(ARCH_X86) || defined(ARCH_X86_64) || defined(ARCH_IA64) || defined(ARCH_MIPS64) || defined(ARCH_ARM64) || defined(ARCH_LOONGARCH64) || defined(ARCH_RISCV64) #ifndef DYNAMIC_ARCH blas_set_parameter(); #endif @@ -2822,7 +2822,7 @@ void *blas_memory_alloc(int procpos){ if (!blas_num_threads) blas_cpu_number = blas_get_cpu_number(); #endif -#if defined(ARCH_X86) || defined(ARCH_X86_64) || defined(ARCH_IA64) || defined(ARCH_MIPS64) || defined(ARCH_ARM64) || defined(ARCH_LOONGARCH64) +#if defined(ARCH_X86) || defined(ARCH_X86_64) || defined(ARCH_IA64) || defined(ARCH_MIPS64) || defined(ARCH_ARM64) || defined(ARCH_LOONGARCH64) || defined(ARCH_RISCV64) #ifndef DYNAMIC_ARCH blas_set_parameter(); #endif diff --git a/driver/others/parameter.c b/driver/others/parameter.c index 3a1363e0ea..e1451fc714 100644 --- a/driver/others/parameter.c +++ b/driver/others/parameter.c @@ -902,3 +902,53 @@ void blas_set_parameter(void) } #endif + +#if defined(ARCH_RISCV64) + +#include + +/* RISC-V has no architectural cache-size query (cf. x86 CPUID / LoongArch + CPUCFG), so read the L2 (level 2, unified) size from Linux sysfs and fall + back to 512 KB. Returns the L2 size in kilobytes. */ +int get_L2_size(void) { + int size = 0; +#if defined(OS_LINUX) || defined(OS_ANDROID) + int idx; + for (idx = 0; idx <= 4; idx++) { + char path[80]; FILE *fp; int level = 0; long val = 0; char unit = 0; + snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu0/cache/index%d/level", idx); + fp = fopen(path, "r"); if (fp == NULL) continue; + if (fscanf(fp, "%d", &level) != 1) { fclose(fp); continue; } + fclose(fp); if (level != 2) continue; + snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu0/cache/index%d/size", idx); + fp = fopen(path, "r"); if (fp == NULL) continue; + if (fscanf(fp, "%ld%c", &val, &unit) >= 1) { + if (unit == 'M' || unit == 'm') val *= 1024; + if (unit == 'G' || unit == 'g') val *= 1024 * 1024; + size = (int)val; + } + fclose(fp); if (size > 0) break; + } +#endif + if (size <= 0) size = 512; + return size; +} + +void blas_set_parameter(void) { +#if defined(SGEMM_DEFAULT_P_BASE) + /* Scale each precision's packed-A dimension P from the detected L2, relative + to the size the active core's base blocking targets (RISCV_L2_REFERENCE_KB). + The bases come from the core's own param.h block, so this is not tied to any + single core; Q and R keep their param.h defaults. */ + int l2 = get_L2_size(); /* KB */ + int scale = l2 / RISCV_L2_REFERENCE_KB; + if (scale < 1) scale = 1; + if (scale > 4) scale = 4; + sgemm_p = SGEMM_DEFAULT_P_BASE * scale; + dgemm_p = DGEMM_DEFAULT_P_BASE * scale; + cgemm_p = CGEMM_DEFAULT_P_BASE * scale; + zgemm_p = ZGEMM_DEFAULT_P_BASE * scale; +#endif +} + +#endif diff --git a/param.h b/param.h index 9389c4135f..4a9e0a4b08 100644 --- a/param.h +++ b/param.h @@ -3265,10 +3265,27 @@ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define SHGEMM_DEFAULT_P 128 #undef SBGEMM_DEFAULT_P #define SBGEMM_DEFAULT_P 128 -#define SGEMM_DEFAULT_P 128 -#define DGEMM_DEFAULT_P 64 -#define CGEMM_DEFAULT_P 64 -#define ZGEMM_DEFAULT_P 64 +/* Base packed-A (P) blocking for this core. On static builds blas_set_parameter() + scales P from the L2 cache detected at runtime, relative to RISCV_L2_REFERENCE_KB + (the L2 size these bases target); Q and R keep their param.h defaults. A cache + equal to the reference reproduces the stock blocking. DYNAMIC_ARCH uses the + literals directly (kernel/setparam-ref.c fills the gotoblas table from them). */ +#define RISCV_L2_REFERENCE_KB 512 +#define SGEMM_DEFAULT_P_BASE 128 +#define DGEMM_DEFAULT_P_BASE 64 +#define CGEMM_DEFAULT_P_BASE 64 +#define ZGEMM_DEFAULT_P_BASE 64 +#if defined(DYNAMIC_ARCH) +#define SGEMM_DEFAULT_P SGEMM_DEFAULT_P_BASE +#define DGEMM_DEFAULT_P DGEMM_DEFAULT_P_BASE +#define CGEMM_DEFAULT_P CGEMM_DEFAULT_P_BASE +#define ZGEMM_DEFAULT_P ZGEMM_DEFAULT_P_BASE +#else +#define SGEMM_DEFAULT_P sgemm_p +#define DGEMM_DEFAULT_P dgemm_p +#define CGEMM_DEFAULT_P cgemm_p +#define ZGEMM_DEFAULT_P zgemm_p +#endif #undef SHGEMM_DEFAULT_Q #define SHGEMM_DEFAULT_Q 128