1. RISC-V向量代码生成的技术背景
RISC-V作为一种开放指令集架构,近年来在高性能计算和机器学习领域获得了广泛关注。其向量扩展(RVV)为数据并行计算提供了硬件支持,但不同厂商实现的RVV配置差异(如向量寄存器长度、SIMD单元宽度等)给代码移植和优化带来了挑战。
1.1 RISC-V向量扩展的核心特性
RVV采用向量长度无关(Vector Length Agnostic, VLA)编程模型,开发者无需针对特定硬件配置重写代码。关键特性包括:
- 可配置的向量寄存器组(通常为128-1024位)
- 丰富的向量指令集(加载/存储、算术运算、归约等)
- 掩码寄存器支持条件执行
- 分段加载/存储处理非连续数据
这种灵活性也带来了优化难题:如何在不同硬件上自动选择最优的向量化策略和微内核(micro-kernel)尺寸。
1.2 传统编译器的局限性
传统编译器(如GCC、LLVM)在RVV代码生成方面存在以下不足:
- 自动向量化能力有限,难以充分利用VLA特性
- 缺乏对特定计算模式(如矩阵乘法)的针对性优化
- 生成的代码难以适应不同RVV实现
这使得高性能库(如OpenBLAS)仍需依赖手工编写的汇编微内核,维护成本高昂。
2. MLIR与xDSL的技术栈解析
2.1 MLIR的多级中间表示
MLIR(Multi-Level Intermediate Representation)是LLVM生态系统中的编译器基础设施,其核心创新在于:
- 分级方言系统:支持从高级算法到底层硬件的渐进式 lowering
- 可扩展的转换框架:开发者可以定义领域特定的优化pass
- 与现有工具链集成:最终可生成LLVM IR或直接输出目标代码
在矩阵计算领域,MLIR提供了linalg、tensor等高级方言,可以表达块状算法而不绑定具体硬件。
2.2 xDSL的轻量级编译器工具包
xDSL是Python实现的编译器构造框架,与MLIR共享核心抽象概念但更易用:
- 快速原型设计:Python语法简化了方言和转换的开发
- 动态IR构建:支持运行时生成和修改IR
- 无缝工具集成:与Jupyter、测试框架等Python生态天然兼容
xDSL特别适合实现MLIR中缺失的中间转换阶段,正如本工作中的RVV lowering。
3. 混合编译流水线设计
3.1 整体架构
论文提出的六阶段流水线如图2所示:
- 配置阶段:用户指定微内核尺寸(mr×nr)、数据类型和向量长度
- 内核生成:xDSL构建包含scf、memref、arith和自定义rvv方言的MLIR
- IR转换:通过三个关键pass将高级方言降级到emitc
- C代码生成:调用mlir-translate工具输出带RVV intrinsics的C代码
- 测试框架生成:自动创建验证环境和性能测试
- 部署执行:在目标硬件上编译和评估
3.2 关键转换pass实现
3.2.1 MemRefToEmitCPass
将MLIR的memref类型(多维数组抽象)转换为C指针:
// 转换前 %buf = memref.alloc() : memref<?xf32, 1> // 转换后 %buf = emitc.ptr<f32>(...)处理了跨步访问、子视图等复杂内存模式,确保生成的C代码保持原始语义。
3.2.2 SCFToEmitCPass
将结构化控制流(scf.for)转换为传统循环:
// 转换前 %result = scf.for %i = %lb to %ub step %step iter_args(%acc = %init) -> f32 { // 循环体 scf.yield %new_acc : f32 } // 转换后 emitc.variable %acc = %init : f32 emitc.for %i = %lb to %ub step %step { // 循环体 emitc.assign %acc = %new_acc : f32 }3.2.3 RVVToEmitCPass
最复杂的转换,将自定义rvv操作映射到RVV intrinsics。以FMA操作为例:
# xDSL中定义rvv.vfmacc操作 @irdl_op_definition class vfmacc_vf_f32m1Op(IRDLOperation): name = "rvv.vfmacc_vf_f32m1" vd = operand_def(RVVFloat32M1Type) # 累加器向量 memref = operand_def(MemRefType) # 内存指针 offset = operand_def(IndexType) # 标量偏移 vs = operand_def(RVVFloat32M1Type) # 源向量 avl = operand_def(IndexType) # 向量长度 result = result_def(RVVFloat32M1Type)转换器将其重写为emitc调用:
%res = emitc.call_opaque(__riscv_vfmacc_vf_f32m1)(%vd, %scalar, %vs, %avl)4. 矩阵乘法微内核生成实战
4.1 BLIS算法框架
论文采用BLIS库的Goto算法结构(图1),将矩阵乘法分解为:
- 外层循环:按缓存块大小分块矩阵
- 打包阶段:将子矩阵重组为连续内存布局
- 微内核:计算小块矩阵乘(mr×nr × kc)
其中微内核是性能关键,通常仅占代码量的5%但决定80%以上的性能。
4.2 xDSL实现微内核
以8×4 FP32微内核为例(图3),核心步骤包括:
- 向量加载:使用
vle32.v指令加载Ac的8个元素 - FMA计算:4次
vfmacc操作处理Bc的标量 - 寄存器传递:通过scf.yield更新累加器
生成的MLIR代码如图4所示,其中关键FMA操作:
%61 = "rvv.vfmacc_vf_f32m1Op"(%42, %4, %53, %52, %51) : (!rvv.vfloat32m1, memref<-1xf32>, index, !rvv.vfloat32m1, index) -> !rvv.vfloat32m14.3 性能优化技巧
寄存器阻塞:确保mr×nr的微块适合向量寄存器
- K230(128位):mr=4(4×fp32)
- BPI(256位):mr=8
指令调度:交错加载和计算隐藏延迟
# 预取下一次迭代数据 next_A = lb.vle32(Ac, k_mul_lda + mr, vl) # 执行当前计算 cr = lb.vfmacc(cr, Bc[k], ar, vl)边界处理:自动生成多种尺寸内核处理非对齐情况
5. 性能评估与结果分析
5.1 测试平台配置
| 平台 | 核心频率 | 向量宽度 | L1缓存 | L2缓存 |
|---|---|---|---|---|
| K230 | 1.6GHz | 128-bit | 32KB | 256KB |
| BananaPi F3 | 1.6GHz | 256-bit | 32KB | 512KB |
编译选项:-march=rv64gcvzfh -mabi=lp64d
5.2 微内核性能
图10展示了不同mr×nr配置的性能(GFLOPS):
- K230最佳配置:20×6(8.1 GFLOPS)
- BPI最佳配置:16×15(16.2 GFLOPS)
关键发现:
- 当mr是向量寄存器容量的整数倍时性能最佳
- 过大nr会导致寄存器溢出,性能下降
- 非对齐访问损耗可达30%
5.3 GEMM整体性能
对比OpenBLAS的测试结果(图11):
方阵乘法(S1-S5):
- K230:5.1 vs 4.5 GFLOPS(+13%)
- BPI:8.6 vs 6.8 GFLOPS(+26%)
BERT模型层(B1-B5):
- K230:5.9 vs 4.0 GFLOPS(+47%)
- BPI:12.2 vs 5.1 GFLOPS(+140%)
性能提升主要来自:
- 自动选择最优微内核尺寸
- 精确控制向量指令调度
- 减少边界检查开销
6. 扩展应用与未来方向
6.1 技术推广场景
深度学习算子优化:
- 卷积层转换为GEMM
- 注意力机制中的矩阵运算
科学计算内核:
- 稀疏矩阵向量乘
- 张量收缩运算
嵌入式AI:
- 量化算子代码生成
- 混合精度计算
6.2 后续改进方向
支持更多数据类型:
- BF16浮点
- INT8/INT4量化
自动化微内核选择:
- 基于硬件探测的自动调优
- 机器学习驱动的预测模型
扩展指令集支持:
- RISC-V矩阵扩展(RVM)
- 自定义指令扩展
这套技术栈已开源在GitHub(见论文4.3节),开发者可以基于此构建自己的RVV代码生成器。实际部署时建议:
- 对关键计算模式建立专门的方言
- 使用CI/CD自动化测试不同硬件配置
- 结合性能分析工具持续优化转换pass