3. 硬件架构设计:脉动阵列原理、数据流优化与片上存储层次设计

好,咱们进入这一章的核心——硬件架构。说实话,VLA模型推理最吃计算资源的部分就是矩阵乘法。而脉动阵列,就是专门为这个场景量身定做的。

我个人习惯把脉动阵列想象成一个“数据流水线工厂”。数据像流水一样,有节奏地流过每个处理单元(PE)。每个PE只做一件事:乘加运算。但组合起来,效率惊人。

3.1 脉动阵列基本原理

脉动阵列的核心思想很简单:数据复用。你想想看,矩阵乘法里,一个权重会被反复使用。如果每次用都从内存里读,功耗就上去了。脉动阵列让数据在PE之间“流动”,避免频繁访存。

核心优势:

  • 数据复用率高,减少片外访存
  • PE结构简单,适合FPGA实现
  • 流水线化操作,吞吐量高

我在项目中遇到过一个问题:刚开始做脉动阵列时,总觉得PE越多越好。结果发现,阵列大了,布线延迟反而成了瓶颈。嗯,这里要注意:阵列大小要跟片上存储和时钟频率匹配

下面这张图,是我自己总结的脉动阵列核心逻辑。你看一眼就明白了。

脉动阵列数据流示意图 权重流 (Weight Stationary) PE₀₀ PE₀₁ PE₀₂ PE₀₃ PE₁₀ PE₁₁ PE₁₂ PE₁₃ PE₂₀ PE₂₁ PE₂₂ PE₂₃ PE₃₀ PE₃₁ PE₃₂ PE₃₃ 输入特征图流(垂直方向) 部分和流(水平方向) 权重(驻留在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资源有限,不能什么都往里塞。

我一般把存储分成三级:

  1. 寄存器(Register):速度最快,容量最小。给PE内部用,存当前计算的权重或部分和。
  2. BRAM(Block RAM):速度中等,容量适中。存输入特征图Tile和权重Tile。
  3. 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