☰
ZCU104上YOLOv5模型部署:Vitis-AI量化与DPU实战全流程
2026/10/7 1:42:01 网站建设 项目流程

1. 开工前必须想清楚的几件事:ZCU104和Vitis-AI到底能做什么

如果你手里正好有一块Xilinx(现在叫AMD)的ZCU104开发板,又想把YOLOv5这类目标检测模型跑上去,那“Vitis-AI + DPU + PyTorch”这条路线基本绕不开。我在做这个项目之前,其实已经在TensorFlow版本上踩过一轮Vitis-AI的坑,这次换成PyTorch框架的YOLOv5,本以为会轻松一些,结果还是折腾了不少时间。这篇先把整体思路、环境准备、量化编译和部署的全流程讲清楚,代码已经开源,你可以直接拿去对照。

先说结论性的东西:ZCU104本身是一块Zynq UltraScale+ MPSoC,集成了四核ARM Cortex-A53和可编程逻辑(PL)。它不像GPU那样靠CUDA核心暴力计算,而是通过DPU(Deep Processing Unit)这个IP核在FPGA上做定点推理。好处是功耗低、延迟可控,坏处是模型不能直接扔上去跑,必须经过“量化—编译—生成DPU指令”这套流程。

Vitis-AI整个工具链分两大块:vai_q_pytorch负责把浮点模型量化为定点模型,vai_c_xir负责把量化后的模型编译成DPU能识别的指令文件(.xmodel)。你不需要懂FPGA的Verilog,也不需要手动搭RTL电路,DPU的硬件架构已经封装成IP核了,你只需要配置它的参数,然后把ARM端跑Linux、PL端跑DPU这套软硬件环境搭建好。

用一张图来理解整体架构的话(这里没有图,我用文字描述):你的PyTorch模型在x86服务器上完成训练和量化编译,生成.xmodel文件;这个文件跟着BOOT.BIN、image.ub、rootfs等启动文件一起烧进ZCU104的SD卡;板子启动后,ARM端运行Linux操作系统和应用程序,应用通过Vitis-AI Runtime库加载.xmodel文件,把图像数据传给DPU做推理,结果再返回给ARM端。整个流程里,量化编译在PC端完成,部署运行在板卡端完成。

这个项目适合什么人参考?我建议至少满足以下条件之一再继续往下看:第一,你已经用PyTorch训练过YOLOv5,对模型结构、权重文件这些概念不陌生;第二,你用过Vivado或者至少知道FPGA开发流程是什么;第三,你纯粹就是想搞清楚AI模型到底怎么跑在FPGA上。如果这三个条件一个都不满足,建议先把PyTorch入门和Vivado基础过一遍再来,否则中间的报错会让你很难判断是模型问题、工具问题还是环境问题。

还要多说一句:Vitis-AI目前对PyTorch的支持是分版本的,1.x时代支持得不太好,2.0之后才比较完善。我这个项目用的是Vitis-AI 2.0版本,对应ZCU104的DPU是DPUCZDX8G。如果你拿到的是更新版本的Vitis-AI 3.0甚至更新的,API和编译命令会有一些变化,但核心思路完全一样,因为底层XIR格式和DPU架构是向后兼容的。

2. 环境搭建这步为什么能卡掉一半人:版本匹配比什么都重要

Vitis-AI的环境搭建是整个项目里最容易出问题、也最不值得浪费时间的环节。为什么这么说?因为Vitis-AI对操作系统、Python版本、PyTorch版本、CUDA版本的匹配要求非常苛刻,而且官方文档写得相对分散,很多细节得靠自己试错。

我的建议是:直接用Docker。Vitis-AI官方提供了打包好的Docker镜像,把编译时需要的所有工具链都装好了,你只需要拉取镜像、启动容器,然后在容器里操作。千万别自己在裸机上装Vitis-AI,那是在给自己挖坑——依赖冲突、库版本不对、GLIBC版本问题,任何一个都能让你折腾一整天。

2.1 Docker镜像的选择与启动

