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的典型流程是这样的:

  1. 先训练一个浮点模型,达到不错的精度。
  2. 在模型中插入伪量化节点(FakeQuantize),这些节点会模拟量化过程。
  3. 用较小的学习率继续训练几个epoch,让模型适应量化。
  4. 导出量化后的模型。

这里有个关键点:伪量化节点的位置。一般来说,权重和激活都需要量化。权重量化在卷积层之前,激活量化在激活函数之后。我见过有人只量化权重不量化激活,结果精度还行,但硬件实现时发现激活值范围太大,根本没法用。

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_typeinput_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上跑的硬件的。