4. FPGA加速入门:HLS开发流程、流水线设计、定点数优化
FPGA 加速,很多人一听就觉得门槛高。得写 Verilog,得懂时序,还得会看波形图。
说实话,我早年也是这么想的。直到我接触了 HLS(High-Level Synthesis,高层次综合),才发现原来用 C/C++ 也能写 FPGA 逻辑。当然,不是说 HLS 能解决所有问题,但它确实让算法工程师和软件出身的人,有了快速上手 FPGA 的路径。
这一章,我就带你走一遍 HLS 的核心流程。重点聊聊三个东西:开发流程、流水线设计、定点数优化。这三板斧砍下来,你基本就能把一段 C 代码,变成一块能跑在板子上的加速逻辑。
4.1 HLS 开发流程:从 C 到 RTL
HLS 的本质,是把 C/C++ 描述的算法,自动转换成硬件描述语言(VHDL/Verilog)。你想想看,这相当于让编译器帮你写 RTL 代码。
但别高兴太早。编译器没那么聪明,它需要你给它“提词”——也就是用 pragma 指令或者 directive 来指导它。
一个典型的 HLS 开发流程,分五步走:
- 算法建模:用 C/C++ 写出功能正确的算法。这一步跟写软件没区别。
- C 仿真验证:在 PC 上跑一遍,确保逻辑对。这一步很快,几秒钟就出结果。
- 综合(Synthesis):HLS 工具把 C 代码转成 RTL。这一步会生成报告,告诉你用了多少资源、能跑多快。
- 协同仿真(Co-simulation):把生成的 RTL 和你的测试激励一起仿真。这一步比较慢,但必须做。
- 导出 IP:把生成的 RTL 打包成 IP 核,供 Vivado 等工具调用。
核心要点:HLS 不是“一键生成”。它更像一个翻译官,你需要告诉它“这里要并行”、“那里要流水线”、“这个变量用寄存器”。
我个人习惯,在写 HLS 代码之前,会先画一个数据流图。把输入、输出、中间计算节点都标清楚。这样写 pragma 的时候,心里才有底。
4.2 流水线设计:让数据“流”起来
流水线,是 FPGA 加速的灵魂。没有流水线,你的硬件就跟单核 CPU 一样,一条指令一条指令地跑,浪费了 FPGA 的并行能力。
举个例子。假设你要计算 y = a * b + c。如果不做流水线,硬件会先算乘法,等乘法结果出来,再算加法。这中间,乘法器在忙的时候,加法器在闲着。
做了流水线之后呢?乘法器算完第一组数据,立刻把结果传给加法器,同时乘法器开始算第二组数据。这样一来,乘法器和加法器都在满负荷工作。
在 HLS 里,开启流水线非常简单,加一行 pragma 就行:
// 对循环体做流水线
for (int i = 0; i < N; i++) {
#pragma HLS PIPELINE II=1
out[i] = a[i] * b[i] + c[i];
}
这里的 II=1 是关键。II 是 Initiation Interval(启动间隔),意思是每隔 1 个时钟周期,就能启动一次新的计算。II=1 是流水线的最高境界,意味着你的硬件达到了“每个周期出一个结果”的吞吐率。
避坑指南:我曾经在一个项目中,为了追求 II=1,把代码改得面目全非。结果综合出来资源爆了,时序也跑不过。后来我学乖了,II 值不是越小越好,要根据你的数据依赖和资源情况来定。II=2 或者 II=3,有时候反而是最优解。
为什么会这样?因为流水线有“气泡”。如果循环体内部有数据依赖,比如当前迭代需要用到上一次迭代的结果,那流水线就必须停下来等。这时候,II 就没办法做到 1。
所以,写 HLS 代码的时候,要尽量消除循环体内的数据依赖。把能提前算的,都提到循环外面去。
4.3 定点数优化:算得快,还省资源
FPGA 做浮点运算,代价很高。一个单精度浮点乘法器,要占几百个 LUT 和 DSP。而一个定点乘法器,可能只需要几十个 LUT。
说白了,能用定点,就别用浮点。这是 FPGA 加速的铁律。
HLS 里,定点数用 ap_fixed<W, I> 来表示。W 是总位宽,I 是整数部分位宽。小数部分位宽就是 W - I。
举个例子:
#include <ap_fixed.h>
// 定义一个 16 位定点数,8 位整数,8 位小数
typedef ap_fixed<16, 8> fixed16_t;
fixed16_t a = 3.25; // 实际存储为 3.25 * 2^8 = 832
fixed16_t b = 1.5;
fixed16_t c = a * b; // 结果也是定点数
定点数的好处,不仅仅是省资源。它还能让你的设计跑在更高的频率上。因为位宽小了,组合逻辑的路径就短了,时序自然就好。
但定点数也有坑。最大的坑就是 精度丢失 和 溢出。
| 问题 | 原因 | 解决办法 |
|---|---|---|
| 精度丢失 | 小数位不够,截断导致误差 | 增加小数位宽,或者做四舍五入 |
| 溢出 | 整数位不够,结果超出表示范围 | 增加整数位宽,或者做饱和截断 |
| 乘法位宽膨胀 | 两个 W 位定点数相乘,结果需要 2W 位 | 及时截断,或者用 ap_fixed 的缩放模式 |
注意:定点数的位宽选择,一定要做仿真验证。我见过有人凭感觉选了 16 位,结果跑出来的结果跟浮点版本差了 10%。后来一查,是小数位不够,累积误差太大了。所以,先仿真,再综合,这个顺序不能乱。
我个人习惯,在做定点数优化时,会先写一个 Python 脚本。用 Python 的 numpy 模拟定点数的行为,把位宽、截断模式、饱和模式都试一遍。找到最优的位宽组合后,再移植到 HLS 代码里。这样能省很多综合时间。
4.4 知识结构总览
下面这张图,把 HLS 开发、流水线、定点数优化的核心关系画出来了。你可以把它当作本章的“地图”。
从这张图你能看到,HLS 开发流程是骨架,流水线设计和定点数优化是两条腿。资源与性能是最终的目标。三者缺一不可。
4.5 实战建议:从一个小例子开始
如果你刚接触 HLS,我建议你别一上来就搞复杂的神经网络。先拿一个 FIR 滤波器或者矩阵乘法练手。
步骤很简单:
- 先写 C 代码,跑通功能。
- 加
PIPELINEpragma,看 II 值能不能到 1。 - 把浮点改成定点,对比精度和资源。
- 最后综合,看时序报告。
嗯,这里要注意。HLS 工具生成的报告,有时候会“骗人”。它说能跑 200MHz,但放到 Vivado 里布局布线后,可能只能跑 150MHz。所以,一定要做布局布线后的时序仿真,别只看 HLS 的报告。
我记得有一次,HLS 报告说资源用了 30%,结果布局布线后,BRAM 爆了。原因是 HLS 把数组综合成了分布式 RAM,而 Vivado 把它映射到了 BRAM 上。从那以后,我每次都会在综合后,手动检查一下资源映射情况。
一句话总结:HLS 是工具,不是魔法。你越懂硬件,HLS 给你的回报就越大。流水线让数据流起来,定点数让资源省下来。这两件事做好了,你的 FPGA 加速就成功了一大半。