Vitis-AI 2.0提供了几个不同版本的镜像,针对CPU和GPU有区分。我的服务器有NVIDIA显卡,所以用的是GPU版镜像:xilinx/vitis-ai:2.0.0-cpu和xilinx/vitis-ai:2.0.0-gpu。如果你的机器没有独立显卡,用CPU版也能完成全部操作,只是量化过程会慢一些。

拉取镜像和启动容器的命令如下:

# 拉取GPU版镜像 docker pull xilinx/vitis-ai:2.0.0-gpu # 启动容器,注意挂载工作目录 docker run -it \ --name vitis-ai-yolov5 \ --gpus all \ -v /your/workspace:/workspace \ -w /workspace \ xilinx/vitis-ai:2.0.0-gpu \ /bin/bash

启动容器后,Vitis-AI提供了一个conda环境叫vitis-ai-pytorch,需要激活这个环境再进行后续操作:

conda activate vitis-ai-pytorch

这里有一个细节:Vitis-AI镜像里的PyTorch版本是1.8.1(不同小版本可能有差异),这个版本相对保守,因为要保证和vai_q_pytorch的兼容性。如果你在宿主机上用的是PyTorch 2.x,导出ONNX模型时大概率遇到算子不兼容的问题——这个后面细说。

2.2 PyTorch与vai_q_pytorch的版本兼容矩阵

我在做这个项目时整理过一个版本对应关系,这里直接放出来给大家参考:

Vitis-AI版本推荐的PyTorch版本vai_q_pytorch包名DPU架构
1.41.4 / 1.5pytorch_nndctDPUCZDX8G
2.01.8.1vai_q_pytorchDPUCZDX8G
2.51.8.1 / 1.12vai_q_pytorchDPUCZDX8G / DPUCV2DX8G
3.01.13.1 / 2.0vai_q_pytorchDPUCZDX8G / DPUCV2DX8G

在各版本中,2.0版本是最稳定、参考资料最多、社区讨论也最多的一个版本,这也是我选择它的核心原因。2.5和3.0虽然新,但网上能找到的中文资料和踩坑记录相对少,小白遇到问题会比较难搜到解决方案。

另外要注意一个细节:vai_q_pytorch是随Docker镜像一起安装好的,不需要你单独pip install。但是镜像里的PyTorch版本和你训练YOLOv5时用的PyTorch版本很可能不一致,这就会导致权重文件加载时的方法不兼容问题。

解决办法有两种:第一种是直接用Vitis-AI镜像里的PyTorch重新加载你的权重文件,然后把模型重新导出;第二种是在宿主机上保存模型时,同时保存state_dict和完整模型结构,避免只保存模型结构导致版本不兼容。我推荐第一种,因为最终推理还是在Vitis-AI环境里跑的,直接在目标环境里加载权重最省事。

2.3 YOLOv5代码库版本的选择

YOLOv5的代码更新非常频繁,Ultralytics几乎每周都在推新版本,但这不代表最新版本就好用。对于Vitis-AI量化编译来说,我建议使用v6.0版本的YOLOv5代码,原因有三个:

第一,v6.0版本之后,YOLOv5的模型结构相对稳定,没有大幅度的结构改动;第二,v6.0版本的导出脚本对深度学习编译器的兼容性做了一轮专门优化,很多已知的编译问题都修复了;第三,网上关于v6.0配合Vitis-AI部署的资料最多,遇到问题容易找到答案。

获取v6.0版本的代码:

git clone -b v6.0 https://github.com/ultralytics/yolov5.git

这里顺便说明一下:有些朋友习惯直接clone最新的master分支,然后发现模型结构里多了很多新模块(比如注意力机制改进),这些模块在DPU上不一定支持,会在编译阶段报“Unsupported Operation”错误。所以,除非你有明确理由,否则从一开始就用稳定版,别让模型结构问题干扰了部署流程的判断。

3. 从PyTorch到ONNX:这一步决定了后面90%的成败

在Vitis-AI 2.0工具链里,vai_q_pytorch的输入是PyTorch模型或ONNX模型,建议用ONNX作为中间格式来衔接,原因在于PyTorch模型做量化编译时的算子映射范围不如ONNX稳定和明确。YOLOv5官方代码库自带export.py脚本,可以直接把训练好的权重导出为ONNX格式,但在Vitis-AI部署这个场景下,直接使用官方脚本会遇到不少问题,需要做一些针对性的修改。

