ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Versal NPU上部署YOLOv11检测与分割的完整实践指南

Versal NPU上部署YOLOv11检测与分割的完整实践指南 去年底接了一个边缘视觉项目需求很直接设备端同时跑目标检测和实例分割且不能上 GPU功耗和成本卡得很死。我最初的方案是 YOLOv11-seg 放在常规的 ARM 平台上跑但帧率惨不忍睹后来切换到 AMD/Xilinx Versal ACAP 的 NPU 才算是解决。这个组合——YOLOv11 Detection Segmentation、Versal、VitisAI——在当时的公开资料很少很多坑都是自己一步步试出来的。这篇就把完整的部署思路、可复现步骤和踩坑过程整理出来给打算在 Versal 上跑 YOLOv11 的同行做个参考。1. 为什么把 YOLOv11 检测和分割搬到 Versal 上1.1 YOLOv11 检测与分割能做什么YOLOv11 是 Ultralytics 在 YOLOv8 基础上的升级版模型结构改动不算伤筋动骨但把骨干里的 C2f 换成了 C3k2并且在训练策略、分配策略上做了不少调整整体精度和收敛速度都有提升。对部署来说最友好的一点是它依然通过ultralytics这一个包就能搞定训练、验证、导出模型体积和计算量也可控非常符合边缘设备的需求。任务上YOLOv11 检测Detection输出的是目标框和类别分割Segmentation则多了一个 mask 分支能在框出目标的同时输出逐像素的掩码。这个能力放到实际场景里很值钱——比如质检线要区分工件表面划痕到底是一块还是多块检测框只能告诉你位置和大小分割能告诉你形状和面积再比如自动引导车AGV的视觉避障分割出来的可通行区域远比一个矩形框可靠。但分割的计算量比纯检测明显高出一截。在 Versal 上如果只是 YOLOv11n 检测模型NPU 跑起来很轻松一旦换成 v11-segmask 分支会引入矩阵乘法和额外的卷积层处理器的吞吐压力直接上一个台阶。这也是我最初在 ARM 上跑不动的根本原因。1.2 Versal 相比 GPU 方案的优势与代价Versal ACAP 是 AMD/Xilinx 推出的异构计算平台严格说它不是一个简单的 SoC而是集成了三类引擎标量引擎ARM Cortex-A72 为核心的应用处理器、自适应引擎也就是可编程逻辑 FPGA 部分、智能引擎AI Engine针对矩阵运算优化。而 VitisAI 工具链负责把训练好的模型编译成 NPU/DPU 上运行的xmodel镜像DPUDeep Learning Processor UnitIP 核则烧在 PL 端。为什么选它而不是 GPU三个原因。第一是功耗一颗 Versal 做推理时的整板功耗远低于同等级 GPU在无风扇机箱里也能压住。第二是启动粒度和确定性GPU 驱动和 CUDA 环境在工业设备上经常是维护噩梦而 Versal 的 dpu 一旦编好模型开机加载 xmodel 就能跑行为非常确定不会出现驱动版本漂移导致部署翻车。第三是价格在批量设备里Versal 的成本曲线比一块 GPU 加一套散热方案更平滑。当然它也有代价。VitisAI 对模型算子有支持边界不是所有 PyTorch 算子都能进 DPU不支持的部分要么改模型结构要么落到 CPU 上跑而 CPU 处理能力又非常有限。所以整个部署本质上是一个算子适配 性能抠细节的过程。YOLOv11 的检测头相对干净分割头就复杂不少后处理通常还要在 CPU 上自己实现这就引出了后面一堆工作。2. 工具链版本选择是第一个坑2.1 从零开始的环境配置清单先说结论如果要复现我这个项目推荐的版本组合是 Vitis AI 3.5 PyTorch 2.2.0 ultralytics 8.3.x ONNX Runtime仅用于验证。这个组合能比较顺畅地走完导出、量化、编译全流程。环境可以分为两边。一边是训练和模型转换主机通常是 Ubuntu 20.04/22.04 的机器需要安装Python 3.8—3.10建议用 conda 管理环境ultralytics包用于加载权重和导出 ONNXtorch和torchvision版本和 ultralytics 依赖匹配onnx、onnxruntime用于检查和验证导出结果Vitis AI 工具链官方推荐 Docker 方式xilinx/vitis-ai-gpu镜像里带了 quantizer 和 compiler另一边是 Versal 目标板上的运行环境也就是 Vitis AI RuntimeVART。板上需要安装匹配的 runtime 版本以及板卡对应的 DPU 驱动。如果你用的是官方评估板如 VCK190/VEK280驱动和xmodel部署脚本在出厂 BSP 里一般都有现成的。有个容易忽视的点Vitis AI 的 quantizer 对 PyTorch 和 ONNX 的版本有硬限制并不是越新越好。我最初在 conda 里装了一个非常新的 PyTorch 2.5quantizer 直接报了一堆unsupported operator排查到最后发现是版本兼容性问题——这个0基础小白也能照做的环境配置在官方文档里写得很分散真正跑起来需要自己在版本表格里逐个对。2.2 工具链版本匹配的几条硬规则版本匹配有三个地方特别容易踩雷建议第一次搞的人直接照单操作第一quantizer 建议使用 Docker 镜像里的 Python 环境不要在宿主机上另起一套。vai_q_onnx依赖的tensorflow或者onnx版本很旧跟当前 Python 生态冲突严重。使用 Docker 能帮你隔离掉这部分。第二目标板上的 VART 版本必须与编译 xmodel 的 Vitis AI 版本保持一致至少要保证 major/minor 版本对应。如果编译器是 3.5板子上 runtime 却是 3.0轻则加载失败重则推理结果完全错乱且没有任何报错。第三DPU 的架构配置要在编译时显式指定。Versal 上常见的 DPU 型号有 DPUCVDX8H用于 VCK190、VEK280 等编译时要给 compiler 指定对应的arch.json。这些规则不算复杂但如果不注意往往会在后面花大量时间排查。我的建议是任何一步报Unsupported或者N/A的错误先往版本上怀疑而不是急着改算子。3. 模型转换从 PyTorch 到 DPU3.1 导出 ONNX必须手动指定输出名VitisAI 的部署流程基本是PyTorch → ONNX → 量化 → xmodel。第一步是导出 ONNX。Ultralytics 提供了非常简单的导出命令如果只是训完模型想要一个 ONNX直接yolo export modelyolov11s-seg.pt formatonnx opset17 simplifyTrue但项目里我建议还是自己写导出脚本因为默认导出到 ONNX 图的节点名经常是output0、output1这一类不直观编译时容易搞混。更关键的是YOLOv11-seg 的 ONNX 输出不止一个张量——检测头输出一个形状为(batch, 4 num_classes mask_coeff, num_anchors)的主输出分割分支还会输出一个形状类似(batch, 32, 160, 160)的原型 mask 张量。这个结构在后面编写后处理时必须有明确对应关系所以导出时最好用torch.onnx.export手动指定逻辑名。import torch from ultralytics import YOLO model YOLO(yolov11s-seg.pt) model.model.eval() dummy torch.zeros(1, 3, 640, 640, devicecpu) torch.onnx.export( model.model, dummy, yolov11s_seg.onnx, opset_version17, input_names[images], output_names[det_out, mask_proto], dynamic_axes{ images: {0: batch}, det_out: {0: batch}, mask_proto: {0: batch}, }, )导出后先用onnxruntime验证一遍 shape 和输出是否合理再进入 Vitis AI 流程。这里提醒一句dynamic_axes虽然在 ONNX 层面是合法的但 VitisAI compiler 对动态 shape 的支持有限性能也不是一个量级。如果 DPU 资源允许我通常固定 batch1、固定 input 分辨率不要贪 dynamic shape 的方便。3.2 量化校准分割头比检测头更敏感YOLOv11 权重默认是 FP32直接编译进 DPU 也不是不行但 DPU 的算力是围绕 INT8 设计的不量化的结果就是推理速度上不去很多底层单元跑不满。VitisAI 通常要求先量化工具是vai_q_onnx。vai_q_onnx quantize \ --input_model yolov11s_seg.onnx \ --output_model yolov11s_seg_quantized.onnx \ --calib_dataset ./calib_images \ --calib_imgs 128 \ --data_transform normalize_ImageNet校准数据集的准备也有讲究。不要把训练集直接拿来做校准最好是单独抽一批覆盖各种场景的代表性图片内容要能覆盖你部署时实际遇到的场景。比如我的场景里有白天、逆光、低光照三种情况校准集里就必须按比例包含这些否则量化后模型在低光照下精度崩得厉害。实际量化下来我最大的感受是分割头比检测头敏感得多。检测头输出是框和类别某种程度上对量化噪声比较鲁棒分割头输出的是逐像素的概率图一旦某个关键层的量化范围没算好掩码边缘会变rua、甚至出现大量碎点。遇到这种情况优先检查校准图片数量和质量其次可以考虑给分割分支单独做一次量化阈值调整。VitisAI 的 quantizer 支持按节点配置量化策略但我个人建议先确保校准集正确再往细调不然很容易在调参的坑里出不来。3.3 编译 xmodel 与常见路径错误量化完成后进入编译环节生成 DPU 可以直接执行的xmodelvai_c_onnx \ --input_model yolov11s_seg_quantized.onnx \ --arch /opt/vitis_ai/compiler/arch/dpucvdx8g/DPUCVDX8G/arch.json \ --output_dir ./output \ --net_name yolov11s_seg_vck190这里最容易出错的不是命令本身而是arch.json路径选错。不同开发板的 DPU 后缀不一样VCK190 上的 DPU 是 DPUCVDX8H部分 Zynq UltraScale 开发板是 DPUCZDX8G用错了会编译出完全不能加载的 xmodel。正确做法是先到 Vitis AI 安装目录下找到匹配自己板卡的arch.json或者直接问官方支持要对应 BSP 的架构文件。另外一个常见报错是某个算子不支持比如ScatterND、CumSum、InstanceNormalization这类动态形状强相关的算子DPU 上往往没有实现。YOLOv11-seg 的 ONNX 图里比较隐蔽地带有一些上采样和矩阵乘操作它们可能被 compiler 标为subgraph落到 CPU 上。这会导致两个问题一是性能骤降二是 xmodel 运行时对 CPU 的依赖变强。遇到这种算子最简单有效的思路是回到模型导出阶段在 ONNX 图里用手工节点替代或者直接在源码层面替换掉对应操作。编译成功后会生成yolov11s_seg_vck190.xmodel。接下来进入最关键的部署环节。4. 在 VCK190 上跑推理与后处理4.1 用 VART Runner 加载并运行 xmodelVitis AI Runtime 的 Python 接口叫vitis_ai_runner使用起来比想象中简单核心就是 runner 对象import vitis_ai_runner as var runner var.Runner(/path/to/yolov11s_seg_vck190.xmodel) inputs runner.get_inputs() outputs runner.get_outputs() input_tensors [] for in_t in inputs: input_tensors.append(in_t) output_tensors [] for out_t in outputs: output_tensors.append(out_t)运行时需要把预处理后的图像数据按正确的 NHWC/NCHW 顺序填进输入张量。VitisAI 的 DPU 输入布局默认是 NHWC和 PyTorch 的 NCHW 不一致必须在预处理时把(1, 3, 640, 640)转成(1, 640, 640, 3)这一步漏了不会报错但输出会完全乱掉。我第一次跑就栽在这里检测框全部错位折腾了半天才发现是 layout 问题。推理调用本身很简单job runner.execute_async(input_tensors, output_tensors) runner.wait(job)执行完成后从output_tensors里拿数据检测头是一个形状为(1, 4 80 32, 8400)的数组COCO 80 类 4 个框坐标 32 个 mask 系数分割分支的输出是(1, 32, 160, 160)的原型 mask。后处理就是自己把这些原始输出还原成可用的框和 mask。4.2 检测解码和分割 mask 计算YOLOv11 的检测头仍然是 anchor-free 的解码方式与 v8 基本一致。简单说就是输出的 8400 个位置对应三种下采样倍率的特征图每个位置预测一个中心点坐标和宽高结合 DFL 机制再加一个分类分数。我直接参考 Ultralytics 的Ops类在 numpy 上实现了解码函数将原始输出拆成网格坐标、box 预测和类别分数对 box 的每个通道应用softmax或DFL解码把分布还原成实际宽高根据 anchor 步长把特征图坐标映射回原图坐标阈值过滤低置信度的框再跑 NMS。这里有一个容易忽略的性能问题DPU 只负责模型推理后处理的 NMS 全部落在 CPU 上。如果检测框特别多纯 Python 的 NMS 会非常耗时导致整体帧率被拖垮。我的做法是先用低置信度阈值滤掉乱框再做类别内 NMS再把 NMS 逻辑改成向量化 numpy 版本。必要的话可以用 C 重写 NMS 通过 pybind 回调性能差距非常明显。分割 mask 的计算是另一个关键点。YOLOv11-seg 的输出是 mask 系数和原型 mask两者做矩阵乘法才能还原出每个实例的 maskmask_coeff det_out[..., 84:] # (1, 32, 8400) proto mask_proto # (1, 32, 160, 160) # 对每个保留框mask coeff proto再裁剪到框内 mask np.dot(mask_coeff[i], proto.reshape(32, -1)) mask mask.reshape(160, 160) mask sigmoid(mask) mask (mask 0.5).astype(np.uint8)然后用手头的坐标缩放把 mask 从 160x160 的空间映射回原图尺寸再按检测框裁剪即可。这个流程在 GPU 上可以用矩阵乘一把梭但在 CPU 上要非常注意内存布局尽量别用 Python 循环逐框处理否则再大的 NPU 算力都补不回来。4.3 推理结果的保存图片、PNG mask 与 JSON检测和分割做完面临的是结果落盘问题。我这边至少有三种保存需求第一种是可视化结果把检测框画到图上用 OpenCV 就行。import cv2 for box in boxes: x1, y1, x2, y2 box.astype(int) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(result_vis.jpg, img)第二种是分割 mask 的透明底 PNG方便直接叠加到原图上做进一步可视化mask_rgba np.zeros((h, w, 4), dtypenp.uint8) mask_rgba[..., 3] mask * 255 # alpha 通道 mask_rgba[..., 0] (mask * 255).astype(np.uint8) # R 通道 cv2.imwrite(mask_result.png, mask_rgba)第三种是结构化结果落到 JSON 或 COCO 格式方便和算法团队的验证脚本对接{ filename: img_001.jpg, objects: [ {class: defect, score: 0.86, bbox: [120, 80, 300, 260], mask_area: 8392} ] }保存路径和格式建议从一开始就定好避免后面临时改造成本高。4.4 实测帧率与处理器占用我这里实测的运行环境是 VCK190 DPUCVDX8H模型 YOLOv11s-seg输入分辨率 640x640。检测加分割同时开启NPU 的推理时间大约在 20ms 到 30ms 之间换算下来大约 30~45 FPS但加上 CPU 后处理和图像缩放之后整体实际帧率在 20 FPS 左右。瓶颈非常明确地落在了后处理和 NMS 上。如果换成 YOLOv11n-seg输入保持 640NPU 推理时间可以压到 10ms 上下纯检测模式甚至能到 5ms 级别。这个差异说明对 Versal 部署来说真正要下功夫优化的不只是模型本身后处理链路对整体性能的影响往往被低估。我在项目里后来把 NMS 换成了 C 实现帧率直接翻了将近一倍这就是一个典型的后处理瓶颈案例。5. 优化方向与踩坑复盘5.1 小目标检测如何优化热搜里也经常出现小目标优化这个词我在这块也花了不少时间。YOLOv11 在 COCO 上对小目标的 Recall 其实一般放到部署场景里更麻烦。在 Versal 上有几条实际可走的路线第一提高输入分辨率。从 640 提到 1280小目标的召回会有明显提升但计算量翻四倍NPU 吞吐会显著下降。Versal 的 DPU 并行度取决于配置不是无限可扩展的所以我通常把分辨率提升控制在合理范围比如 960并观察实测帧率。第二注意 DPU 对输入 shape 的支持边界。Versal 上 DPU 往往要求输入分辨率的尺寸是特定对齐值如 32 或 64 的倍数如果你要跑 1280 这种分辨率建议先确认编译器能接受。不能接受就得在外部垫边到固定尺寸这会影响有效计算占比。第三模型结构层面优化。可以针对小目标增加更浅层的检测头P2 层或者在 yaml 里调整 anchor-free 的采样策略。但这些改动意味着需要重新训练工程成本不小。如果只是想快速验证小目标效果可以先从输入分辨率和 NMS 阈值入手不一定动模型结构。5.2 CPU 与 NPU 的异构协同Versal 上 CPUA72能力虽然不强但也不能让它闲着。我在实际项目里把流水线设计成了三层流水CPU 线程 A 负责图像采集和预处理NPU 负责推理CPU 线程 B 负责后处理和结果落盘。三个环节并行执行整体吞吐比串行调用高很多。需要注意VART Runner 的execute_async本身就是异步的合理利用它可以减少等待。一个小技巧是输入图像预处理可以提前做好 double buffer推理完立刻触发下一次推理而不是等后处理全部结束再取下一帧。CPU 资源分配也要留心。如果后处理是纯 Python 写的A72 很容易跑满反而会影响系统的其他工作比如网络上传结果。建议后处理脚本限定线程数必要时用nice或者单独进程处理。5.3 后续可以扩展的方向这个项目做完之后还有几个点值得继续深挖把检测和分割分成两个独立 xmodel按场景动态切换。比如在空旷场景只跑检测进入复杂区域再切换到分割能省不少功耗。将后处理中的 mask 计算和 NMS 直接下沉到 PL 端实现为自定义算子理论上可以把整体帧率再拉一大截但这个开发工作量不低。探索 YOLOv11 的 INT8 量化感知训练。虽然vai_q_onnx的后训练量化已经能用但如果在训练阶段就加入量化感知分割 mask 的精度还能再上一个台阶。我个人的体会是Versal VitisAI 这套方案真正考验人的不是把模型跑起来而是在DPU 算子支持有限 CPU 算力紧张 精度要求苛刻三重约束下做系统工程。对一个目标是落地到工业设备的团队来说完全够用但前提是做好版本管理和充分的算子适配验证。最后再分享一个小技巧——保存 xmodel 时把 DPU 的 batch size 配成实际吞吐的整数倍在任务量大的时候可以有效降低调度开销这是我在量产压力测试里才发现的确实管用。
返回列表