一维脉动阵列:向量点积的实现
好,咱们今天来聊一个非常经典的结构——一维脉动阵列实现向量点积。说白了,就是让数据从两端流进来,计算结果在中间汇聚。这个模式我在很多加速器里都见过,它也是理解更复杂脉动阵列的基础。
向量点积的数学本质
先回顾一下向量点积。两个长度为 N 的向量 A 和 B,点积就是对应元素相乘再求和:
C = A[0]*B[0] + A[1]*B[1] + ... + A[N-1]*B[N-1]
这个计算看起来简单,但如果你用传统的 CPU 或 GPU 去算,数据搬运的开销往往比计算本身还大。我当年做图像处理加速时,就遇到过这个问题——算力不是瓶颈,数据喂不进去才是。
脉动阵列怎么解决?它把乘法器和累加器排成一排,让数据像流水一样流过每个单元。每个单元只做一件事:乘一个数,加一个数,然后传给下一个。
一维阵列的结构设计
我们来看一个具体的例子。假设 N=4,我们需要 4 个处理单元(PE)。每个 PE 内部有一个乘法器和一个累加器。
数据流是这样设计的:
- A 向量从阵列左端流入,每个时钟周期向右移动一个 PE
- B 向量从阵列右端流入,每个时钟周期向左移动一个 PE
- 每个 PE 将自己收到的 A 和 B 相乘,然后累加到本地结果中
- 最终结果从中间的某个 PE 输出
嗯,这里要注意:数据必须同步。A 和 B 的流动速度要一致,否则乘错对就全乱了。
核心思想:每个 PE 只做一次乘加,数据在 PE 之间流动,避免了全局广播。这就是脉动阵列省带宽的秘密。
数据流时序分析
我们用一个 4 单元阵列来演示。假设 A = [a0, a1, a2, a3],B = [b0, b1, b2, b3]。
时钟周期 0:
- PE0 收到 a0(从左)和 b3(从右),计算 a0 * b3,累加
- 其他 PE 空闲
时钟周期 1:
- a0 移到 PE1,a1 进入 PE0
- b3 移到 PE2,b2 进入 PE3
- PE0:a1 * b2,累加
- PE1:a0 * b3,累加
- PE2、PE3 空闲
时钟周期 2:
- a0 到 PE2,a1 到 PE1,a2 到 PE0
- b3 到 PE1,b2 到 PE2,b1 到 PE3
- PE0:a2 * b1
- PE1:a1 * b2 + a0 * b3
- PE2:a0 * b3
- PE3 空闲
继续推演下去,你会发现:
| 时钟周期 | PE0 累加值 | PE1 累加值 | PE2 累加值 | PE3 累加值 |
|---|---|---|---|---|
| 0 | a0*b3 | 0 | 0 | 0 |
| 1 | a1*b2 | a0*b3 | 0 | 0 |
| 2 | a2*b1 | a1*b2 + a0*b3 | a0*b3 | 0 |
| 3 | a3*b0 | a2*b1 + a1*b2 + a0*b3 | a1*b2 + a0*b3 | a0*b3 |
| 4 | 0 | a3*b0 + a2*b1 + a1*b2 + a0*b3 | a2*b1 + a1*b2 + a0*b3 | a1*b2 + a0*b3 |
看到了吗?在时钟周期 4,PE1 的累加值正好是完整的点积结果。这就是「结果在中间汇聚」的含义——数据从两端流入,在阵列中间完成最终求和。
个人经验:我习惯把这种结构叫做「对撞式脉动阵列」。数据像两列火车对开,在中间交汇处完成计算。设计时要注意,结果输出的位置取决于阵列长度是奇数还是偶数。奇数时正中间那个 PE 输出,偶数时中间偏左或偏右的那个输出。
SVG 结构图:一维脉动阵列数据流
下面这张图展示了 4 单元阵列在时钟周期 3 时的数据分布和计算状态:
硬件实现要点
如果你要真正去写 RTL 实现这个阵列,有几个地方得特别小心:
我曾经踩过的坑:第一次做这个设计时,我忘了考虑数据对齐。A 和 B 的输入时序必须精确匹配,差一个周期结果就全错了。我的建议是:先画好时序图,再写代码。
具体来说,每个 PE 的 Verilog 代码大概长这样:
module pe (
input clk,
input rst_n,
input [7:0] a_in, // 从左端流入的数据
input [7:0] b_in, // 从右端流入的数据
output reg [7:0] a_out, // 向右端传递的数据
output reg [7:0] b_out, // 向左端传递的数据
output reg [15:0] acc // 累加结果
);
reg [15:0] mul_result;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
a_out <= 8'd0;
b_out <= 8'd0;
acc <= 16'd0;
end else begin
a_out <= a_in; // 数据向右传递
b_out <= b_in; // 数据向左传递
mul_result <= a_in * b_in;
acc <= acc + mul_result; // 累加
end
end
endmodule
你看,每个 PE 的逻辑其实非常简单。但就是这些简单的 PE 排成一排,配合精确的时序控制,就能高效完成向量点积。
性能分析
这个结构的性能怎么样?我们来算一笔账:
| 指标 | 传统串行计算 | 一维脉动阵列 |
|---|---|---|
| 计算周期数(N=4) | 4 次乘 + 3 次加 = 7 周期 | 4 周期(流水线填满后) |
| 数据带宽需求 | 每次需要读两个数 | 每个周期只需读两个数(流式) |
| 硬件利用率 | 100%(单 PE) | 75%(前 3 周期未填满) |
| 扩展性 | 线性增加时间 | 线性增加硬件,时间不变 |
说白了,脉动阵列用面积换时间。N 个 PE 并行工作,把 O(N) 的时间复杂度降到了 O(1)(流水线填满后)。代价是硬件面积增加了 N 倍。
我的建议:如果你的向量长度很大(比如 1024),不要真的摆 1024 个 PE。实际工程中常用「分块」策略——用固定大小的阵列(比如 64 个 PE)分多次处理长向量。这个我们后面章节会详细讲。
小结
一维脉动阵列实现向量点积,核心就三点:
- 数据对撞:A 从左向右,B 从右向左,在中间相遇
- 流水计算:每个 PE 只做一次乘加,结果逐级累加
- 结果汇聚:最终结果在阵列中间的 PE 输出