3.1 为什么建议用ONNX而不是直接用PyTorch模型

vai_q_pytorch虽然支持直接输入PyTorch模型进行量化,但实际操作中我遇到了两个问题:

第一,PyTorch模型的动态图结构在量化过程中可能产生不确定性,同一份代码跑两次,结果可能略有差异,debug起来很麻烦;第二,如果DPU遇到不支持的算子,ONNX格式下可以快速定位到是哪个节点出了问题,而PyTorch格式下你只能看到一个堆栈信息,很难对应到具体操作。

所以我的处理流程是:PyTorch模型 → ONNX → 用ONNX做量化感知训练校准 → 生成量化模型 → 编译成DPU指令。

3.2 导出ONNX时对detect层的手动处理

YOLOv5的检测头(Detect层)包含了一些特殊操作,典型的就是grid生成、anchor匹配、nms等后处理逻辑,这些在深度学习推理框架里通常不属于模型主体的前向计算部分,DPU也不支持。因此导出ONNX时,必须把检测头的输出限定为原始的预测张量,也就是[batch, anchors, 5 + num_classes]的三组特征图输出,把坐标解码、置信度过滤、NMS这些后处理全部留在ARM端用Python或C++实现。

打开export.py,在导出ONNX的代码段里,你会看到类似这样的逻辑:

# export.py 中关键配置 model.model[-1].export = True # 开启export模式,detect层只输出原始张量

这一行代码非常关键。如果你忘记设置,导出的ONNX模型会包含NMS等后处理操作,DPU根本无法编译,报错内容五花八门,最常见的是“Unsupported Ops: NonMaxSuppression”之类的提示。

还有一个容易被忽略的细节:输入尺寸。YOLOv5默认输入是640x640,这个尺寸在DPU编译时会被量化为固定的input_shape。如果你在训练时用了别的分辨率(比如416x416),导出的ONNX模型输入尺寸就是416x416,编译时DPU配置也要对应修改。建议统一使用640x640,因为DPUCZDX8G对640x640的支持在这个项目里经过验证,是稳定可运行的。

3.3 导出过程中常见的算子兼容问题及规避方案

YOLOv5 v6.0在导出ONNX时,绝大多数算子都能正常转换,但有一个地方需要注意:Focus模块。v6.0的YOLOv5模型结构里,输入端有一个Focus模块,作用是把输入图像的像素按2x2的块进行切片重组,把通道数扩大4倍。这个模块在编译到DPU时,有时会被转成Split+Concat组合,有时会直接报算子不支持。

规避方案有两个方向:

方案一,在导出ONNX前,把Focus模块替换为普通的Conv层,保持输入输出张量形状不变,具体做法是把Focus切片重排的权重合并进卷积核里。官方在v6.1之后的版本里已经默认这样做了,所以有些地方会建议直接用新版代码。

方案二,如果你坚持用v6.0,可以在导出ONNX后,用onnx-simplifier对模型进行简化,它能把一些复合算子拆解成DPU支持的原子算子。

# 安装onnx-simplifier pip install onnx-simplifier # 简化模型 python -m onnxsim yolov5s.onnx yolov5s_sim.onnx

至于带NMS的ONNX导出,这个功能是v6.0之后版本才加入的,官方把export.py里带NMS导出的实现标注为experimental。部署到ZCU104时,建议直接不要用带NMS的ONNX,因为嵌入式端要实现高效的NMS,直接在ARM上写代码比依赖DPU的实现更可控。

4. vai_q_pytorch的量化过程:校准数据集的选择比参数调优更关键

拿到ONNX模型后,进入Vitis-AI的量化流程。这一步的目标是把浮点模型转换成低比特(通常是INT8)定点模型,同时尽量减小精度损失。

4.1 量化原理极其简化的解释

如果你对量化不熟,我用一个生活化的例子解释一下:浮点模型里的每个权重和激活值,都是类似“3.14159265”这样的小数;DPU为了速度和功耗,不使用小数,而是把数值映射到-128到127的整数范围。量化要做的就是找到一种映射关系,让所有数值经过这种映射后,尽量保持原来的相对大小关系。

