3. 硬件架构设计:脉动阵列原理、数据流优化与片上存储层次设计
好,咱们进入这一章的核心——硬件架构。说实话,VLA模型推理最吃计算资源的部分就是矩阵乘法。而脉动阵列,就是专门为这个场景量身定做的。
我个人习惯把脉动阵列想象成一个“数据流水线工厂”。数据像流水一样,有节奏地流过每个处理单元(PE)。每个PE只做一件事:乘加运算。但组合起来,效率惊人。
3.1 脉动阵列基本原理
脉动阵列的核心思想很简单:数据复用。你想想看,矩阵乘法里,一个权重会被反复使用。如果每次用都从内存里读,功耗就上去了。脉动阵列让数据在PE之间“流动”,避免频繁访存。
核心优势:
- 数据复用率高,减少片外访存
- PE结构简单,适合FPGA实现
- 流水线化操作,吞吐量高
我在项目中遇到过一个问题:刚开始做脉动阵列时,总觉得PE越多越好。结果发现,阵列大了,布线延迟反而成了瓶颈。嗯,这里要注意:阵列大小要跟片上存储和时钟频率匹配。
下面这张图,是我自己总结的脉动阵列核心逻辑。你看一眼就明白了。
你看,输入特征图从上往下流,部分和从左往右流。权重就待在PE里不动。这就是Weight Stationary的精髓。
3.2 数据流优化策略
数据流的选择,直接决定了你的功耗和性能。我做过三种主流方案,给你对比一下。
| 数据流类型 | 权重复用 | 输入复用 | 部分和累加 | 适用场景 |
|---|---|---|---|---|
| Weight Stationary | 高(权重不动) | 低 | 在PE间传递 | 权重固定的全连接层 |
| Output Stationary | 低 | 低 | 在PE内累加 | 输出通道数大的层 |
| Input Stationary | 低 | 高(输入不动) | 在PE间传递 | 输入特征图复用的卷积层 |
我的经验:VLA模型里,Self-Attention的QKV投影层,我推荐用Weight Stationary。因为权重矩阵是固定的,复用率高。但到了MLP层,Output Stationary反而更好——输出维度大,减少部分和传递的功耗。
3.3 Weight Stationary 深度解析
说白了,Weight Stationary就是把权重提前加载到PE的寄存器里。计算过程中,权重不动,输入数据流过来,部分和往右传。
我举个例子。假设一个4x4的矩阵乘法:
// 伪代码:Weight Stationary 数据流
// 每个PE内部预存权重 W[i][j]
for (k = 0; k < K; k++) {
// 第k行输入特征图,从上往下广播
input_row = Input[k][:];
// 每个PE同时计算
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
// PE(i,j) 接收来自上方的输入和左方的部分和
partial_sum_in = (i == 0) ? 0 : PE(i-1, j).output;
input_val = (j == 0) ? input_row[i] : PE(i, j-1).input_passed;
// 乘加运算
PE(i,j).output = partial_sum_in + input_val * W[i][j];
}
}
}
你看,代码逻辑很清晰。每个PE只做一次乘加,然后把结果往右传。这样,整个阵列的吞吐量就是每个时钟周期完成16次乘加。
避坑指南:我曾经在实现Weight Stationary时,忽略了权重加载的时间。阵列越大,加载权重需要的时钟周期越多。如果权重频繁更新(比如VLA模型的动态权重),加载开销会吃掉所有性能优势。我的建议是:权重更新频率不要超过每100个时钟周期一次,否则考虑Output Stationary。
3.4 Output Stationary 适用场景
Output Stationary的思路正好反过来。部分和待在PE里不动,输入和权重流过来。这样做的好处是:每个PE只负责一个输出元素的计算,累加过程完全在PE内部完成。
什么时候用?我告诉你一个判断标准:当输出通道数远大于输入通道数时。比如VLA模型的MLP层,输出维度是输入维度的4倍。这时候用Output Stationary,每个PE只累加自己的部分和,不需要在PE间传递,省掉了大量连线功耗。
3.5 片上存储层次设计
存储层次,说白了就是怎么在功耗和容量之间做取舍。FPGA的BRAM资源有限,不能什么都往里塞。
我一般把存储分成三级:
- 寄存器(Register):速度最快,容量最小。给PE内部用,存当前计算的权重或部分和。
- BRAM(Block RAM):速度中等,容量适中。存输入特征图Tile和权重Tile。
- DRAM(片外DDR):速度慢,容量大。存整个模型权重和中间结果。
这里有个关键点:数据搬移的功耗远大于计算功耗。我做过实测,一次DRAM读操作的功耗,相当于100次乘加运算。所以,存储层次设计的核心目标就是:尽量减少DRAM访问次数。
我的设计原则:
- 权重尽量常驻BRAM,不要频繁从DRAM加载
- 输入特征图按Tile切分,每个Tile在BRAM里算完再换下一个
- 部分和尽量在PE间传递,不要写回DRAM再读出来
举个例子。VLA模型里,Self-Attention的QKV计算需要三个权重矩阵。我习惯把这三个矩阵同时加载到BRAM里。虽然占用了3倍的BRAM空间,但避免了反复加载。你想想看,如果每次计算一个头就加载一次权重,那功耗就上去了。
3.6 实际设计中的权衡
嗯,这里要跟你聊点实际的。理论归理论,真正做设计时,你会发现很多矛盾点。
矛盾一:阵列大小 vs BRAM容量
阵列越大,并行度越高,但需要的BRAM也越大。我做过一个设计,128x128的阵列,光权重就要占掉16KB的BRAM。如果模型有几十层,BRAM根本不够用。我的做法是:把模型按层切分,每次只加载一层的权重到BRAM。虽然增加了层间切换的开销,但整体功耗反而降低了。
矛盾二:数据复用 vs 控制复杂度
Weight Stationary的数据复用率高,但控制逻辑复杂。每个PE需要知道什么时候加载权重,什么时候开始计算。Output Stationary控制简单,但数据复用率低。我个人的习惯是:对于规律性强的矩阵乘法(比如固定大小的全连接层),用Weight Stationary;对于不规则的计算(比如动态稀疏的注意力),用Output Stationary。
一个小技巧:如果你不确定选哪种数据流,可以先用RTL仿真跑一下。看哪个方案的DRAM访问次数少,就用哪个。我一般用这个指标作为第一判断标准。
好了,这一章的内容就这些。脉动阵列的原理、数据流的选择、存储层次的规划,都是低功耗推理的核心。下一章,我们会把这些东西串起来,做一个完整的VLA推理加速器设计。
公众号:蓝海资料掘金营,微信deep3321