2. 网络模型选择与量化:适合FINN的模型结构、量化感知训练(QAT)原理、FINN的量化配置、模型导出为ONNX
好,咱们进入第二章。这一章很关键,说白了就是决定你的模型能不能在FPGA上跑得又快又准。我见过太多人模型选错了,量化没做好,最后部署时性能惨不忍睹。咱们一步步来。
2.1 什么样的模型结构适合FINN?
FINN这框架,它天生是为极度量化的模型设计的。你想想看,FPGA上最擅长什么?是位运算、是流水线。所以,适合FINN的模型,得满足几个硬性条件。
核心原则:模型结构要「规整」,避免不规则的计算流。
我个人习惯把适合的模型分成三类:
- 纯卷积网络(如VGG风格):全是3x3或1x1卷积,没有分支,没有残差连接。这种结构在FINN里最友好,因为数据流是线性的,硬件实现起来最简单。我在项目中用过一个类似VGG-11的模型,量化到4bit,在Zynq上跑出了惊人的吞吐量。
- 深度可分离卷积(如MobileNet风格):虽然有点复杂,但FINN对深度可分离卷积的支持已经不错了。注意,这里有个坑——逐点卷积(1x1)在FINN里会被优化成矩阵乘法,效率极高。
- 二值/三值网络(如BinaryNet、TernaryNet):这是FINN的「亲儿子」。权重和激活都只有1bit或2bit,硬件上直接用XNOR和popcount搞定。我记得有一次做手势识别,用二值网络,精度只掉了1%,但延迟从毫秒级降到了微秒级。
避坑指南:我曾经把一个带有大量残差连接的ResNet-50直接塞进FINN,结果资源消耗爆炸。为什么?因为残差连接需要额外的加法器和数据缓存,在FPGA上非常吃资源。如果你非要用ResNet,建议选ResNet-18这种浅层的,或者把残差块的数量砍一砍。
另外,不要用带有以下结构的模型:
- 动态形状(如可变长输入)
- 循环结构(如RNN、LSTM)
- 复杂的注意力机制(如Transformer中的多头注意力)
这些在FINN里要么不支持,要么效率极低。
2.2 量化感知训练(QAT)原理
好,模型选好了,接下来就是量化。很多人觉得量化就是训练完模型后,直接转成int8就完事了。嗯,这种做法叫训练后量化(PTQ),精度往往掉得厉害。真正好用的是量化感知训练(QAT)。
QAT的原理,说白了就是在训练过程中模拟量化误差。你想想看,模型在训练时用的是浮点数,但部署时用的是定点数。这两者之间有差距。QAT就是让模型在训练时就「习惯」这种误差,从而在部署时保持精度。
具体怎么做?核心是直通估计器(STE)。前向传播时,我们把浮点权重和激活量化成定点数(比如int8),但反向传播时,我们假装量化函数是可微的,直接用浮点梯度去更新浮点权重。这听起来有点「作弊」,但实践证明非常有效。
我的经验:QAT训练时,学习率要调小一点,一般是原浮点模型学习率的1/10到1/100。我刚开始做QAT时,直接用原来的学习率,结果模型直接不收敛了。后来改成1e-4,效果就好了很多。
QAT的典型流程是这样的:
- 先训练一个浮点模型,达到不错的精度。
- 在模型中插入伪量化节点(FakeQuantize),这些节点会模拟量化过程。
- 用较小的学习率继续训练几个epoch,让模型适应量化。
- 导出量化后的模型。
这里有个关键点:伪量化节点的位置。一般来说,权重和激活都需要量化。权重量化在卷积层之前,激活量化在激活函数之后。我见过有人只量化权重不量化激活,结果精度还行,但硬件实现时发现激活值范围太大,根本没法用。
2.3 FINN的量化配置
FINN的量化配置,是通过一个叫QuantConfig的类来管理的。我个人习惯把它放在一个单独的YAML文件里,方便调试。
先看一个典型的配置示例:
# finn_quant_config.yaml
quant_config:
# 权重量化
weight:
bit_width: 4
min: -1.0
max: 1.0
strategy: "minmax" # 可选: minmax, percentile, mse
# 激活量化
activation:
bit_width: 4
min: 0.0
max: 6.0
strategy: "percentile"
percentile: 99.9
# 偏置量化(通常不量化,或者用更高精度)
bias:
bit_width: 8
strategy: "minmax"
这里有几个参数需要特别注意:
| 参数 | 说明 | 我的建议 |
|---|---|---|
bit_width |
量化位宽,常见的有1、2、4、8 | 4bit是个甜点,精度和效率平衡得很好 |
min/max |
量化范围,决定了浮点数到定点数的映射 | 不要手动设死,用策略自动计算 |
strategy |
计算量化范围的方法 | 激活用percentile,权重用minmax |
percentile |
百分位值,用于排除异常值 | 99.9%是个不错的起点 |
重要提示:量化范围的选择直接影响精度。范围太小会截断大量值,范围太大会浪费量化精度。我建议先用一小批数据跑一下,看看激活值的分布,再决定范围。
在FINN中,量化配置是通过brevitas库来实现的。Brevitas是Xilinx专门为FINN开发的量化训练库。你需要在模型中用brevitas.nn.QuantConv2d替换普通的nn.Conv2d,然后传入量化配置。
import brevitas.nn as qnn
# 定义一个量化卷积层
self.conv1 = qnn.QuantConv2d(
in_channels=3,
out_channels=64,
kernel_size=3,
weight_bit_width=4,
bias_bit_width=8,
weight_quant_type="int",
bias_quant_type="int",
input_quant_type="int",
input_bit_width=4
)
嗯,这里要注意,input_quant_type和input_bit_width是控制输入激活的量化。如果你不设,默认是不量化的,那你的模型就变成了「权重量化,激活浮点」,这在FINN里是跑不起来的。
2.4 模型导出为ONNX
模型训练好了,量化配置也调好了,最后一步就是导出为ONNX。FINN的输入格式就是ONNX,所以这一步很关键。
导出ONNX,我用的是torch.onnx.export。但有几个坑,我踩过,你们别踩。
我曾经犯过的错:直接导出整个模型,结果ONNX文件里包含了训练相关的节点(比如损失函数、优化器),导致FINN解析失败。正确的做法是只导出model.eval()后的推理部分。
正确的导出代码:
import torch
import brevitas.onnx as bo
# 切换到评估模式
model.eval()
# 创建一个示例输入
dummy_input = torch.randn(1, 3, 32, 32)
# 导出ONNX
bo.export_brevitas_onnx(
model,
dummy_input,
"model_quant.onnx",
export_q_node=True # 保留量化信息
)
这里有个关键参数export_q_node,一定要设为True。为什么?因为FINN需要从ONNX中读取量化信息(比如位宽、缩放因子),如果没有这些信息,FINN就不知道该怎么在硬件上实现量化计算。
导出后,我建议用onnxruntime验证一下:
import onnxruntime as ort
sess = ort.InferenceSession("model_quant.onnx")
input_name = sess.get_inputs()[0].name
output_name = sess.get_outputs()[0].name
# 用同样的输入跑一下
result = sess.run([output_name], {input_name: dummy_input.numpy()})
print("ONNX推理结果:", result)
如果结果和PyTorch推理的结果一致(误差在1e-5以内),那就说明导出成功了。如果不一致,多半是量化配置有问题,或者模型里有一些FINN不支持的算子。
小技巧:导出ONNX后,可以用netron工具可视化一下,看看模型结构是否完整,量化节点是否都在。我每次导出后都会看一眼,确认没问题再进FINN流程。
好了,这一章的内容就到这里。模型选型、QAT训练、量化配置、ONNX导出,这四个步骤环环相扣。下一章,我们会把导出的ONNX模型送进FINN的编译流程,看看它到底是怎么变成FPGA上跑的硬件的。