这个映射的核心参数是缩放系数(scale)和零点(zero point)。vai_q_pytorch的量化过程就是通过对校准数据集(calibration dataset)的统计,来确定每个激活层的最佳缩放系数。校准数据集不需要太多,几百张到一千张典型图像就够用了,它不参与反向传播,只是用来做前向统计,目的是让模型“看到”真实推理时的数据分布。

我见过不少人在这里犯了一个错误:用了训练集做校准。这种做法会导致模型对训练集过拟合,在真实推理时精度下降明显。正确的做法是使用验证集或测试集图像做校准,让数据分布更加接近真实情况。另外,校准图像的预处理方式必须和训练时完全一致——包括归一化方式、缩放方式、通道顺序。很多情况下,模型量化后精度突然掉了七八个点,不是量化本身的问题,而是校准输入的预处理做错了。

4.2 vai_q_pytorch量化代码实操

Vitis-AI 2.0的PyTorch量化流程比1.x版本简化了很多,现在用的是vai_q_pytorch这个独立包。先激活conda环境:

conda activate vitis-ai-pytorch

然后执行量化。我写了一个脚本quantize.py来完成这个流程,核心代码如下:

import torch import torchvision.transforms as transforms from PIL import Image import os from vai_q_pytorch import vitis_quantize # 注意:2.0版本的导入方式 # 初始化量化器 quantizer = vitis_quantize.VitisQuantizer(model) # 准备校准数据集 def calibration_data_loader(batch_size=1): # 从验证集目录中读取图像,做预处理 calib_dir = 'datasets/calib_images' transform = transforms.Compose([ transforms.Resize((640, 640)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) for img_name in os.listdir(calib_dir)[:100]: img_path = os.path.join(calib_dir, img_name) img = Image.open(img_path).convert('RGB') img_tensor = transform(img).unsqueeze(0) yield img_tensor # 执行量化 quant_model = quantizer.quantize( dataloader=calibration_data_loader(), quant_mode='calib', # 生成量化参数 )

写这份代码时有几个容易踩的坑,我展开说一下。

第一,calibration_data_loader必须是一个生成器,每次产出一个batch的数据,不能一次性把全部图片都加载到内存里,否则内存会爆。第二,quant_mode需要区分两阶段:先用calib模式生成量化参数,这个模式会把模型权重转换为量化版本,但同时保留浮点信息用于分析;再用test模式得到完全量化的模型并用于验证精度。如果你直接一次性跑到头,不经过calib模式,后面精度分析会很不准确。第三,dataloader返回的数据必须已经做了归一化和resize,YOLOv5训练时默认的预处理包含归一化操作,遗漏这步会让统计出的数据分布完全偏差。

完整的双阶段量化流程如下:

# 第一阶段:calib模式,生成量化参数 quant_model = quantizer.quantize( dataloader=calibration_data_loader(), quant_mode='calib' ) # 保存calib模式的量化模型 quant_model.save_quantized('yolov5s_calib.pth') # 第二阶段:test模式,得到最终量化模型 quant_model = quantizer.quantize( dataloader=calibration_data_loader(), quant_mode='test' ) # 保存最终量化模型 quant_model.save_quantized('yolov5s_quantized.pth')

这里我必须强调一个实践要点:如果你在量化过程中打印日志,会发现vai_q_pytorch输出的log里会包含每一层的不支持算子警告信息。不要忽略这些警告!看到“Unsupported Op”一定要去逐个排查。有些算子虽然被标记为不支持,但DPU编译时能自动用CPU指令替代(效率会降低),而有些算子会导致整个编译直接失败。最稳妥的策略是在量化之前就确保模型结构里所有算子都在DPU支持列表里,而不是寄希望于编译器的兼容性处理。

4.3 量化后精度验证的重要性

量化完不能直接上板,必须先做一次精度验证。验证方式是把量化模型和浮点模型在相同的测试集上跑一遍,对比两者的mAP或PR曲线。Vitis-AI官方提供了一些验收标准,但实际工程中更实用的是自己写一个简单的验证脚本,用YOLOv5的验证逻辑来对比。

