RISC-V 与处理器
RISC-V 矩阵处理器的基本架构
从 ISA 扩展、寄存器组织、矩阵乘数据通路和软件接口角度整理 RISC-V 矩阵处理器原型思路。
RISC-V 矩阵处理器可以作为主处理器旁路的领域专用扩展,也可以作为协处理器通过总线或定制接口连接。第一版笔记只讨论原型结构,不声称已有完整性能结果。
矩阵计算是神经网络、信号处理和科学计算中的核心负载。通用 RISC-V 标量核可以完成矩阵乘,但指令数量、访存开销和循环控制开销都比较大。矩阵处理器的目标是把常见的矩阵/向量运算映射到更宽的数据通路和更高效的片上数据复用结构中。
这篇笔记记录一个面向学习和研究原型的 RISC-V 矩阵处理器架构拆解。重点是模块关系、指令抽象和验证关注点,不包含具体性能指标。
架构组成
RISC-V Core
├─ Decode / Custom Instruction
├─ Matrix Register File
├─ Load Store Unit
└─ Matrix MAC Array
从系统连接方式看,大致有三种路线:
| 方式 | 描述 | 优点 | 代价 |
|---|---|---|---|
| 自定义指令扩展 | 在 decode/execute 路径加入矩阵指令 | 软件调用开销低 | 与处理器流水线耦合高 |
| 协处理器接口 | 通过专用握手接口发射任务 | 主核和矩阵单元边界清晰 | 需要定义协议 |
| MMIO 加速器 | 通过寄存器配置和 DMA 运行 | 易于系统集成 | 调用开销较大 |
第一版原型可以先从协处理器或 MMIO 风格开始,因为它们更容易独立验证。等指令语义和数据通路稳定后,再考虑更紧密的自定义指令集成。
设计目标
矩阵处理器不是简单地添加几个乘法器。它至少要回答以下问题:
- 数据如何从内存进入矩阵寄存器或片上 buffer?
- 矩阵块的尺寸是否固定,还是由配置寄存器决定?
- 支持哪些数据类型,例如 int8、int16、fp16 或定点格式?
- 累加结果使用多宽的类型?
- 矩阵单元与主核如何同步?
- 异常、中断和非法指令如何处理?
原型阶段可以限制范围,例如只支持 int8 输入和 int32 累加,只支持固定 tile 尺寸,只实现最小 load/compute/store 流程。明确限制比泛泛描述“可扩展”更有工程价值。
指令考虑
可能需要覆盖三类操作:
- 矩阵寄存器加载与存储。
- 矩阵乘、向量乘和累加。
- 配置寄存器与状态查询。
一个抽象指令集合可以写成:
| 指令 | 含义 | 备注 |
|---|---|---|
mld md, rs1 | 从内存加载矩阵块到矩阵寄存器 | 地址来自通用寄存器 |
mst ms, rs1 | 将矩阵寄存器写回内存 | 需要处理布局 |
mmul md, ma, mb | 矩阵块乘法 | 结果写入目标矩阵寄存器 |
macc md, ma, mb | 矩阵乘并累加 | 适合分块 K 维 |
mcfg rs1 | 写入矩阵配置 | 设置尺寸、模式、数据类型 |
mstatus rd | 读取矩阵单元状态 | 查询 busy/done/error |
这些名称只是说明语义,不代表真实编码。真正使用 RISC-V custom opcode 时,需要根据 ISA 规范、工具链和处理器实现选择合法编码,并考虑反汇编、编译器内建函数和异常处理。
矩阵寄存器组织
矩阵寄存器可以理解为一组比通用寄存器更宽、更接近计算阵列的数据存储。它可能由寄存器堆、SRAM 或分布式 RAM 实现。
需要考虑:
- 寄存器数量,例如
m0到m7。 - 每个寄存器保存的 tile 大小,例如
8x8 int8。 - 读写端口数量是否满足 MAC 阵列需求。
- 是否支持不同数据类型复用同一物理空间。
- 上下文切换时是否需要保存矩阵寄存器。
一个简单组织方式是:
Matrix Register File
├─ M0: A tile
├─ M1: B tile
├─ M2: C tile / accumulator
└─ ...
如果矩阵寄存器太大,保存和恢复上下文会很昂贵;如果太小,矩阵乘需要频繁 load/store,数据复用不足。这个权衡需要结合目标负载决定。
数据通路
矩阵乘核心可以抽象为:
硬件实现时要处理片上寄存器端口、乘加阵列规模、累加位宽和访存带宽。
一个常见的数据通路是 systolic array 或类似的二维 MAC 阵列:
B stream →
┌────┬────┬────┐
A ↓ │MAC │MAC │MAC │
├────┼────┼────┤
│MAC │MAC │MAC │
├────┼────┼────┤
│MAC │MAC │MAC │
└────┴────┴────┘
在 systolic 风格中,A 和 B 的数据按节拍流过阵列,每个 PE 完成本地乘加并把数据传给相邻 PE。它的优势是局部连接规则、数据复用清晰;代价是边界调度和 valid 对齐需要谨慎处理。
另一种方式是 SIMD dot-product 阵列:
A vector ─┐
├─ parallel multiply → adder tree → accumulator
B vector ─┘
这种结构更接近向量点积,对小矩阵或向量乘加比较直接。选择哪种结构取决于目标 tile、数据类型、频率目标和实现复杂度。
Load/Store 单元
矩阵处理器的性能经常受数据搬运限制。Load/Store 单元需要处理:
- 内存地址生成。
- 数据对齐和宽度转换。
- 行主序/列主序或 packed layout。
- 与主处理器 cache 的一致性。
- 矩阵寄存器写入顺序。
如果矩阵单元直接从内存读取数据,需要明确它看到的是物理地址还是虚拟地址。如果系统中存在 cache,还要考虑矩阵单元读取的数据是否与 CPU cache 中的数据一致。学习原型可以先使用简单的片上 SRAM 或显式 flush 的 buffer,降低系统复杂度。
配置寄存器
矩阵处理器不一定要把所有尺寸都编码进指令。可以用配置寄存器保存当前模式:
| 配置项 | 示例 | 作用 |
|---|---|---|
M | 输出行数 | 控制外层循环 |
N | 输出列数 | 控制列方向 |
K | 归约维度 | 控制累加次数 |
dtype | int8/int16 | 控制乘法和符号扩展 |
stride_a | 字节步长 | 控制矩阵 A 地址 |
stride_b | 字节步长 | 控制矩阵 B 地址 |
stride_c | 字节步长 | 控制结果地址 |
配置寄存器可以减少指令位宽压力,但会引入状态。软件必须清楚何时配置生效,硬件也要处理 busy 时写配置的行为。
软件接口
void matmul_i8_acc32(const int8_t *a, const int8_t *b, int32_t *c, int m, int n, int k);
软件接口需要隐藏底层 tile 和寄存器映射,但仍要暴露必要的对齐和尺寸约束。
一个更接近实际调用的接口可能包括初始化、配置和执行:
typedef struct {
int m;
int n;
int k;
int lda;
int ldb;
int ldc;
} matmul_cfg_t;
int matrix_accel_run(const int8_t *a,
const int8_t *b,
int32_t *c,
const matmul_cfg_t *cfg);
软件库负责把任意尺寸拆成硬件支持的 tile,并处理尾块:
for m_tile:
for n_tile:
clear C tile
for k_tile:
load A tile
load B tile
macc C, A, B
store C tile
尾块处理不能忽略。矩阵尺寸通常不会刚好等于硬件 tile 的整数倍,需要通过 padding、mask 或 fallback 软件路径处理。
验证计划
矩阵处理器的验证可以分层进行:
| 层级 | 检查内容 | 方法 |
|---|---|---|
| PE | 单个乘加单元 | directed test、溢出边界 |
| Array | MAC 阵列数据流 | 小矩阵对拍、valid 对齐 |
| Register file | 读写和端口冲突 | 随机读写、scoreboard |
| Instruction | decode 和状态更新 | 指令级仿真 |
| System | 软件库调用 | C 测试和 RTL 联合仿真 |
参考模型可以先用 Python 或 C 实现:
def matmul_i8_acc32(a, b):
m, k = a.shape
k2, n = b.shape
assert k == k2
c = [[0 for _ in range(n)] for _ in range(m)]
for i in range(m):
for j in range(n):
acc = 0
for kk in range(k):
acc += int(a[i][kk]) * int(b[kk][j])
c[i][j] = acc
return c
测试用例至少应该覆盖:
- 全零、全一、正负混合。
- 最大值和最小值。
- 非 tile 对齐尺寸。
- 随机矩阵。
- 连续多次调用。
- busy 状态下的软件访问。
与 RISC-V 生态的关系
RISC-V 的优势是开放 ISA 和可扩展性,但这不意味着任意自定义指令都容易维护。一个矩阵扩展如果希望被软件长期使用,需要考虑:
- 汇编器和反汇编器支持。
- 编译器 intrinsic 或内联汇编接口。
- ABI 是否需要保存矩阵寄存器。
- 操作系统上下文切换如何处理。
- 调试器如何显示矩阵状态。
因此,对研究原型来说,可以先通过 MMIO 或协处理器接口验证计算结构,再逐步探索 ISA 集成。这样可以降低早期工具链负担。
设计风险
存储带宽不足
矩阵阵列越大,对输入带宽的要求越高。如果 load/store 跟不上,阵列利用率会下降。需要通过 tile、预取、双缓冲或更宽接口缓解。
寄存器端口过多
矩阵寄存器如果直接支持大量并行读写端口,面积和时序压力会很大。实际实现可能需要 bank 分割或数据调度。
指令语义过早复杂化
早期就支持太多数据类型、尺寸和模式,会让 RTL 和软件都难以验证。建议从最小可用语义开始。
忽略系统一致性
如果矩阵单元和 CPU 共享内存,必须明确 cache、DMA 和地址转换关系。否则单模块仿真正确,系统运行仍可能出错。
阶段性总结
RISC-V 矩阵处理器的关键不只是 MAC 阵列,而是 ISA 语义、矩阵寄存器、load/store、数据通路和软件库之间的协同。原型阶段应先收窄目标,明确 tile、数据类型和调用方式,通过参考模型和分层验证保证功能正确,再逐步考虑更深的流水线、更多数据类型和更完整的工具链支持。
后续需要继续补充指令编码、异常处理、编译器内建函数和系统级验证方法。