脉动阵列架构基础
各位同学,今天我们来聊聊脉动阵列。说实话,我第一次接触这个概念是在做图像处理加速器的时候。当时被一堆里的术语搞得头晕,什么「数据复用」、「广播」、「权重固定」……后来真正动手做了一版设计,才明白这东西到底好在哪。
脉动阵列,说白了就是一种让数据像心跳一样在计算单元之间规律流动的架构。你想想看,传统的计算方式,数据从内存取出来,算完再存回去,来回折腾,功耗和延迟都上去了。脉动阵列的思路是:让数据待在阵列里,边移动边计算,一次取出来多用几次。
核心思想:每个处理单元(PE)只做简单的乘加运算,数据在相邻PE之间传递,避免频繁访问片外存储器。
数据复用与广播
这里有个关键问题:数据怎么在阵列里流动?我见过不少新手设计,上来就把所有数据同时广播到所有PE。结果呢?布线资源爆炸,时序收敛困难。其实脉动阵列有三种常见的数据流动方式:
- 权重固定(Weight Stationary):权重存在PE里不动,输入数据在阵列中流动。我习惯用这种方式做卷积,因为权重复用率高。
- 数据固定(Data Stationary):输入数据待在PE里,权重在阵列中流动。适合某些特定的矩阵运算场景。
- 输出固定(Output Stationary):部分和结果留在PE里,输入和权重都流动。这种方式我用的少,但有些低功耗设计会选它。
你可能会问:「这三种方式到底选哪个?」嗯,没有标准答案。我在做边缘检测加速器时,试过权重固定和数据固定两种方案。最后发现,如果卷积核尺寸小(比如3x3),权重固定更省带宽;如果输入特征图很大,数据固定反而更优。
我的经验:别纠结理论上的最优解。先搭一个简单的权重固定阵列跑仿真,看看数据复用率和带宽瓶颈在哪。很多时候,实际瓶颈不在计算,而在数据搬运。
经典脉动阵列结构
经典的脉动阵列长什么样?我画个图你就明白了。下面这个结构是二维脉动阵列,常用于矩阵乘法或卷积运算。
看到这个图了吗?每个PE只和上下左右的邻居通信。数据从左边流进来,权重从上面流下来,部分和往下累加。这种结构的好处是:每个PE只需要连接4个邻居,布线简单,频率容易跑高。
注意:我曾经在一个项目中,把阵列规模从8x8扩到16x16,结果时序直接崩了。原因很简单:PE之间的连线变长,组合逻辑延迟增加。所以阵列规模不是越大越好,要综合考虑工艺节点和时钟频率。
权重固定 vs 数据固定
这两种设计选择,我花了不少时间才真正搞明白。咱们用个表格对比一下:
| 特性 | 权重固定 | 数据固定 |
|---|---|---|
| 权重存储 | 存在PE本地寄存器 | 在阵列中流动 |
| 输入数据 | 在阵列中流动 | 存在PE本地寄存器 |
| 适用场景 | 卷积核复用率高(如3x3卷积) | 输入数据复用率高(如全连接层) |
| 带宽需求 | 低(权重一次加载多次使用) | 低(输入一次加载多次使用) |
| 控制复杂度 | 中等(需要控制权重加载时机) | 中等(需要控制输入加载时机) |
我个人更偏爱权重固定。为什么?因为卷积运算中,同一个卷积核要滑过整个特征图。权重固定后,一次加载就能用很多次,省带宽。我在做YOLO加速器时,权重固定方案让片外带宽需求降低了40%。
但数据固定也有它的舞台。比如在做全连接层时,输入数据是固定的特征向量,权重矩阵很大。这时候把输入数据固定在PE里,让权重流动,反而更高效。
避坑指南:我曾经犯过一个错误——在权重固定阵列里,忘了考虑权重更新的开销。每次更新权重时,所有PE都要重新加载,如果更新频繁,带宽反而比数据固定还高。所以,如果你的卷积核经常变(比如动态卷积),权重固定可能不是好选择。
一个简单的PE设计示例
说了这么多理论,咱们看个实际的PE代码。这是权重固定模式下,一个PE的简化Verilog描述:
module pe #(
parameter DATA_WIDTH = 8
)(
input clk,
input rst_n,
input [DATA_WIDTH-1:0] data_in, // 来自左侧PE的输入数据
input [DATA_WIDTH-1:0] weight_in, // 来自上方PE的权重(权重固定模式下,实际只加载一次)
input [DATA_WIDTH*2-1:0] psum_in, // 来自上方PE的部分和
output [DATA_WIDTH-1:0] data_out, // 输出到右侧PE
output [DATA_WIDTH-1:0] weight_out, // 输出到下方PE
output [DATA_WIDTH*2-1:0] psum_out // 输出到下方PE的部分和
);
reg [DATA_WIDTH-1:0] weight_reg; // 权重本地寄存器
reg [DATA_WIDTH*2-1:0] psum_reg;
// 权重加载(仅在权重更新时使能)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
weight_reg <= 0;
else if (weight_load_en)
weight_reg <= weight_in;
end
// 乘加运算
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
psum_reg <= 0;
else
psum_reg <= psum_in + data_in * weight_reg;
end
// 数据直通
assign data_out = data_in;
assign weight_out = weight_reg; // 权重固定模式下,向下传递的是本地存储的权重
assign psum_out = psum_reg;
endmodule
这段代码很简单,但包含了脉动阵列的核心:每个PE做乘加,数据直通,权重本地存储。实际项目中,你还需要考虑位宽扩展、饱和处理、流水线级数等问题。
好了,关于脉动阵列的基础就聊到这。记住一句话:脉动阵列的本质是用数据流动换带宽,用局部性换效率。下次你设计加速器时,不妨先画个数据流图,看看哪些数据可以复用,再决定用哪种固定方式。
一句话总结:脉动阵列不是银弹,但在卷积、矩阵乘法这类规则运算中,它确实是最优雅的硬件实现方式之一。
可可精品资料库