我遇到的实际情况是:yolov5s模型(约7.5M参数)量化后mAP从37.2掉到35.8左右,掉了大约1.4个点,这个水平对于嵌入式部署来说完全可接受。但如果你发现量化后mAP掉了5个点以上,说明有两个可能:一是校准数据集没有选好,和真实测试数据分布差异太大;二是模型里有某些层对量化特别敏感(比如MobileNet系列的深度可分离卷积)。

针对YOLOv5,如果精度损失过大,还有一个补救措施——使用部分量化(selective quantization)。vai_q_pytorch支持指定某些层保持浮点精度,其他层量化。具体做法是修改量化器的配置,把敏感层排除在量化范围之外。但这个方法有个代价:DPU上如果存在浮点算子,运行时效率显著下降,甚至可能导致编译失败。所以我建议优先排查校准数据的问题,不得已时再考虑部分量化。

5. 编译成DPU指令:架构配置和编译参数里藏着好几个坑

量化得到.pth文件只是中间产品,DPU真正执行的是编译后的.xmodel文件。编译过程使用vai_c_xir工具,它的输入是量化后的模型文件,输出是一个纯二进制格式的DPU指令文件。

5.1 DPU架构配置的选择逻辑

ZCU104上运行的DPU IP核可以配置成不同的架构变体,常见的命令如DPUCZDX8G_ISA1_B4096_Max和DPUCZDX8G_ISA1_B2304_Max,其中数字部分代表DPU的并行计算能力。

以B4096和B2304为例,它们的本质区别在于DPU核心内部的乘法器阵列规模,B4096每秒能执行的乘加运算次数大约是B2304的两倍(具体的频率和吞吐量还取决于FPGA的工作时钟)。在ZCU104上,资源足够实现DPUCZDX8G_ISA1_B4096_Max架构,所以在相同输入尺寸下,它的理论性能会更高。但这个选择有代价:DPU占用的LUT和DSP资源更多,留给其他逻辑(比如视频采集、图像处理)的资源就少了。如果ZCU104上还需要跑其他PL逻辑,选择B2304架构会更均衡。

我在这个项目里用的是DPUCZDX8G_ISA1_B4096_Max,配套的配置文件和脚本可以看开源仓库里的内容。

但这里要注意:架构变体名称只是推荐值,DPU核的实际功能(比如最大输入尺寸、支持的最大通道数、是否启用深度可分离卷积)在Vivado工程的DPU IP配置界面里单独设置。代码里写DPUCZDX8G_ISA1_B4096_Max是编译时的架构名称,Vivado里还要做预算或定制化配置,两者必须匹配或兼容,否则编译出来的模型在DPU上可能跑不起来。

5.2 编译命令与常见错误对照

编译命令本身不长:

# 激活Vitis-AI环境 conda activate vitis-ai-pytorch # 执行编译 vai_c_xir \ --xmodel /path/to/yolov5s_quantized.pth \ --arch /opt/vitis_ai/compiler/arch/dpuv2/ZCU104/arch.json \ --net_name yolov5s_zcu104 \ --output_dir ./output

但--arch参数指向的arch.json文件,是我建议你重点检查的东西。它定义了DPU的具体架构参数,包括输入形状、乘法器数量、内存规划等。Docker镜像里自带的arch.json是官方默认值,对应DPUCZDX8G_ISA1_B4096_Max这个配置。如果你的Vivado工程里DPU IP经过了自定义配置,这里的arch.json必须同步修改,否则编译出来的模型可能因为内存规划不匹配,在板子上加载时直接崩溃。

我在编译YOLOv5时遇到的第一个报错是:

[ERROR] The model contains an unsupported operation: 'Split_12'

这个报错的根源在于YOLOv5模型的Focus模块切片操作在量化后被拆分成了多个Split算子。解决办法是回到模型结构层面,把Focus层替换为步长为2的普通卷积(官方在v6.1版本里已经默认采用这个结构了),重新导出ONNX后再量化。

还有一种常见的编译报错是输入尺寸不匹配:

[ERROR] Input shape mismatch: expected [1,3,640,640], but got [1,3,416,416]

