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会自动帮你做量化。
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时,总想一次把所有神经元都并行化,结果资源爆了。后来学乖了,先设一个保守的折叠系数,跑通了再慢慢调。
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%以内。因为网络小,量化损失不大。
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的支持同样出色。