第四章:FPGA平台部署
好,咱们进入FPGA部署这一章。说实话,很多做AI的同学一听到FPGA就觉得头大——硬件描述语言、时序约束、资源利用率……听着就劝退。但我想说,别怕。FPGA部署其实没那么玄乎,尤其是现在有了HLS和Vitis AI这些工具链,门槛已经降了很多。
我个人习惯把FPGA部署分成三个层次:硬件架构理解、高层次综合、AI工具链使用。咱们一层层来拆。
4.1 FPGA架构基础
先聊聊FPGA到底是个什么东西。说白了,它就是一盒乐高积木。你买回来的时候,里面的逻辑单元、DSP块、BRAM都是散的,你想搭什么电路就搭什么电路。跟CPU不一样,CPU是固定好的——指令怎么取、怎么译码、怎么执行,都是焊死的。FPGA呢?你可以自己定义。
FPGA的核心部件有这么几个:
- 逻辑单元(Logic Cells):也叫LUT+FF,用来实现组合逻辑和时序逻辑。我刚开始做的时候,总觉得这东西不够用,后来发现其实是代码写得不够优化。
- DSP块:专门做乘加运算的硬核。做AI推理的时候,卷积运算全靠它。我记得有一次项目,DSP块用超了,只能降频跑,那叫一个难受。
- BRAM/URAM:片上存储。速度极快,但容量有限。模型参数放不下的时候,就得往DDR里塞,延迟就上来了。
- 可编程互联:这些乐高块之间的连线。嗯,这里要注意——布线资源有时候比逻辑资源更稀缺。
为什么会这样?因为FPGA的布线是固定的,你逻辑放得再漂亮,布不通也是白搭。我在项目中遇到过好几次,综合通过、实现报错,一看全是布线拥塞。
4.2 HLS高层次综合
好,架构看完了,怎么编程?传统做法是写Verilog或VHDL,但说实话,那玩意儿写起来太痛苦了。一个卷积核,C语言三行搞定,Verilog能写三百行。所以就有了HLS——高层次综合。
HLS允许你用C/C++写代码,然后自动转成硬件描述。听起来很美好对吧?但我要泼一盆冷水:HLS不是魔法。你写出来的C代码如果不考虑硬件特性,综合出来的电路效率会非常低。
举个例子,我早期做过一个项目,用HLS写了一个简单的矩阵乘法。C代码跑仿真没问题,但综合出来的延迟高得离谱。后来一查,发现是循环没有做流水线优化。HLS默认是串行执行的,你得手动加#pragma HLS pipeline才能让它并行。
常用的HLS优化指令:
| 指令 | 作用 | 使用场景 |
|---|---|---|
#pragma HLS pipeline |
循环流水线化 | 卷积、矩阵运算 |
#pragma HLS unroll |
循环展开 | 小循环、固定步长 |
#pragma HLS array_partition |
数组分块 | 提高BRAM读写带宽 |
#pragma HLS dataflow |
任务级流水 | 多模块并行 |
看一段实际代码,一个简单的卷积层HLS实现:
void conv2d(
float in[IN_SIZE][IN_SIZE][IN_CH],
float out[OUT_SIZE][OUT_SIZE][OUT_CH],
float weight[K][K][IN_CH][OUT_CH],
float bias[OUT_CH]
) {
#pragma HLS INTERFACE m_axi port=in depth=100000
#pragma HLS INTERFACE m_axi port=out depth=100000
#pragma HLS INTERFACE s_axilite port=return
for (int row = 0; row < OUT_SIZE; row++) {
for (int col = 0; col < OUT_SIZE; col++) {
for (int ch_out = 0; ch_out < OUT_CH; ch_out++) {
#pragma HLS PIPELINE
float sum = bias[ch_out];
for (int kx = 0; kx < K; kx++) {
for (int ky = 0; ky < K; ky++) {
for (int ch_in = 0; ch_in < IN_CH; ch_in++) {
sum += in[row+kx][col+ky][ch_in] * weight[kx][ky][ch_in][ch_out];
}
}
}
out[row][col][ch_out] = sum;
}
}
}
}
注意看,我在最内层的循环前加了PIPELINE指令。这样综合工具就会尝试让每个时钟周期都输出一个结果。如果不加,那就要等所有循环跑完才能输出,延迟差几十倍。
4.3 Vitis AI工具链使用
好了,HLS讲完了,但咱们做AI部署的,总不能手写每个算子吧?这时候Vitis AI就派上用场了。它是Xilinx推出的AI推理工具链,说白了就是帮你把训练好的模型转成能在FPGA上跑的指令流。
Vitis AI的流程大致是这样的:
- 模型量化:把FP32的权重转成INT8。FPGA做INT8运算比FP32快得多,资源也省。
- 编译:把量化后的模型编译成DPU(深度学习处理单元)能识别的指令。
- 部署:把编译好的文件烧到FPGA上,跑推理。
我个人觉得,Vitis AI最方便的地方在于它支持PyTorch和TensorFlow。你训练好的模型,直接导出成ONNX或者XModel,丢给Vitis AI就行。不用自己写硬件代码,这点对算法工程师特别友好。
举个例子,部署一个ResNet-50:
# 1. 量化
vai_q_tensorflow quantize \
--input_frozen_graph frozen_graph.pb \
--input_nodes input \
--output_nodes softmax_tensor \
--input_shapes ?,224,224,3 \
--calib_dataset ./calib_data \
--output_dir ./quantized
# 2. 编译
vai_c_tensorflow \
--frozen_pb ./quantized/deploy_model.pb \
--arch /opt/vitis_ai/compiler/arch/DPUCZDX8G/ZCU102/arch.json \
--output_dir ./compiled \
--net_name resnet50
# 3. 运行推理(Python API)
from vitis_ai.python import Runner
runner = Runner("./compiled/resnet50.xmodel")
input_data = preprocess(image)
output_data = runner.run(input_data)
result = postprocess(output_data)
看到没?三行Python就搞定了推理。但这里有个坑——量化校准。你量化的时候需要提供校准数据集,这个数据集要能代表真实推理时的数据分布。我见过有人随便拿几张图做校准,结果量化后精度掉了5个点,那叫一个惨。
4.4 模型编译与部署
最后一步,编译和部署。Vitis AI的编译器会把模型解析成DPU指令,然后生成一个.xmodel文件。这个文件就是FPGA上跑的“可执行程序”。
编译的时候有几个参数要特别注意:
- DPU架构:不同的FPGA芯片有不同的DPU配置。ZCU102用的是DPUCZDX8G,而Alveo卡用的是DPUCAHX8H。别搞混了。
- 并行度:可以设置DPU同时处理多少个卷积核。并行度越高,吞吐量越大,但资源消耗也越大。我一般从8开始试,不够再加。
- 输入输出形状:要跟模型定义一致。有一次我改了输入尺寸忘了改编译参数,跑出来的结果全是错的。
部署的时候,通常有两种方式:
| 方式 | 特点 | 适用场景 |
|---|---|---|
| 嵌入式部署 | FPGA上跑Linux,直接调用DPU驱动 | 边缘设备、嵌入式系统 |
| PCIe加速卡 | FPGA插在服务器上,通过PCIe通信 | 数据中心、云端推理 |
嵌入式部署的话,你得先给FPGA烧一个包含DPU的bitstream,然后启动Linux,再加载模型。PCIe方式就简单一些,插上卡,装好驱动,直接调用API就行。
我记得有一次做嵌入式部署,板子死活起不来。查了半天,发现是DDR配置不对。FPGA的DDR控制器需要跟板子上的DDR颗粒匹配,频率、时序、型号,差一点都不行。从那以后,我每次拿到新板子,第一件事就是确认DDR配置。
总结一下FPGA部署的关键点:
- 先搞清楚芯片资源,别超了
- HLS写代码要想着硬件,加好pipeline和unroll
- Vitis AI量化时校准数据集要靠谱
- 编译参数跟模型定义要一致
- 部署前先确认DDR和时钟配置
FPGA部署这条路,说难也难,说简单也简单。难在硬件细节多,简单在工具链已经帮你做了大部分工作。你只要把上面这些点都踩一遍,基本就能跑起来了。嗯,下一章咱们聊聊GPU部署,那个又是另一番天地了。