YOLO26模型导出实战:从ONNX到TensorRT的避坑全攻略
我最近帮一个做安防项目的朋友排查问题他训练好的YOLO26模型在电脑上跑得好好的一部署到对方的工控机上就各种翻车不是导出后推理结果全是框就是换了设备精度掉得离谱最离谱的一次是模型导出后CPU跑出来的结果和GPU对不上。查了一整晚最后发现问题出在导出参数上。这件事让我觉得模型导出这件事值得单独拿出来好好聊一聊——在YOLO26这套生态里训练只是前半程导出才是真正决定项目能不能落地的分水岭。这篇内容适合正在做目标检测部署、想把YOLO26模型搬到ONNX、TensorRT、OpenVINO等运行时里的同学尤其适合那种训练已经跑通了但不知道下一步怎么把模型交出去的处境。我会从导出前需要理清的概念、官方API参数逐项拆解、四种主流格式的实战选择再到导出后的验证方法和几种真实业务场景人员入侵检测、姿态估计、单相机测距逐个过一遍。全程不说废话尽量把那些文档里不会写、但实际一定会踩的坑提前给你排掉。1. 训练完的best.pt和能上线的模型之间差的不只是格式1.1 为什么很多人卡在训练完不知道怎么部署绝大多数人第一次接触模型导出以为是把.pt后缀改成.onnx或者点一下export按钮就完事。等自己动手才发现导出的模型要么在推理框架里加载报错要么推理结果和PyTorch里完全对不上要么速度慢到没法用。先想清楚一个问题你在训练时保存的best.pt本质上是一个PyTorch动态图的完整快照里面包含三样东西——模型结构定义、权重参数、以及优化器状态等训练辅助信息。而部署端需要的是一个只保留前向推理计算过程的静态计算图外加纯粹的权重数据。导出这个动作本质上就是把动态的、带着训练痕迹的模型转换成一个静态的、只负责计算的模型。YOLO26这类模型在导出时还有一个更麻烦的特点它的网络结构里包含控制流比如对不同尺寸输入做不同处理、动态shape、以及一些训练时才需要的模块比如数据增强分支、loss计算头。导出框架必须把这些训练态的东西剥离掉只保留推理态的计算路径。所以导出真不是格式转换而是一次结构重写。1.2 导出到底在导什么计算图、权重和前后处理搞懂导出先搞懂一次完整的目标检测推理发生了什么。输入一张图像先做letterbox缩放保持宽高比填充到模型输入尺寸再做归一化然后进入网络前向传播输出原始的预测张量最后做NMS非极大值抑制和后处理得到最终的框、类别、置信度。模型导出导的是中间那一大块前向传播计算图至于前后的图像预处理和NMS后处理取决于你选择的导出方式如果你只是导出裸模型onnx不带NMS那么预处理和后处理都要自己在部署代码里写。如果你用Ultralytics的nmsTrue导出ONNX里会自带一份NMS计算节点但这份NMS的参数是固定的灵活性会受限。有些框架如TensorRT的EfficientNMS插件、OpenVINO的DetectionOutput也提供NMS模块需要单独配置。这里必须明确一个认知导出模型只是把网络前向固化下来前后处理永远是部署代码里的一部分。很多人导出后推理结果不对不是模型导坏了而是部署代码里的预处理和后处理与训练时不一致。后面我专门有一节会展开讲这个。1.3 YOLO26的模块编排特点让导出比老版本更挑环境YOLO26这一代相比早期版本在很多地方做了结构迭代骨干网络里用了更多密集连接风格的模块检测头拆得也更细部分版本还支持姿态估计、分割、旋转框等多任务头。结构越复杂参与导出的算子种类就越多对导出器和目标运行时的算子支持要求也越高。这意味着几件事同样的模型onnx opset版本低了某些新算子可能不支持导出直接失败。同一份ONNX文件放到旧版TensorRT上转engine时可能出现算子不支持而回退到性能很差的实现。如果你在模型里加了自定义模块比如一些人脸检测项目会自己加attention模块导出时遇到不认识的操作就需要手工把自定义操作替换成标准算子或者用onnx2tf这类工具做算子映射。所以不要觉得在Ultralytics的训练环境里能导出成功就万事大吉部署端的运行时版本、硬件平台才是决定导出方案的关键约束。2. 动手导出前先把这几个影响成败的输入项理清楚2.1 best.pt还是last.pt导错模型你查一晚上都找不出问题这个错误特别低级但出现概率奇高。很多项目训练结束后直接导出last.pt但last.pt通常保存的是最后一个epoch的权重而不是验证集上表现最好的权重。如果训练过程中出现过拟合或精度振荡last.pt的效果会明显差于best.pt。Ultralytics的训练输出目录里runs/detect/train/weights/下一般有两个文件best.pt验证集指标最优的权重项目交付、继续训练、导出部署首选。last.pt最后一个epoch的权重适合断点续训不适合直接部署。另一个容易忽略的点是Ultralytics的.pt文件里其实存了训练时的类别名和模型的yaml配置如果你换了数据集重新训练导出前一定要确认当前.pt关联的类别信息是对的。我遇到过有人把两个项目的权重搞混导出后类别数和类别名完全对不上但模型本身不报错导致部署端数据解析一团糟。2.2 输入尺寸imgsz不是越大越好导出的模型输入尺寸是在导出时固定的。Ultralytics默认导出imgsz640这也是YOLO系列最经典的训练尺寸。但实际项目中不少场景需要更高的检测精度或更快的推理速度这时就要在导出前想清楚如果部署端是GPU且有性能余量可以导出imgsz1280或1600小目标检测效果会明显提升但推理耗时上升。如果部署端是CPU或边缘盒子建议导出imgsz416或480以换取速度。如果导出的模型要支持动态尺寸就不要只传一个固定imgsz而应该设置dynamicTrue下一节展开。很多人的误区是训练时用640导出时觉得部署端图像更大导出用1280精度更高。这样导出的模型在部署端会把输入图缩放到1280再推理看起来是加大了输入但实际上模型并没有在1280分辨率下训练等于让模型面对分布外数据效果可能不升反降。最稳妥的做法是导出imgsz和训练时的imgsz保持一致或者只做小幅调整。2.3 half、int8、dynamic、simplify这些开关各管什么Ultralytics的export命令里有一堆开关不搞清楚每个开关的含义随便组合很容易导出跑不起来或者精度崩了的模型。halfTrue把权重和计算图改成FP16。在TensorRT和GPU部署时能明显提速、降显存。但要注意FP16对数值范围比较敏感某些低光检测项目里图像暗部区域的数值本来就很小FP16的精度损失可能被放大。int8True量化到INT8体积和速度都更极致但需要校准数据。Ultralytics里直接设int8True时它会用训练集的一小部分做校准如果数据集分布和实际部署场景差异大量化后精度可能崩。建议用TensorRT或OpenVINO的独立量化工具用真实场景数据做校准。dynamicTrue允许推理时动态改变batch size和输入尺寸。代价是导出的模型会更大、推理性能略降而且部分框架不支持。大多数单路视频流场景用固定尺寸就够了。simplifyTrue用onnx-simplifier对计算图做简化去掉冗余节点、合并算子。建议默认开启不仅让模型更小还能提高在部分运行时上的兼容性。nmsTrue把NMS写进导出的计算图。仅当你的部署端不方便写后处理代码时再用如果需要精细控制置信度阈值和IoU阈值建议还是导出裸模型自己在部署代码里做NMS。device导出TorchScript或TensorRT时devicecpu和devicegpu结果有差异。TensorRT的engine是绑定GPU型号的在哪里转换就只能在哪里用同一型号GPU一般也能通用但不保证。导出ONNX时用cpu就够了。3. YOLO26用Ultralytics API导出的完整命令与参数取舍3.1 一条命令导出ONNX最常用的中间格式假设你训练好的模型在runs/detect/train/weights/best.pt导出ONNX只需要一条命令yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640 halfFalse simplifyTrue opset17执行成功后会在同目录下生成best.onnx。这里补充几个参数选择的思路opsetONNX算子集版本。导出时opset设得越高能用的新算子越多但对推理框架的版本要求也越高。实际项目中我建议先在部署环境里测试opset17或18如果运行时加载报Unsupported Operator再往下降。simplify装好onnx-simplifier后建议直接开启导出的文件小、节点少、部署时踩坑概率低。half导出ONNX时除非确定推理端是支持FP16的GPU且框架配置完善否则ONNX阶段建议用FP32导出等进入TensorRT再做FP16转换。ONNX本身是中间格式不应该在这里提前牺牲精度。导出后用Netron打开best.onnx能看到完整的计算图结构。建议养成这个习惯检查输入节点的名称和shape、输出节点的名称和维度这一步能帮你提前发现很多部署端才会暴露的问题。3.2 TensorRT导出FP16和INT8的选择逻辑TensorRT是NVIDIA GPU上性能最强的推理引擎也是YOLO系列项目落地最常用的部署方案。Ultralytics对TensorRT支持得很彻底一条命令就能完成转换yolo export modelbest.pt formatengine imgsz640 halfTrue workspace4 device0这里几个容易理解偏差的点workspace参数控制TensorRT构建engine时允许使用的显存上限单位GB。workspace设得太小转换时可能失败或生成性能很差的engine设得太大同一张卡上同时在跑训练任务时会OOM。我一般先设4如果转换失败再调大。引擎绑定GPUTensorRT生成的engine文件不是通用的它和转换时的GPU架构、CUDA版本、TensorRT版本强绑定。你在自己的RTX 4090上转好的engine拿到对方的A100或RTX 3060上很可能加载失败需要重新转换。这一点很多团队到了客户现场才意识到非常被动。INT8量化直接在Ultralytics里设int8True也不是不行但校准集质量取决于它内部自动截取的数据。我的经验是如果项目对精度要求高INT8最好在TensorRT里手动做先用一个脚本选300到500张能代表真实场景的图片做成校准数据集用trtexec或Python API转换。3.3 OpenVINO、NCNN、CoreML各踩什么坑实际项目里不可能人人都有NVIDIA显卡。CPU部署、边缘盒子、iOS端、国产算力卡这些场景都有各自的导出目标。OpenVINOIntel CPU/核显部署yolo export modelbest.pt formatopenvino imgsz640 halfFalse导出的产物是一个文件夹里面有.xml和.bin文件。OpenVINO在CPU上的优化很到位Int8量化可以用它自带的压缩工具做。踩坑点在于部分自定义算子比如一些attention实现在OpenVINO里不支持需要先转ONNX再转OpenVINO过程中可能遇到算子映射失败这时要么改写模型结构要么在部署代码里做算子替换。NCNN移动端/嵌入式部署yolo export modelbest.pt formatncnn imgsz320NCNN是腾讯开源的移动端推理框架在ARM平台和国产芯片上用得非常多。YOLO26导出到NCNN通常的坑是NCNN对某些新算子的支持滞后导致转换失败或精度损失。如果转出来的模型推理结果不对优先检查是不是PReLU、SiLU这类激活函数在某些老版本NCNN上的实现精度问题。另外NCNN的FP16是在移动端GPU上跑的如果目标设备是纯CPU建议用FP32的param和bin。CoreMLiOS/macOS部署yolo export modelbest.pt formatcoreml imgsz640CoreML导出后通常是一个.mlpackage。Ultralytics默认把NMS也封装进CoreML模型里这样做的好处是iOS端调用非常简单缺点是NMS参数被固定了不能灵活调阈值。如果你要精细调参导出时设置nmsFalse自己在iOS端用CoreML输出的原始张量做后处理。3.4 导出格式对比不同部署场景怎么选我做项目时一般按这张表来快速决定导出格式这里也分享出来部署场景推荐格式关键参数注意点NVIDIA GPU服务端TensorRT enginehalfTrue, workspace4绑定GPU架构换卡需重新转换跨平台中间格式ONNXopset17, simplifyTrue兼容性最强再接其他格式转换Intel CPU/核显OpenVINOhalfFalse注意自定义算子兼容性手机/嵌入式ARMNCNNimgsz改小算子支持可能滞后验证精度iOS/macOSCoreMLnms按需设置NMS固定参数灵活度有限PaddlePaddle生态PaddlePaddle先转ONNX再转国产算力卡常用算子兼容需测试记住一个原则ONNX是通用语言TensorRT/OpenVINO/NCNN是部署专属格式。不要直接从一个框架格式跳到另一个框架格式老老实实经过ONNX中转能省掉一大半未知的兼容性坑。4. 导出后的模型不是导出成功就完事验证这关必须过4.1 快速验证输入输出shape和数值范围导出成功的提示只是说文件生成了并不代表计算图是对的。我见过导出不报错但输出张量全是一个固定值的情况这种情况如果不做验证直接部署后果就是客户现场全部检测不到目标。最快速的验证方法是用Python加载导出的模型喂一张和训练时预处理方式完全一致的图检查输出维度import onnxruntime as ort import numpy as np session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name output_name session.get_outputs()[0].name # 构造一个模拟输入shape要和导出时imgsz一致 dummy_input np.random.randn(1, 3, 640, 640).astype(np.float32) outputs session.run([output_name], {input_name: dummy_input}) print(输入shape:, session.get_inputs()[0].shape) print(输出shape:, outputs[0].shape)对于YOLO26检测模型输出shape一般是(1, 84, 8400)或(1, 4num_classes, num_anchors)这个样子具体取决于版本和导出方式。如果输出的第二维对不上十有八九是导出时模型头处理出了问题。另外用随机输入跑一遍检查输出是否有NaN或Inf有的话基本可以断定导出过程中的某个算子发生了数值溢出。4.2 精度对比导出的模型不能比PyTorch掉太多数值验证通过后还要做精度对比。方法很简单准备同一张真实图片分别用PyTorch加载best.pt推理、用ONNX Runtime等框架加载导出的模型推理对比两者的检测框、类别、置信度。需要注意的是PyTorch推理时的预处理和后处理必须和部署端一致否则两者的差异不是来自模型本身而是来自前后处理不一致。通常的执行顺序是读取图片做letterbox缩放得到模型的输入tensor。分别跑best.pt和导出模型。对两个输出做完全相同的解码和NMS。对比检测结果框的位置误差应在几个像素以内置信度误差应在0.01以内。如果检测框差异较大优先怀疑模型导出的数值精度。把网络的中间feature map导出来对比或者用Netron检查计算图中是否有可疑的降精度节点比如某些框架默认把中间计算转成了FP16。4.3 常见导出假成功现象与排查这几年帮人排查导出问题发现几类高频假成功现象整理出来供你对照排查输出全部为0或固定值通常是模型在导出时把某些权重误当成常量折叠了或者对某些算子做了错误优化。解决方法是换一个opset版本、关闭simplify重试。输出shape不对YOLO26的多任务头比如同时输出检测框和关键点在导出时如果没处理对可能只导出了其中一个分支。用Netron检查输出节点数量和PyTorch模型的输出数量逐一对应。CPU上能跑GPU上结果不同这大概率是GPU推理框架用FP16跑了你期望FP32精度的网络在低光图像或小目标场景下尤其明显。检查推理框架的精度设置。同一份ONNX在不同框架上结果不同这是ONNX生态最常见的坑不同运行时对某些算子的实现有细微差异尤其是一些减少精度的融合操作。解决办法是让模型尽量使用标准算子组合并统一在目标推理框架里做精度验证。5. 三种真实应用场景的导出实战5.1 人员入侵检测NMS放在哪一层最合理人员入侵检测是YOLO26热度最高的落地场景之一。这类项目通常部署在边缘盒子或工控机上输入可能是摄像头RTSP流帧率要求高同时客户希望对人的检测尽量不漏报。这种场景导出的关键决策点是NMS放在哪一层。我的建议是导裸模型后处理自己写。原因很实际边缘盒子的场景复杂白天晚上光线差异大置信度阈值经常需要在现场调。如果NMS被固化在导出模型里改一次阈值就要重新导出一次模型非常痛苦。自己在代码里写NMS阈值可以做成配置文件现场调试效率高得多。另一个容易忽视的点是人员入侵检测的部署端输入图像通常来自视频流分辨率可能是1080P甚至4K。如果直接缩放到640输入远处的人可能只有几个像素大小很容易漏检。实际项目中我更推荐用更高分辨率导出比如imgsz1280同时在部署端做分块推理把大图切成几个重叠的块分别推理再合并结果。这个方案的缺点是推理次数变多需要根据盒子算力做取舍。5.2 姿态模型导出关键点输出的shape和版本差异很多人看热词里有关yolo26姿态模型版本区别这里必须提醒姿态估计模型的导出和纯检测模型不一样导出前必须确认你的模型是哪个任务类型。YOLO系列姿态模型的输出通常包含两部分检测框信息和关键点信息。导出的输出tensor里除了每个目标的box坐标、置信度、类别概率外还有一组关键点坐标和可见性分数。具体数量取决于你训练时设定的关键点个数比如人体姿态是17个关键点每个关键点有x、y、visibility三个值。导出时最常遇到的问题是自定义数据集的关键点数目和官方预训练模型不一致。比如你训练了一个只有5个关键点的姿态模型导出后输出维度却是基于17个关键点算出来的。这时要检查训练时是否正确修改了模型head的输出通道数。另外姿态模型导出后的后处理比检测模型更麻烦解码时不仅要做NMS选出目标框还要把归一化的关键点坐标映射回原图。如果你在部署代码里发现了关键点位置整体偏移的问题先检查letterbox的填充区域有没有被正确补偿回去。5.3 单相机测距输出距离自定义head怎么导出才不被丢掉热词里有yolo26 单相机测距 输出距离这类项目通常是在YOLO26检测头旁边接一个回归分支输出目标的距离信息。这种自定义网络头的导出是模型导出里最考验基本功的场景。问题出在Ultralytics官方导出逻辑是跟着官方模型结构走的。你加的自定义head在导出时不一定能自动包含到计算图里。常见的表现是PyTorch推理时有距离输出导出ONNX后输出节点不见了或者输出shape和预期不一致。解决思路有两种第一种把自定义head改写到YOLO26的forward路径里让距离分支成为模型的一部分然后用Ultralytics的导出接口整体导出。这种做法的难点在于要理解模型的结构定义改完后要用Netron确认输出节点。第二种单独把距离分支导出为一个模型部署时先跑YOLO26拿检测框再从原图的对应区域计算距离。缺点是增加了一次推理速度慢。我实际项目里更推荐第一种。单相机测距的输出结果还涉及相机内参、目标在图像中的像素位置等信息这些因素和网络的输入输出耦合在一起拆开来做反而容易出问题。导出前哪怕花一天时间把代码结构梳理清楚也比部署阶段出了bug再回头改要划算得多。6. 部署端调用导出模型时容易忽略的细节6.1 ONNX Runtime推理代码模板导出完成后部署代码才是真正面向线上环境的部分。这里给出一份ONNX Runtime的推理模板后面以此为基底扩展import cv2 import numpy as np import onnxruntime as ort class YOLO26ONNX: def __init__(self, onnx_path, conf_thres0.25, iou_thres0.45): self.session ort.InferenceSession( onnx_path, providers[CUDAExecutionProvider, CPUExecutionProvider], ) self.conf_thres conf_thres self.iou_thres iou_thres input_info self.session.get_inputs()[0] self.input_name input_info.name self.input_size (input_info.shape[2], input_info.shape[3]) # H, W def letterbox(self, img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] # h, w r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw new_shape[1] - new_unpad[0] dh new_shape[0] - new_unpad[1] dw, dh dw // 2, dh // 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom dh, dh new_shape[0] - new_unpad[1] left, right dw, dw new_shape[1] - new_unpad[0] img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value(color, color, color)) return img, r, dw, dh def preprocess(self, img): img, r, dw, dh self.letterbox(img, self.input_size) img img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW img np.ascontiguousarray(img, dtypenp.float32) / 255.0 img np.expand_dims(img, axis0) return img, r, dw, dh def infer(self, img): input_tensor, r, dw, dh self.preprocess(img) outputs self.session.run(None, {self.input_name: input_tensor})[0] return outputs, r, dw, dh这份代码只包含模型推理和预处理NMS部分要根据你导出的模型输出格式来写。如果你导出时用的是带NMS的模型输出直接就是检测框结果如果导的是裸模型需要自己实现解码加NMS。6.2 输入预处理必须和训练时保持一致我见过太多次Deployment代码和训练代码预处理不一致导致的线上问题典型的有训练时用RGB部署时忘记把OpenCV读进来的BGR转成RGB。训练时归一化除以255部署时代码里少写了这一步模型输出置信度全乱。letterbox的填充颜色训练时是114部署时写成0也会小幅度影响精度。输入图像的通道顺序用NCHW还是NHWC不同框架要求不一样ONNX里默认是NCHW。这些细节单独看都很小但它们联合起来足以让一个训练好的模型在部署端失灵。建议在部署代码里加一条断言调试期间用同一张图分别走训练代码和部署代码对比预处理后的tensor是否一致确认无误后再放开。6.3 关于内置NMS、动态尺寸和批量推理最后补充几个生产环境里经常需要决策的点。内置NMS的取舍很多推理框架比如TensorRT的EfficientNMS支持把NMS作为网络的一部分推理速度更快。但这类NMS通常有最大检测框数量限制比如最多300个框在密集场景下可能丢框。如果你做的是人群计数类项目建议还是自己在后处理里实现NMS控制权在自己手里。动态尺寸动态尺寸看起来很美好实际部署时却会频繁触发推理引擎重新分配内存导致单次推理时间不稳定。对于视频流场景固定尺寸的推理延迟更可控建议优先固定。批量推理如果你用coarse-to-fine的检测策略或者需要在同一时刻处理多路视频流可以考虑batch1导出。batch推理能充分利用GPU并行能力但前提是同一批次的图像尺寸要一致否则预处理逻辑会变复杂。我自己的习惯是项目初期先用固定640尺寸、batch1跑通全流程稳定性验证通过后再根据性能瓶颈决定要不要升级到动态尺寸或扩大batch。这样做的好处是排查问题时变量少出了问题能快速定位。模型导出这件事很多时候不是不会导而是没想清楚目标环境就急着导。先把部署环境、精度要求、实时性要求三条约束条件列出来导出参数的选择其实就自动浮出水面了。以上这些经验都是我在实际项目里反复踩坑验证过的希望能帮你少走一段弯路。