YOLOv8 部署到昇腾NPU:从 ONNX 到推理运行的完整记录

YOLOv8 部署到昇腾NPU:从 ONNX 到推理运行的完整记录 接手 YOLO 部署到昇腾的需求很常见。目标检测模型在 GPU 上用 TensorRT 推理是成熟路径换成 CANN 那一套流程和坑都不一样。这篇文章是我的完整部署记录——从 ONNX 导出到 ATC 转换到写推理代码到排查问题。少理论全是实际操作的流水账。模型准备导出 ONNX从 Ultralytics 的 YOLOv8 开始。先把 PyTorch 模型导出成 ONNXyoloexportmodelyolov8n.ptformatonnxopset17导出时注意几个关键参数opset17CANN ATC 对 ONNX opset 17 支持最完整opset 太高可能有算子映射不全dynamicFalseYOLO 部署中分辨率通常固定640x640 或 1280x1280动态分辨率在 CANN 上代价大固定省事导出成功后得到yolov8n.onnx。用onnxruntime验证一下 ONNX 能正常推理再进下一步importonnxruntimeasort sessionort.InferenceSession(yolov8n.onnx)# 确认输入输出名字和 shapeprint([inp.nameforinpinsession.get_inputs()])print([out.nameforoutinsession.get_outputs()])ATC 模型转换ONNX 有了下一步是转成 OM 格式atc--modelyolov8n.onnx\--framework5\--outputyolov8n\--soc_versionAscend910\--input_shapeimages:1,3,640,640\--logdebug参数解释--framework5ONNX 格式--outputyolov8n输出文件名自动加.om后缀--soc_versionAscend910目标 NPU 型号--input_shape固定输入为 1x3x640x640如果转换过程中报算子不支持先检查 ONNX opset 版本再检查 YOLO 的后处理算子NonMaxSuppression、MultiLevel等。ATC 对 ONNX 标准 NMS 算子支持有限常见做法是推理 ONNX 时只保留模型的前向部分NMS 后处理在 CPU 上做。# 跳过 NMS 相关算子后处理在推理代码里手动做atc--modelyolov8n_no_nms.onnx\--framework5\--outputyolov8n\--soc_versionAscend910\--input_shapeimages:1,3,640,640转换成功后生成yolov8n.om。ACL 推理代码OM 模型到手后用 AscendCL 写推理代码。核心流程跟 Prompt 8 的通用 Demo 一致但 YOLO 需要处理后处理的张量变换。#includeiostream#includevector#includeacl/acl.hintmain(){// 1. 初始化aclInit(nullptr);aclrtSetDevice(0);aclrtContext context;aclrtCreateContext(context,0);// 2. 加载 OM 模型uint32_tmodelId;aclmdlLoadFromFile(./yolov8n.om,modelId);aclmdlDesc*modelDescaclmdlCreateDesc();aclmdlGetDesc(modelDesc,modelId);// 3. 准备输入——YOLO 的输入是 [1,3,640,640] 的 float 图像size_t inputSizeaclmdlGetInputSizeByIndex(modelDesc,0);size_t outputSizeaclmdlGetOutputSizeByIndex(modelDesc,0);void*devInputnullptr;void*devOutputnullptr;aclrtMalloc(devInput,inputSize,ACL_MEM_MALLOC_HUGE_FIRST);aclrtMalloc(devOutput,outputSize,ACL_MEM_MALLOC_HUGE_FIRST);// 读入图像预处理resize → normalize → HWC→CHW// 省略图像读取和预处理代码……// float* hostInput preprocess(image.jpg, 640, 640);// 4. 拷贝输入到 DeviceaclrtMemcpy(devInput,inputSize,hostInput,inputSize,ACL_MEMCPY_HOST_TO_DEVICE);// 5. 创建 DatasetaclmdlDataset*inputSetaclmdlCreateDataset();aclDataBuffer*inputBufaclCreateDataBuffer(devInput,inputSize);aclmdlAddDatasetBuffer(inputSet,inputBuf);aclmdlDataset*outputSetaclmdlCreateDataset();aclDataBuffer*outputBufaclCreateDataBuffer(devOutput,outputSize);aclmdlAddDatasetBuffer(outputSet,outputBuf);// 6. 推理aclmdlExecute(modelId,inputSet,outputSet);// 7. 结果拷回 Hoststd::vectorfloathostOutput(outputSize/sizeof(float));aclrtMemcpy(hostOutput.data(),outputSize,devOutput,outputSize,ACL_MEMCPY_DEVICE_TO_HOST);// 8. 后处理CPU 上做 NMS 边框解码// std::vectorBox boxes postprocess(hostOutput);// 9. 释放资源aclDestroyDataBuffer(inputBuf);aclDestroyDataBuffer(outputBuf);aclmdlDestroyDataset(inputSet);aclmdlDestroyDataset(outputSet);aclrtFree(devInput);aclrtFree(devOutput);aclmdlDestroyDesc(modelDesc);aclmdlUnload(modelId);aclrtDestroyContext(context);aclrtResetDevice(0);aclFinalize();return0;}编译g-oyolov8_infer yolov8_infer.cpp\-I/usr/local/Ascend/ascend-toolkit/latest/include\-L/usr/local/Ascend/ascend-toolkit/latest/lib64\-lascendcl-stdc11常见部署问题问题 1ATC 转换时报 Unsupported op NonMaxSuppression。YOLO 的 ONNX 导出默认包含 NMS 算子。ATC 的非标准算子映射表里没有这个。方案是在导出时设置nmsFalse推理代码里用 CPU 实现 NMS。CPU NMS 在小 Batch 下性能损失可忽略。问题 2输出 Tensor 维度不符合预期。YOLOv8 的输出是[1,84,8400]8400 个候选框每个 84 4 坐标 80 类别。如果你在推理代码里按[1,8400,84]去解析结果全错。注意检查 ONNX 的输出维度顺序。问题 3推理结果全部为 0 或 NaN。大概率是输入数据预处理出错——图像没有正确归一化到 [0,1]或者 HWC 转 CHW 时数据排列不对。建议第一步先打印输入 Tensor 的前几个 float 值确认是否合理。问题 4OM 模型加载慢。YOLO 模型转换后的 OM 加载时间可能在 1-5 秒因为 GE 在加载时做了一次完整的图优化。生产环境建议预热——服务启动时加载模型后做一次 dummy 推理避免第一个请求被打到。性能数据YOLOv8nnano 版本在 Ascend 910 上的实测数据配置延迟单图吞吐FPS固定 640x640, Batch12.8ms357固定 640x640, Batch47.1ms563固定 640x640, Batch812.5ms640动态输入, Batch14.2ms238固定 Shape 比动态 Shape 快 50% 以上。Batch 增大时吞吐增加但单图延迟也在涨——Batch8 时单图延迟降到 1.56ms 但第一张图要等其他 7 张准备好才一起推理实际在线服务的端到端延迟需要考虑排队时间。结语YOLO 部署到昇腾的路径很成熟ONNX → ATC → AscendCL。主要的坑集中在 ONNX 导出时的 NMS 算子处理和 ATC 对动态 Shape 的支持程度上。先用固定 Shape 跑通再根据实际性能需求考虑多 Batch 和动态 Shape 优化。对比 TensorRT 的 YOLO 部署路径CANN 的 ATC 转换流程本质上是对齐的——都是先做图优化再序列化成优化模型格式。区别在于 CANN 的算子融合策略跟达芬奇架构深度绑定对于 Conv BN ReLU 这种适合昇腾 AI Core 的模式融合得更激进。CANN YOLO 部署示例TensorRT 与 CANN 推理对比后处理CPU 上实现 NMSYOLO 的原始输出是[1,84,8400]——8400 个候选框每个框包含 4 个坐标、1 个目标置信度、80 个类别概率。后处理要做的是解码坐标、过滤低置信度框、做 NMS非极大值抑制。structBox{floatx1,y1,x2,y2,score;intclassId;};std::vectorBoxpostprocess(constfloat*rawOutput,intnumBoxes,intnumClasses){std::vectorBoxcandidates;for(inti0;inumBoxes;i){// 每个框的数据: cx,cy,w,h 80 class scoresconstfloat*ptrrawOutputi*(4numClasses);floatcxptr[0],cyptr[1],wptr[2],hptr[3];// 找最高置信度的类别floatmaxScore0;intbestClass-1;for(intc0;cnumClasses;c){floatscoreptr[4c];if(scoremaxScore){maxScorescore;bestClassc;}}if(maxScore0.5f){// 置信度阈值candidates.push_back({cx-w/2,cy-h/2,// x1, y1cxw/2,cyh/2,// x2, y2maxScore,bestClass});}}// 按类别分别做 NMSreturnnms(candidates,0.45f);}这段代码在 CPU 上执行。对单张 640x640 的图片后处理耗时约 0.3-0.5ms——对比推理本身的 2.8ms可以在 Host 侧的等待时间里异步完成不影响吞吐。多 Batch 推理攒批策略YOLO 部署到线上服务时单张推理太浪费 NPU 的算力。多发做法是攒批——等 N 张图片到齐后一次推理。// 攒批推理的简化逻辑constintBATCH_SIZE4;std::vectorImagebatch;while(true){Image imgwaitForImage();// 等一张图片batch.push_back(preprocess(img));if(batch.size()BATCH_SIZE){// 攒够了// 把 4 张图拼成 [4,3,640,640] 的 Tensorvoid*batchInputpackBatch(batch);// 推理多图同时跑aclmdlExecute(modelId,batchInput,output);// 后处理for(inti0;iBATCH_SIZE;i){autoboxespostprocess(getOutputSlice(output,i));sendResult(boxes);}batch.clear();}}攒批策略的关键是等待超时。如果流量低时迟迟凑不够 BATCH_SIZE推理会被无限拖延。常见做法是设置 5ms 超时时间到了不管攒了多少张都提交推理。推理性能更多细节YOLOv8n 在 Ascend 910 上不同配置的表现差异预处理 vs 推理 vs 后处理耗时分布预处理 0.8ms图像解码 resize normalize推理 2.8ms后处理 0.4ms。预处理和后处理都在 CPU 上走跟推理是异步的。模型大小差异YOLOv8n3.2M 参数OM 约 7MB→ 2.8msYOLOv8s11.2M 参数OM 约 23MB→ 4.5msYOLOv8m25.9M 参数OM 约 52MB→ 8.1ms。模型大小跟推理延迟基本线性关系。CANN vs TensorRT 同配置对比固定 640x640、Batch1CANN 2.8ms vs TensorRT 2.1ms。差 30% 左右。差距主要来自 CANN 在后处理算子的支持上还不够原生——NMS 在 GPU 上可以走 TensorRT 的自定义插件在 CANN 上只能走 CPU。端到端延迟差距在 15-20%。这些数据不是 Benchmark 级别的精确测试但能反映实际部署中不同选择的大致性能区间——帮你决定用哪个版本的 YOLO、Batch 设多大、预处理和后处理要不要做流水线优化。参考仓库CANN 示例仓库CANN 推理参考实现