4、第一个FINN项目:创建项目、导入预训练模型、运行默认编译流程、在FPGA上部署

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

前面讲了那么多FINN的原理、架构、数据流,说实话,光听不练容易飘。今天我们就来走一遍完整的流程——从零创建一个FINN项目,导入一个现成的预训练模型,跑通默认编译,最后烧到FPGA上看看效果。

我个人习惯,每次学新工具都是先跑通一个最小demo,再回头啃细节。这样心里有底,遇到问题也知道往哪个方向查。

4.1 环境准备与项目创建

先确认你的环境已经装好了FINN。我假设你用的是官方推荐的Docker镜像,或者自己从源码编译的Python环境。

打开终端,激活你的FINN环境:

source finn_env/bin/activate

然后创建一个新目录,作为我们的项目根目录:

mkdir my_first_finn_project
cd my_first_finn_project

FINN的项目结构其实挺清晰的。我个人习惯在项目根目录下放一个build.py脚本,用来驱动整个编译流程。你也可以用Jupyter Notebook,但命令行方式更可控,适合批量实验。

小提示: 如果你在Docker里跑,记得把项目目录挂载到容器外,不然容器一关数据就没了。我曾经因为这个丢过一次编译结果,气得我直接改写了挂载脚本。

4.2 导入预训练模型

FINN支持多种模型格式,最常用的是ONNX。我们这次用一个在CIFAR-10上训练好的小型CNN,叫cnv_w1a1——权重和激活都是1比特,说白了就是二值神经网络。

为什么选它?因为模型小,编译快,适合第一次跑通流程。

下载模型文件(假设你已经有了cnv_w1a1.onnx),放到项目目录下。然后我们写一个简单的Python脚本来导入它:

import finn.core.onnx_exec as oxe
from finn.core.modelwrapper import ModelWrapper

model = ModelWrapper("cnv_w1a1.onnx")
print("模型输入形状:", model.graph.input)
print("模型输出形状:", model.graph.output)

跑一下,你应该能看到输入是(1, 3, 32, 32),输出是(1, 10)。嗯,CIFAR-10的标准尺寸。

注意: 如果你的模型不是ONNX格式,可以用PyTorch或TensorFlow先转一下。FINN官方提供了转换工具,但说实话,ONNX是最省心的。

4.3 运行默认编译流程

FINN的编译流程,说白了就是一系列变换步骤。默认流程叫finn_default_build,它会自动把ONNX模型转成可部署的HLS IP核。

我们写一个build.py

from finn.builder.build import build

build(
    onnx_model="cnv_w1a1.onnx",
    output_dir="output",
    steps="finn_default_build"
)

然后运行:

python build.py

这个过程会持续几分钟到十几分钟,取决于你的机器性能。FINN会依次执行:

  • 模型解析与优化:去掉没用的节点,合并算子
  • 量化与折叠:把浮点运算转成定点,合并BatchNorm
  • 流式化:把计算图转成数据流架构
  • HLS代码生成:生成Vivado HLS的C++代码
  • IP核打包:生成Vivado IP核

我记得第一次跑的时候,看到终端里刷刷刷的输出,心里还挺激动的。但如果你遇到报错,别慌——八成是环境问题,比如ONNX版本不匹配。

避坑指南: 我曾经因为ONNX opset版本太高,导致FINN解析失败。建议用ONNX 1.8到1.12之间的版本,太新或太旧都可能出问题。

4.4 在FPGA上部署

编译完成后,output目录下会生成一个deploy文件夹。里面包含:

文件/文件夹 说明
pynq_driver.py PYNQ驱动脚本,用于在FPGA上加载和运行
bitstream.bit FPGA比特流文件
hwh文件 硬件描述文件,PYNQ需要它来识别IP核
parameters文件夹 模型权重和参数

把整个deploy文件夹拷贝到你的PYNQ开发板上(比如Zynq-7020)。然后通过Jupyter或SSH登录到板子,运行:

from pynq import Overlay
from pynq_driver import FINNDriver

overlay = Overlay("bitstream.bit")
driver = FINNDriver(overlay, "parameters/")

# 准备一张测试图片(32x32的CIFAR-10图像)
import numpy as np
input_data = np.random.randn(1, 3, 32, 32).astype(np.float32)

# 推理
output = driver.execute(input_data)
print("推理结果:", np.argmax(output))

如果一切顺利,你会看到输出一个0到9的数字,对应CIFAR-10的类别。

小技巧: 第一次部署建议用随机数据测试,先确认硬件通路没问题。再用真实图片验证精度。我一般会准备一张猫的图片,看看它能不能认出是猫。

4.5 常见问题与调试

嗯,这里总结几个我踩过的坑:

  • 比特流加载失败:检查板子型号是否匹配。Zynq-7020和Zynq-7045的比特流不通用。
  • 推理结果全零:大概率是输入数据没有归一化。FINN默认输入范围是0到1,如果你传了0到255的像素值,结果肯定不对。
  • 编译时间过长:默认流程会做很多优化。如果只是测试,可以跳过一些步骤,比如steps="finn_default_build"改成steps=["step_qonnx_to_finn", "step_tidy_up"]

说实话,第一次跑通整个流程,那种成就感是很实在的。你想想看,一个在电脑上训练的神经网络,现在跑在了一块小小的FPGA上,而且功耗只有几瓦——这就是硬件加速的魅力。

下一章我们会深入FINN的编译选项,教你如何定制自己的编译流程。到时候你会发现,默认流程只是冰山一角。