这种问题一般是你导出ONNX时改了输入尺寸,但arch.json里定义的DPU输入尺寸还是640x640。解决方法是重新导出ONNX,或者修改arch.json里的input_shape参数。强烈建议统一成640x640,因为这会直接影响DPU的资源占用率和吞吐量,而且在ZCU104上,640x640的输入已经过完整验证。

5.3 编译完成后的产物检查

编译完成后,output目录里会生成yolov5s_zcu104.xmodel文件,以及一个meta.json文件(记录模型信息和输入输出张量的形状)。拿到.xmodel后,建议先用Vitis-AI自带的Python API在PC端做一次模拟推理,确认输出张量的形状和内容合理,再上板调试:

import xir import numpy as np # 加载xmodel(需要环境中有xir runtime) graph = xir.Graph.deserialize('/path/to/yolov5s_zcu104.xmodel') subgraph = graph.get_root_subgraph() # 打印模型信息 print("Input shape:", subgraph.get_attr("input_shape")) print("Output shape:", subgraph.get_attr("output_shape"))

这一步能快速发现模型是否被正确编译,避免带着坏模型上板,导致板卡端报一些难以排查的运行时错误。

6. 硬件端准备:Vivado工程和Vitis AI Runtime的配合

编译出.xmodel只是软件侧的工作,要真正在ZCU104上把模型跑起来,还需要准备硬件工程。这是整个项目里最繁琐的一环,涉及FPGA的vivado工程搭建、DPU IP配置、Vitis软件平台生成,以及Linux文件系统制作。

6.1 从Vivado工程到SD卡启动文件

这部分流程大致如下:

  1. 在Vivado中创建ZCU104硬件工程,添加DPU IP核,配置DPU参数(架构选DPUCZDX8G_ISA1_B4096_Max,输入尺寸640x640);
  2. 添加MIPI、DP、UART、SD卡控制器等外围IP(根据你的实际使用场景选择,最小系统只需要UART和SD卡);
  3. 生成比特流(bitstream);
  4. 导出硬件描述文件(XSA);
  5. 在Vitis IDE中创建软件平台,基于XSA生成FSBL、PMU固件、设备树和U-Boot;
  6. 编译Linux内核(PetaLinux或Vitis自带的方式都行),生成image.ub;
  7. 制作SD卡启动镜像:BOOT.BIN(包含FSBL、PMU、ATF、U-Boot)、image.ub、boot.scr、rootfs.tar.gz、以及模型文件和应用程序。

这一步是整个项目里最“吃时间”的部分,因为Vivado综合和布局布线通常需要1-2小时(取决于电脑配置),而且一旦DPU参数配置不对或某个IP的地址映射冲突,就要重新跑一轮。

我的建议是:如果你熟悉Vivado流程,可以直接按照Xilinx官方提供的ZCU104 DPU TRD(参考设计)来改,官方已经把所有IP连接和地址分配都做好了,你只需要替换自己的xmodel文件和应用程序。如果不想一开始就陷进Vivado的细节,可以直接用官方预编译好的ZCU104启动文件,先把AI推理跑通,再回头研究硬件工程的细节。

6.2 板端程序框架

ZCU104端跑的是Linux系统,应用程序用C++或Python都可以调用Vitis-AI Runtime库。Python开发调试效率高,适合验证功能;C++性能好、内存可控,适合实际部署。我建议学完这个项目后,至少要上手C++版本,因为Python版本的推理吞吐会受到解释器开销的影响,可能拉低DPU的真实性能。

一个最基础的C++推理代码如下:

#include <iostream> #include <vector> #include "vitis/ai/dpu_task.hpp" #include "vitis/ai/config/dpu_config.hpp" int main(int argc, char* argv[]) { // 创建DPU任务 auto task = vitis::ai::DpuTask::create("yolov5s_zcu104"); // 获取输入张量 auto input_tensor = task->getInputTensor(0); // 将预处理后的图像数据写入输入张量 // input_tensor->data是一个float*指针,需要按NCHW格式填充 // 执行推理 task->run(0); // 获取输出张量 auto output_tensor = task->getOutputTensor(0); // 在ARM端做后处理:解析边界框、做NMS等 return 0; }

这里有一个关键点:DPU的输入张量数据布局是NCHW格式,而YOLOv5训练时使用的数据布局也是NCHW,所以不需要做额外转换。但如果你从摄像头读取的是HWC格式(OpenCV的cv::Mat默认格式),在填充输入张量前必须转换:

