3. 向量矩阵乘法
任务目标
在RISC-V向量扩展(RVV)平台上实现一个高效的向量矩阵乘法(VMM)内核。核心要求:使用向量加载指令(vle32)将矩阵行与向量元素载入寄存器,利用向量乘加指令(vmacc)完成乘累加运算,并通过数据布局优化(行主序连续存储、向量长度对齐)减少内存访问开销。
环境与依赖
- 支持RVV 1.0的编译器:GCC 12+(
-march=rv64gcv)或Clang 17+ - 目标平台:任何支持RVV的硬件或模拟器(如QEMU 8.0+)
- 无需额外库,仅使用标准C和RVV内建函数(
riscv_vector.h)
核心实现
以下代码实现一个M×K矩阵与长度为K的向量的乘法,输出长度为M的结果向量。数据布局采用行主序连续存储,向量长度(VL)在运行时通过__riscv_vsetvlmax动态获取,确保代码可移植到不同VLEN的硬件。
#include <riscv_vector.h>
#include <stddef.h>
#include <stdint.h>
// 矩阵行主序存储: A[i][k] = A[i*K + k]
// 输入: A (M×K), x (K), 输出: y (M)
void vmm_rvv(const float *A, const float *x, float *y,
size_t M, size_t K) {
for (size_t i = 0; i < M; i++) {
const float *row = A + i * K;
float acc = 0.0f; // 标量累加器(也可用向量归约)
size_t k = 0;
// 主循环:按硬件向量长度分块处理
while (k < K) {
size_t vl = __riscv_vsetvlmax_f32m1(); // 获取最大VL
if (k + vl > K) {
vl = K - k; // 尾部处理
vl = __riscv_vsetvl_f32m1(vl);
}
vfloat32m1_t vec_a = __riscv_vle32_v_f32m1(row + k, vl);
vfloat32m1_t vec_x = __riscv_vle32_v_f32m1(x + k, vl);
// 向量乘加:acc_vec += vec_a * vec_x
vfloat32m1_t acc_vec = __riscv_vfmv_v_f_f32m1(0.0f, vl);
acc_vec = __riscv_vfmacc_vv_f32m1(acc_vec, vec_a, vec_x, vl);
// 向量内归约求和(将当前块累加到标量)
float block_sum = __riscv_vfmv_f_s_f32m1_f32(
__riscv_vfredusum_vs_f32m1_f32m1(
acc_vec, __riscv_vfmv_s_f_f32m1(0.0f, vl), vl), vl);
acc += block_sum;
k += vl;
}
y[i] = acc;
}
}
数据布局优化说明
上述实现采用两个关键优化:
- 行主序连续访问:矩阵行元素在内存中连续,向量加载
vle32可直接批量读取,避免索引计算开销。 - 动态向量长度适配:通过
vsetvlmax获取硬件最大VL,主循环按整块处理,尾部用精确VL处理,避免边界分支预测开销。
编译与运行
# 编译(以QEMU用户态模拟为例)
riscv64-unknown-elf-gcc -march=rv64gcv -mabi=lp64d -O2 -o vmm_test vmm_test.c
# 运行验证程序(需自行编写main函数调用vmm_rvv)
qemu-riscv64 ./vmm_test
验证方法
编写测试程序时,使用固定小矩阵(如M=4, K=8)与已知输入,对比RVV实现与朴素三重循环标量实现的结果。判定通过条件:
- 所有元素的误差 < 1e-5(单精度浮点累加误差范围)
- 测试覆盖K不是VL整数倍的情况(如K=10,VLEN=128时VL=4),验证尾部处理正确性
常见问题
- 编译报错“unknown type name 'vfloat32m1_t'”:确认已包含
riscv_vector.h且编译参数带v扩展(-march=rv64gcv)。 - 性能未提升:检查是否启用优化
-O2,并确认矩阵规模足够大(至少K>100)以摊薄函数调用开销。 - 结果错误:优先检查尾部VL计算逻辑,确保
vl在最后一次迭代时被正确截断。