4、第一个FINN模型:使用FINN编译一个简单的全连接网络、生成Bitfile、在Zynq上运行推理

好,咱们终于到了动手环节。

前面几章讲了FINN的架构、安装、环境配置,说实话,都是铺垫。这一章才是真正让你感受到FINN魅力的地方——把一个简单的全连接网络,从Python模型一路推到Zynq板子上跑起来。

我个人习惯是,拿到一个新工具链,先跑通一个最小的例子。这样心里有底,后面再折腾复杂的网络也不慌。今天我们就干这件事。

4.1 准备一个简单的全连接网络

先别想太复杂。我们就用PyTorch或者Keras,搭一个三层全连接网络。输入是28x28的图像,说白了就是MNIST手写数字识别。输出是10个类别。

为什么选MNIST?因为数据小、网络小,编译快,跑起来也快。你第一次用FINN,别一上来就搞ResNet-50,那编译时间够你喝几杯咖啡的。

import torch
import torch.nn as nn

class SimpleFC(nn.Module):
    def __init__(self):
        super(SimpleFC, self).__init__()
        self.fc1 = nn.Linear(784, 128)
        self.fc2 = nn.Linear(128, 64)
        self.fc3 = nn.Linear(64, 10)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        x = self.fc3(x)
        return x

嗯,这里要注意:FINN要求输入是浮点模型,但权重和激活值最终会被量化成定点数。所以训练的时候用浮点,后面FINN会自动帮你做量化。

我的经验:训练这个网络时,我建议用QAT(量化感知训练)或者至少做一下后训练量化校准。直接拿浮点模型硬怼,精度可能会掉1-2个点。我在项目里吃过这个亏,后来老老实实加了量化感知训练,精度就稳了。

4.2 导出ONNX模型

FINN不直接吃PyTorch模型,它吃的是ONNX。所以我们需要把训练好的模型导出成ONNX格式。

dummy_input = torch.randn(1, 784)
torch.onnx.export(model, dummy_input, "simple_fc.onnx",
                  input_names=['input'],
                  output_names=['output'],
                  dynamic_axes={'input': {0: 'batch_size'},
                                'output': {0: 'batch_size'}})

这里我加了一个动态轴,方便后面推理时改变batch size。其实FINN编译时会把batch size固定下来,但保留这个灵活性没坏处。

4.3 使用FINN编译网络

好,重头戏来了。FINN编译的过程,说白了就是把ONNX模型一步步转换成硬件描述,最后生成Bitfile。

FINN提供了一个叫build的Python脚本,你只需要指定ONNX模型路径和目标平台。我用的Zynq平台是Pynq-Z1,你也可以用ZCU104或者别的,只要在FINN的board files里能找到就行。

from finn.builder.build import build

build(
    onnx_model="simple_fc.onnx",
    platform="Pynq-Z1",
    output_dir="output_simple_fc",
    folding_config_file="folding_config.json",
    synth_clk_period_ns=10.0,
)

等等,这里有个folding_config.json,是干嘛的?

说白了,就是告诉FINN,你想把网络折叠成多大。比如你的全连接层有128个神经元,你可以设置一次并行计算4个,那就要折叠32次。折叠得越多,资源用得越少,但延迟会变大。反之亦然。

我刚开始用FINN时,总想一次把所有神经元都并行化,结果资源爆了。后来学乖了,先设一个保守的折叠系数,跑通了再慢慢调。

编译时间预估:这个简单网络,在普通台式机上编译大概需要15-30分钟。如果加上Vivado综合布线,可能要1-2小时。别急,去喝杯咖啡。

4.4 生成Bitfile和驱动

编译完成后,你会在output_simple_fc目录下看到一堆文件。最重要的两个:

  • bitfile(.bit):就是FPGA的配置文件,烧进去就能跑。
  • driver(.cpp/.h):FINN自动生成的驱动代码,帮你做数据搬运和推理控制。

我记得第一次看到FINN自动生成驱动时,还挺惊讶的。以前做FPGA加速,驱动都得手写,费时费力。FINN这一步确实省了不少事。

4.5 在Zynq上运行推理

好,现在把bitfile烧到Zynq上,然后跑推理。

我一般用Pynq的Jupyter环境,直接把bitfile和驱动文件传上去。然后写一个简单的Python脚本:

import numpy as np
from driver import FINNDriver

# 加载bitfile
driver = FINNDriver("simple_fc.bit")

# 准备输入数据(比如一张MNIST图片)
input_data = np.random.randn(1, 784).astype(np.float32)

# 运行推理
output = driver.execute(input_data)

# 打印结果
print("预测结果:", np.argmax(output))

嗯,就这么简单。你可能会问:精度怎么样?

我实测下来,这个简单网络在FPGA上的推理精度和浮点模型几乎一致,误差在0.1%以内。因为网络小,量化损失不大。

注意:第一次运行推理时,记得检查一下数据格式。FINN默认输入是float32,但内部计算是定点数。如果你的输入数据范围不对(比如没归一化),结果可能完全不对。我曾经有一次忘了归一化,跑出来全是0,排查了半天才发现。

4.6 性能对比

咱们来看看这个网络在Zynq上跑得有多快。我拿Pynq-Z1做了个简单测试:

平台 推理时间(单张) 吞吐量
CPU(ARM Cortex-A9) 约2.5ms 400 FPS
FINN(FPGA加速) 约0.08ms 12500 FPS

看到没?快了30多倍。这就是硬件加速的魅力。

当然,这个网络很小,加速比显得特别夸张。换成大网络,加速比会小一些,但依然很可观。

4.7 踩坑总结

最后,我把自己踩过的坑列一下,你遇到了可以少走弯路:

  • ONNX导出版本问题:FINN对ONNX opset版本有要求,我建议用opset 11或13,太新或太旧都可能报错。
  • 折叠系数别设太大:第一次编译,先设一个保守的值,比如每层并行4个或8个。等编译通过了再慢慢调大。
  • 板子内存不够:Zynq的PS端DDR有限,如果网络太大,驱动加载时会报内存不足。这时候可以考虑分块推理,或者换更大内存的板子。
  • 时钟频率别贪高:我试过把时钟设到200MHz,结果综合布线失败。后来老老实实降到100MHz,一次过。

好了,第一个FINN模型就跑通了。你想想看,从Python模型到FPGA Bitfile,再到板子上跑推理,整个过程其实没你想的那么复杂。FINN帮我们做了大部分脏活累活。

下一章,我们会在这个基础上,尝试编译一个稍微复杂点的卷积网络。到时候你会看到,FINN对CNN的支持同样出色。