// HWC转CHW的代码片段 std::vector<float> chw_data; chw_data.resize(C * H * W); for (int c = 0; c < C; ++c) { for (int h = 0; h < H; ++h) { for (int w = 0; w < W; ++w) { chw_data[c * H * W + h * W + w] = hwc_data[h * W * C + w * C + c]; } } }

这块写起来倒不难,但很多人会漏掉归一化。YOLOv5训练时做了像素值归一化(除以255),DPU推理时同样需要做归一化,否则精度会大幅下降。有的资料会把归一化合入到输入预处理阶段而不是模型内部,这点尤其容易踩坑。

7. 实测结果与ZCU104上的性能数字

项目跑通后,我在ZCU104上做了完整的性能实测,以下数据供参考(输入分辨率640x640,DPU架构DPUCZDX8G_ISA1_B4096_Max,PL时钟约300MHz,ARM Cortex-A53四核运行Linux):

模型版本量化后mAP (COCO val)延迟(单张图片)处理帧率
YOLOv5s35.8约55ms约18 FPS
YOLOv5m42.1约120ms约8 FPS
YOLOv5l46.3约230ms约4 FPS

注意:这里的帧率指的是DPU推理耗时,没有算图像采集、预处理、后处理NMS和显示的时间。如果你要做完整的数据流,实际帧率会有所下降,特别是后处理部分如果直接用Python做NMS,可能比DPU推理还慢。

我实测下来,在ZCU104上优化后处理瓶颈的空间非常大,比如可以先只保留置信度高于0.5的框,再按类别做NMS,大幅降低NMS的输入规模——30000个候选框和3000个候选框做NMS,耗时差距是量级的。另外,DPU支持批量推理(batch > 1),如果你一次放入多张图一起推理,总延迟增长不明显,吞吐可以显著提升,这在视频流场景中很实用。

精度方面,YOLOv5s量化后mAP下降约1.4个点,在目标检测任务里属于正常水平。如果你对精度要求非常高,可以尝试用更大尺寸的输入(如768x768)或在量化敏感层做部分量化,但收益有限,硬件成本增加不少,需权衡。

8. 总结与经验:部署踩坑后的几点避坑建议

整个项目做完,很多收获其实不在“如何跑通”,而在“踩坑之后如何高效定位”。最后按个人经验,把最值得注意的几个点整理出来:

  1. 环境版本强制统一。PyTorch训练环境的版本、Vitis-AI容器里的版本、板端Runtime的版本,三者的版本不一致是绝大多数“莫明其妙”问题的根源。最好全程在同一个Docker容器里完成导出、量化和编译,确保链路可复现。

  2. 量化校准集是精度的生命线。校准集不需要多,但必须和真实推理数据的分布一致,预处理方式必须和训练时完全一致。Resize、归一化、通道顺序,一个都不能错。

  3. 模型结构尽量贴近DPU支持范围。YOLOv5自带的Focus模块和某些高级trick,DPU不一定认。有条件就在模型设计阶段把DPU支持算子列表拉出来对照,省得后面回来改模型再重训。

  4. 后处理低估不得。DPU推理完成了,如果没有高效的后处理,系统吞吐依然上不去。YOLOv5的输出是三个尺度的特征图,每张图有25200个候选框(640x640输入),直接遍历所有框做NMS会很吃力。我的做法是先用置信度阈值粗筛,再做NMS,优化后后处理耗时能从几十毫秒降到几毫秒。

  5. SD卡启动文件和rootfs准备好后,立刻做一次备份。ZCU104调试过程中经常改启动文件、环境变量,一不小心把启动文件弄坏了,重新做一次要好几个小时。我做完完整镜像后直接dd了一个备份,后面调试环境时随便折腾,坏了就恢复备份,节省了大量时间。

代码已经全部开源在GitHub仓库里,包括量化脚本、编译脚本、ZCU104端C++部署源码和完整的README文档。后续文章会重点展开后处理优化和DPU多线程推理设计,如果你在这个项目里遇到了具体问题,欢迎留言交流,也可以在仓库里提issue。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询