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核心暴力计算而是通过DPUDeep 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_nndctDPUCZDX8G2.01.8.1vai_q_pytorchDPUCZDX8G2.51.8.1 / 1.12vai_q_pytorchDPUCZDX8G / DPUCV2DX8G3.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时有时会被转成SplitConcat组合有时会直接报算子不支持。规避方案有两个方向方案一在导出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_size1): # 从验证集目录中读取图像做预处理 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( dataloadercalibration_data_loader(), quant_modecalib, # 生成量化参数 )写这份代码时有几个容易踩的坑我展开说一下。第一calibration_data_loader必须是一个生成器每次产出一个batch的数据不能一次性把全部图片都加载到内存里否则内存会爆。第二quant_mode需要区分两阶段先用calib模式生成量化参数这个模式会把模型权重转换为量化版本但同时保留浮点信息用于分析再用test模式得到完全量化的模型并用于验证精度。如果你直接一次性跑到头不经过calib模式后面精度分析会很不准确。第三dataloader返回的数据必须已经做了归一化和resizeYOLOv5训练时默认的预处理包含归一化操作遗漏这步会让统计出的数据分布完全偏差。完整的双阶段量化流程如下# 第一阶段calib模式生成量化参数 quant_model quantizer.quantize( dataloadercalibration_data_loader(), quant_modecalib ) # 保存calib模式的量化模型 quant_model.save_quantized(yolov5s_calib.pth) # 第二阶段test模式得到最终量化模型 quant_model quantizer.quantize( dataloadercalibration_data_loader(), quant_modetest ) # 保存最终量化模型 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卡启动文件这部分流程大致如下在Vivado中创建ZCU104硬件工程添加DPU IP核配置DPU参数架构选DPUCZDX8G_ISA1_B4096_Max输入尺寸640x640添加MIPI、DP、UART、SD卡控制器等外围IP根据你的实际使用场景选择最小系统只需要UART和SD卡生成比特流bitstream导出硬件描述文件XSA在Vitis IDE中创建软件平台基于XSA生成FSBL、PMU固件、设备树和U-Boot编译Linux内核PetaLinux或Vitis自带的方式都行生成image.ub制作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::vectorfloat 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训练时做了像素值归一化除以255DPU推理时同样需要做归一化否则精度会大幅下降。有的资料会把归一化合入到输入预处理阶段而不是模型内部这点尤其容易踩坑。7. 实测结果与ZCU104上的性能数字项目跑通后我在ZCU104上做了完整的性能实测以下数据供参考输入分辨率640x640DPU架构DPUCZDX8G_ISA1_B4096_MaxPL时钟约300MHzARM Cortex-A53四核运行Linux模型版本量化后mAP (COCO val)延迟单张图片处理帧率YOLOv5s35.8约55ms约18 FPSYOLOv5m42.1约120ms约8 FPSYOLOv5l46.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. 总结与经验部署踩坑后的几点避坑建议整个项目做完很多收获其实不在“如何跑通”而在“踩坑之后如何高效定位”。最后按个人经验把最值得注意的几个点整理出来环境版本强制统一。PyTorch训练环境的版本、Vitis-AI容器里的版本、板端Runtime的版本三者的版本不一致是绝大多数“莫明其妙”问题的根源。最好全程在同一个Docker容器里完成导出、量化和编译确保链路可复现。量化校准集是精度的生命线。校准集不需要多但必须和真实推理数据的分布一致预处理方式必须和训练时完全一致。Resize、归一化、通道顺序一个都不能错。模型结构尽量贴近DPU支持范围。YOLOv5自带的Focus模块和某些高级trickDPU不一定认。有条件就在模型设计阶段把DPU支持算子列表拉出来对照省得后面回来改模型再重训。后处理低估不得。DPU推理完成了如果没有高效的后处理系统吞吐依然上不去。YOLOv5的输出是三个尺度的特征图每张图有25200个候选框640x640输入直接遍历所有框做NMS会很吃力。我的做法是先用置信度阈值粗筛再做NMS优化后后处理耗时能从几十毫秒降到几毫秒。SD卡启动文件和rootfs准备好后立刻做一次备份。ZCU104调试过程中经常改启动文件、环境变量一不小心把启动文件弄坏了重新做一次要好几个小时。我做完完整镜像后直接dd了一个备份后面调试环境时随便折腾坏了就恢复备份节省了大量时间。代码已经全部开源在GitHub仓库里包括量化脚本、编译脚本、ZCU104端C部署源码和完整的README文档。后续文章会重点展开后处理优化和DPU多线程推理设计如果你在这个项目里遇到了具体问题欢迎留言交流也可以在仓库里提issue。