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;
    }
}

数据布局优化说明

上述实现采用两个关键优化:

  1. 行主序连续访问:矩阵行元素在内存中连续,向量加载vle32可直接批量读取,避免索引计算开销。
  2. 动态向量长度适配:通过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在最后一次迭代时被正确截断。