第二节:卷积运算原理与硬件映射
各位同学好,我是你们的FPGA讲师。今天我们来聊聊卷积加速器最核心的部分——卷积运算到底是怎么映射到硬件上的。
说实话,很多初学者一上来就盯着代码看,结果越看越迷糊。我建议你先搞懂数学原理,再谈硬件实现。这个顺序千万别搞反了。
一、卷积计算的数学本质
卷积运算,说白了就是「滑动窗口乘累加」。你想想看,一个3×3的卷积核,在输入特征图上从左到右、从上到下地滑动,每次覆盖9个像素,然后做9次乘法、8次加法,得到一个输出像素值。
数学表达式很简单:
Output(x, y) = Σ Σ Input(x+i, y+j) × Weight(i, j)
i j
其中i和j的取值范围是卷积核的尺寸。比如3×3卷积,i和j都是从0到2。
核心要点:卷积的本质就是「局部加权求和」。每个输出像素只依赖输入的一个局部区域,这个特性决定了我们后面所有的硬件优化策略。
我在项目中遇到过一个问题:有人把卷积和全连接搞混了。全连接是每个输出和所有输入都有关,而卷积是局部连接。这个区别直接决定了硬件架构的复杂度——卷积的并行度可以做得很高,全连接就不行。
二、数据复用策略——为什么需要复用?
好,现在问题来了。一个典型的卷积层,输入是224×224×3,卷积核是3×3×3×64,输出是224×224×64。你算算总共需要多少次乘累加?
答案是:224×224×64×3×3×3 ≈ 8.7亿次。
如果每次运算都从DDR里读数据,那带宽根本扛不住。所以必须做数据复用。我常用的复用策略有三种:
1. 输入特征图复用(Input Reuse)
同一个输入像素会被多个卷积核用到。比如一个3×3的输入块,会被64个不同的卷积核分别计算。所以我们可以把输入数据存在片上BRAM里,反复使用。
2. 权重复用(Weight Reuse)
同一个卷积核会在输入图上滑动很多次。比如一个3×3的卷积核,在224×224的图上要滑动224×224次。所以权重数据也可以存在片上,一次加载多次使用。
3. 输出复用(Output Reuse)
这个稍微绕一点。多个输入通道累加得到同一个输出像素时,中间结果可以暂存在寄存器里,等所有通道算完再写回。
| 复用类型 | 复用次数 | 存储位置 | 典型场景 |
|---|---|---|---|
| 输入复用 | 输出通道数 | BRAM | 多卷积核并行 |
| 权重复用 | 输出特征图尺寸 | BRAM/URAM | 大尺寸输入图 |
| 输出复用 | 输入通道数 | 寄存器 | 多通道累加 |
我的经验:实际项目中很少只用一种复用策略。我一般会根据资源情况混合使用。比如BRAM多的芯片,我就多存输入数据;LUT多的芯片,我就多展开并行计算。
三、硬件架构映射方法
搞懂了数学原理和数据复用,接下来就是怎么把这些东西映射到FPGA上。嗯,这里要注意,映射不是简单的「把公式翻译成Verilog」,而是要设计一个高效的流水线架构。
3.1 基本计算单元——乘累加器(MAC)
最基础的计算单元就是MAC。一个MAC包含一个乘法器和一个加法器。在FPGA里,我通常直接用DSP48模块来实现。
// 一个简单的MAC单元
module mac_unit (
input clk,
input rst_n,
input [7:0] data_in,
input [7:0] weight_in,
input valid_in,
output reg [15:0] result_out,
output reg valid_out
);
reg [15:0] acc;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
acc <= 0;
valid_out <= 0;
end else if (valid_in) begin
acc <= acc + data_in * weight_in;
valid_out <= 1;
end
end
assign result_out = acc;
endmodule
这个代码很简单,但实际项目中我会加一个清零信号。因为每次滑动窗口结束后,累加器需要复位,准备下一次计算。
3.2 并行计算阵列——脉动阵列(Systolic Array)
单个MAC太慢了。我们需要把多个MAC排成阵列,同时计算。这就是脉动阵列的思想。
我画个图帮你理解:
这个阵列里,数据从左边流入,权重从上边流入。每个MAC只和相邻的MAC通信,没有全局连线。这样做的好处是:
- 局部性:每个MAC只和邻居通信,布线简单
- 可扩展:想增加计算能力,直接加行加列就行
- 高吞吐:每个时钟周期,所有MAC同时计算
避坑指南:我曾经在一个项目里把脉动阵列做得太大,结果时序跑不过。后来发现是阵列规模超过了芯片的DSP资源。所以设计前一定要先算好资源预算——你用的FPGA有多少个DSP48?每个MAC用几个?阵列规模不能超过这个数。
3.3 数据调度——行缓冲器(Line Buffer)
脉动阵列解决了计算问题,但数据怎么喂进去?这就轮到行缓冲器出场了。
行缓冲器的思路很简单:用3个(对于3×3卷积)行缓冲器,每行存输入特征图的一行数据。每次从3行缓冲器里各取3个像素,组成一个3×3的窗口,送给脉动阵列。
// 行缓冲器实现示意
module line_buffer #(
parameter WIDTH = 8,
parameter LINE_LEN = 224
)(
input clk,
input rst_n,
input [WIDTH-1:0] data_in,
input valid_in,
output [WIDTH-1:0] data_out
);
reg [WIDTH-1:0] buffer [0:LINE_LEN-1];
reg [7:0] wr_ptr, rd_ptr;
always @(posedge clk) begin
if (valid_in) begin
buffer[wr_ptr] <= data_in;
wr_ptr <= wr_ptr + 1;
end
end
assign data_out = buffer[rd_ptr];
endmodule
实际项目中,我会用3个这样的行缓冲器,再加上一个3×3的寄存器窗口,组成完整的「数据读取-窗口生成-计算」流水线。
四、完整的硬件映射流程
好了,我们把所有东西串起来。一个完整的卷积加速器硬件架构是这样的:
- 数据加载:从DDR读取输入特征图和权重,存入片上BRAM
- 窗口生成:行缓冲器+寄存器窗口,生成3×3的输入块
- 并行计算:脉动阵列同时计算多个输出通道
- 累加输出:多通道累加,得到最终输出像素
- 数据写回:输出结果写回DDR
这5步是流水线执行的。也就是说,当第1步在处理第N行数据时,第2步已经在处理第N-1行,第3步在处理第N-2行...这样每个时钟周期都能出一个结果。
我的建议:刚开始做卷积加速器时,别一上来就搞大阵列。先做一个3×3的脉动阵列,跑通一个简单的卷积层。验证没问题了,再考虑扩展到更大的阵列。我当年就是这么过来的,稳扎稳打比一步到位靠谱得多。
最后说一句,卷积运算的硬件映射,核心就是「用空间换时间」。你多用一些DSP和BRAM,就能换来更高的吞吐率。但资源是有限的,所以需要在面积和速度之间做权衡。这个权衡的艺术,就是卷积加速器设计的精髓所在。
可可精品资料库