资讯动态

YOLOX 转 OpenVINO IR 完整实战:PyTorch 模型部署到 CPU 推理

发布时间:2026/10/20 2:54:32 来源:尧图企业网站定制
最近在 Windows 10 上把一个 YOLOX 目标检测模型完整地迁移到了 OpenVINO 平台从 PyTorch 权重出发经历 ONNX 中间格式最终得到 OpenVINO 需要的 IR 文件.xml .bin再接入推理代码把检测结果跑通。整个过程踩了不少坑也从原理层面把每个环节的“为什么”搞清楚了。这篇就按我实际操作链路展开把 YOLOX 转 OpenVINO IR 的完整方案、命令细节、后处理实现和避坑记录都写清楚。这套流程适合这几类读者手头有 YOLOX 训练好的权重想在 Intel CPU、核显或 Movidius 设备上做低延迟推理的刚接触 OpenVINO搞不清楚 ONNX 与 IR 关系的或者纯粹想把模型部署链路摸一遍理解中间表示、算子兼容、精度设置等概念的。看完全文你能复现出一条从 .pth 到 .xml/.bin 再到可用推理 Demo 的完整链路。1. 整体方案与转换链路设计1.1 为什么选择 ONNX 作为中间格式YOLOX 本身是 PyTorch 训练出来的权重文件是 .pthOpenVINO 不能直接消费这种格式。虽然 OpenVINO 从 2024 版本开始也提供了直接把 PyTorch 模型读进来的实验能力但生产环境里最稳、资料最多、可控性最强的路径依然是先导出 ONNX再从 ONNX 转换到 IR。ONNX 在这里起到的是“通用中间语言”的作用。PyTorch 的算子生态和 OpenVINO 的算子生态并不完全一致而 ONNX 的算子集定义相对中立绝大多数框架和推理引擎都支持它。可以先在 ONNX 层面检查图结构、验证输出形状、甚至用 onnxsim 做一轮轻量化确认模型没问题后再交给 OpenVINO 的转换工具这样可以大大降低排查难度。提示如果你的 YOLOX 模型已经做了自定义结构修改比如改了检测头或者加了注意力模块优先确认这些算子在导出 ONNX 时是否被完整支持。导出失败或者 ONNX 图里出现诡异的小算子通常都是自定义 forward 里的某个 Python 操作无法静态化导致的。1.2 转 IR 时到底发生了什么ONNX 转 IR 的过程很多人以为只是换个文件后缀其实内部做了一大堆事情。OpenVINO 的模型转换工具现代版本叫 ovc老版本叫 mo会把 ONNX 图读进来然后经历几个阶段图解析、算子映射、拓扑优化、常量折叠、精度选择最后生成两个文件.xml描述模型结构的文本文件记录每一层的名称、类型、输入输出关系、参数属性。.bin存储权重和偏置的二进制文件与 .xml 配套使用。这两个文件合起来就是 IRIntermediate Representation。IR 的好处在于它是 OpenVINO 自己定义的、经过优化规划过的图格式运行时可以直接加载到推理引擎里做进一步编译和调度而不用在部署机上再解析一遍 ONNX 或者依赖 Python 环境。在选择转换路径时我坚持“先转换后调优”的顺序。不要一上来就试图把所有预处理、后处理全部塞进模型图里那样只会让转换更容易失败、排查更难。先把纯模型部分转过去跑通结果再根据延迟和吞吐需求决定要不要把预处理折进模型或者优化后处理。1.3 转换链路的三个关键产物完整链路会产出四个东西PyTorch 权重 .pth、ONNX 模型 .onnx、OpenVINO IR.xml .bin、以及一份配套的推理后处理代码。它们之间的关系如下文件作用何时获得yolox_s.pth训练出的原始权重训练完成或下载预训练权重yolox_s.onnx静态化后的计算图 权重用导出脚本从 .pth 转换yolox_s.xml / .binOpenVINO 专用模型表示用 ovc / mo 从 .onnx 转换inference_demo.py加载 IR 并完成预处理、推理、后处理自己编写这条链路里最容易出问题的地方一是导出 ONNX 时算子不支持二是转换 IR 时输入输出张量方向搞错三是推理时后处理和模型输出对不上。后面每一章我都会具体讲到。2. Windows 10 环境准备与工具链安装2.1 创建独立 Python 环境Windows 10 上做这种模型转换工作强烈建议不要直接装在系统 Python 里否则依赖冲突会让人崩溃。我用的是 Anaconda 管理的虚拟环境Python 版本选的 3.10和 OpenVINO 2024 LTS 版本兼容得很好。conda create -n openvino_env python3.10 -y conda activate openvino_env激活环境后先安装 OpenVINO 运行时和开发工具包。OpenVINO 从 2024 版本开始把模型转换工具 ovc 和运行时统一在一个 pip 包里一条命令搞定pip install openvino openvino-dev这里openvino是运行推理时需要的核心库openvino-dev里包含了 ovc 转换器等开发工具。装完以后可以用下面这条命令确认版本python -c from openvino import Core; print(Core().get_versions()[ovr].major)如果输出类似 2024 的版本号说明环境没问题。2.2 准备 YOLOX 源码与权重YOLOX 官方开源仓库提供了完整的模型定义、训练代码和导出脚本。我这边用的是 yolox-s 这个规模的模型做验证权重从官方发布页下载。如果你是自己训练的模型记得把训练好的 best_ckpt.pth 准备好。仓库克隆下来之后需要安装 YOLOX 的依赖。官方 requirements 里有 torch、torchvision、onnx、onnxsim 等。特别注意 onnxsim 这个包它是用来对 ONNX 模型做一些图精简的YOLOX 的导出脚本会调用它。git clone https://github.com/Megvii-BaseDetection/YOLOX.git cd YOLOX pip install -r requirements.txt注意YOLOX 仓库有挺多历史版本不同分支的导出脚本参数可能有细微差异。建议固定使用官方 release 对应的 tag 或较新的主分支。如果发现导出命令和我的描述不一样先看tools/export_onnx.py里的 argparse 参数列表。2.3 确认 GPU 不是必需项整个转换过程是 CPU 操作不需要 CUDA。PyTorch 加载权重时可以用 CPU 推理模式跑一次 demo 验证权重有效性这样能避免后面出了 bug 却搞不清楚是权重问题还是转换问题。我习惯先跑一个最小验证python tools/demo.py image -n yolox-s -c yolox_s.pth --path test.jpg --conf 0.3能正常画出检测框再继续后面的导出工作。3. YOLOX 导出 ONNX 实操细节3.1 官方导出命令及参数解析YOLOX 官方仓库在tools/export_onnx.py里提供了完善的导出支持。以 yolox-s 为例最基础的导出命令是cd YOLOX python tools/export_onnx.py -n yolox-s -c yolox_s.pth --output-name yolox_s.onnx执行完成后会在当前目录生成 yolox_s.onnx。这个命令做了几件事加载模型定义、载入权重、把模型切换到 eval 模式、构造一个虚拟输入张量、通过 tracing 的方式把 forward 过程固定为 ONNX 图。模型默认输入尺寸是 640x640单张 batch。如果不想用网络结构名称而是直接指定实验配置也可以这样python tools/export_onnx.py -f exps/default/yolox_s.py -c yolox_s.pth --output-name yolox_s.onnx-f 参数指向具体的实验配置文件适合自定义模型结构的情况。3.2 动态 batch 与输入尺寸选择导出脚本里有一批可选参数比较关键--batch-size默认是 1如果希望模型一次处理多张图可以设成 4 或者 8。--dynamic加上这个参数会把 ONNX 的 batch 维度设为动态方便在推理时按需指定 batch。--no-onnxsim默认会调 onnxsim 对模型进行精简一般不需要关掉。--opsetONNX 算子集版本默认 11YOLOX 的这些算子用 11 足够了。我个人的建议是转换 OpenVINO IR 时优先采用固定 batch、固定尺寸的模型除非你有强烈的动态 shape 需求。原因是固定 shape 可以给 OpenVINO 更多编译优化空间CPU 上的延迟会更低。如果确实需要在同一模型里跑不同分辨率那就在导出时用动态输入然后在 ovc 转换时用--input_shape指定一个基准 shape推理时再通过 reshape 调整。实际上 OpenVINO 对动态 shape 支持也比较成熟只是性能上不如固定 shape 省心。我当时的导出命令是python tools/export_onnx.py -n yolox-s -c yolox_s.pth --output-name yolox_s.onnx --batch-size 1 --opset 11导出完成后用下面的 Python 脚本快速验证一下 ONNX 结构import onnx model onnx.load(yolox_s.onnx) print(model.graph.input) print(model.graph.output)3.3 导出结果中的输出张量含义这一步最需要弄清楚。YOLOX 是 anchor-free 的检测器它把检测头的输出 decode 之后会得到一个形状为[1, 8400, 85]的张量以 COCO 80 类为例。这里 8400 是三尺度特征图上的预测点总数计算方式是640 输入时三个 stride 分别是 8、16、32对应的特征图尺寸为 80x80、40x40、20x20总 anchor 点数为 80x80 40x40 20x20 840085 的构成是 4 个边界框坐标中心点 x、中心点 y、宽 w、高 h 1 个目标置信度 80 个类别分数。官方导出脚本已经把 decode 过程写进了 ONNX 图所以模型的输出是解码后的绝对坐标而不是特征图上的原始回归量。这个设计对后续 OpenVINO 转换非常友好我们不需要在 IR 里额外做复杂的解码算子后处理只需要做阈值过滤和 NMS 就行。提示如果你拿到一个从其他渠道导出的 YOLOX ONNX输出形状是[1, 85, 8400]或者[1, 3, 80, 80, 85]这种说明模型输出的是未解码特征。这时候要么修改导出脚本让 decode 进图要么在推理代码里自己实现基于网格的 decode。建议优先让 decode 进图因为这样后续部署代码会简单很多。4. ONNX 转 OpenVINO IR 的完整过程4.1 使用 ovc 完成转换OpenVINO 2024 版本之后模型转换工具统一叫 ovc老教程里常见的 mo 命令已经逐渐被替代。用 ovc 把刚才的 yolox_s.onnx 转换成 IR命令如下cd YOLOX ovc yolox_s.onnx --output_dir ir_output执行成功后在ir_output目录下会看到yolox_s.xml和yolox_s.bin两个文件。xml 是模型结构的文本描述bin 是权重二进制两个文件必须放在同一个目录、保持同名运行时才能正确加载。如果 OpenVINO 版本较老用的是 mo那命令对应替换成mo --input_model yolox_s.onnx --output_dir ir_output4.2 输入尺寸与预处理参数如何设置有些教程会建议你在 ovc 转换时传入--scale和--mean_values让模型在 IR 层面直接包含输入归一化操作。但我实测下来在 YOLOX 场景里最优的做法是不在转换阶段做预处理而是在推理代码里用 OpenVINO 的 PrePostProcessing API 或者手动完成归一化。原因是YOLOX 的预处理逻辑是标准化到 0-1即(x / 255.0 - mean) / std其中 ImageNet 的 mean 和 std 都是固定值。如果你用 ovc 的--scale和--mean_values需要小心参数拼写和顺序非常容易搞错。而在推理代码里明确写出来反而更直观、更容易维护。如果你确实想通过在转换时传入预处理参数命令是类似这样的ovc yolox_s.onnx --output_dir ir_output --scale 255 --mean_values [123.675,116.28,103.53] --scale_values [58.395,57.12,57.375]但这种写法有几个容易踩的坑mean_values 和 scale_values 的顺序必须对应通道顺序且 OpenVINO 内部是按照每个通道的 scale 1/(std * 255) 之类的计算方式去实现的理解成本比较高。所以我更推荐运行时预处理。4.3 验证生成的 IR 文件转换完成后推荐用 OpenVINO 自带的 benchmark_app 工具快速验证模型加载和推理是否正常benchmark_app -m ir_output/yolox_s.xml -d CPU -niter 10这个命令会加载模型、跑 10 次推理、输出延迟和吞吐数据。如果这一步能顺利跑完说明 IR 文件本身没有致命问题。如果出现算子不支持或者图加载错误通常会在 benchmark 阶段直接爆出来。另外也建议打印一下模型的输入输出信息确认和 ONNX 阶段的形状一致from openvino import Core core Core() model core.read_model(ir_output/yolox_s.xml) for inp in model.inputs: print(inp.any_name, inp.shape, inp.element_type) for out in model.outputs: print(out.any_name, out.shape, out.element_type)正常情况下你会看到输入[1, 3, 640, 640]输出[1, 8400, 85]。5. 在 Windows 上运行 YOLOX OpenVINO 推理5.1 最基本的推理骨架模型转成 IR 后推理代码非常简单。下面是一段最小骨架加载模型、构造输入、执行推理import cv2 import numpy as np from openvino import Core core Core() model core.read_model(ir_output/yolox_s.xml) compiled_model core.compile_model(model, CPU) def preprocess(image, input_size640): img cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w img.shape[:2] ratio min(input_size / h, input_size / w) new_h, new_w int(h * ratio), int(w * ratio) img cv2.resize(img, (new_w, new_h)) canvas np.full((input_size, input_size, 3), 114, dtypenp.uint8) canvas[0:new_h, 0:new_w] img x canvas.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) x (x - mean) / std x x.transpose(2, 0, 1)[None] return x, ratio, (new_w, new_h) image cv2.imread(test.jpg) input_data, ratio, _ preprocess(image) output compiled_model([input_data])[0] # shape [1, 8400, 85] print(output.shape)这里有几个大家容易疑惑的点。首先YOLOX 官方预处理用的 mean [0.485, 0.456, 0.406]std [0.229, 0.224, 0.225]这和 ImageNet 的标准归一化一致。其次输入图需要做等比例缩放之后填充到 640x640填充值官方默认是 114。5.2 解码与 NMS 后处理实现模型输出[1, 8400, 85]的每一行代表一个预测框。后处理要做的工作是过滤置信度低的框、将中心点坐标转成左上角右下角坐标、按类别做 NMS。我给出的后处理代码参考如下def postprocess(output, ratio, conf_thres0.3, iou_thres0.45): preds output[0] # (8400, 85) boxes preds[:, :4] obj_conf preds[:, 4:5] cls_conf preds[:, 5:] scores obj_conf * cls_conf # (8400, 80) cx, cy, w, h boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] x1 cx - w / 2 y1 cy - h / 2 x2 cx w / 2 y2 cy h / 2 boxes_xyxy np.stack([x1, y1, x2, y2], axis1) final_boxes, final_scores, final_cls [], [], [] for cls_id in range(scores.shape[1]): score scores[:, cls_id] mask score conf_thres if not mask.any(): continue cand_boxes boxes_xyxy[mask] / ratio cand_scores score[mask] indices cv2.dnn.NMSBoxes( cand_boxes.tolist(), cand_scores.tolist(), score_thresholdconf_thres, nms_thresholdiou_thres ) if len(indices) 0: indices indices.flatten() final_boxes.extend(cand_boxes[indices]) final_scores.extend(cand_scores[indices]) final_cls.extend([cls_id] * len(indices)) return final_boxes, final_scores, final_cls注意 NMS 前要把坐标除以 ratio因为我们的输出是 640x640 坐标系下的要映射回原始图像大小。cv2.dnn.NMSBoxes 的输入是 xyxy 格式或 xywh 格式都可以但要注意它接收的是 Python list不要直接传 numpy 数组否则一些版本下会报类型错误。5.3 预处理与后处理放进生产代码时的优化上面这段代码能跑通但生产使用时有几个优化空间。第一不要每次推理都重新执行cv2.cvtColor、resize这些操作可以在视频流场景里复用已分配的内存。第二NMS 可以用向量化实现替代逐类循环或者把后处理放到独立的线程里。第三如果延迟敏感可以考虑在 OpenVINO 中通过set_input_tensor方式减少数据拷贝。另外一个比较实用的技巧是如果你的模型部署在 Intel CPU 上可以尝试把 compiled_model 的 device 从 CPU 改成 AUTO让 OpenVINO 自动选择最优设备。但要注意AUTO 模式在首次推理时可能会经历设备选择过程延迟曲线前几帧会偏高适合长期运行的服务型应用不适合对首帧延迟极其敏感的场景。6. 实战遇到的坑与排查技巧6.1 高频问题排查表问题现象可能原因解决方案导出 ONNX 时提示torch.onnx.export不支持某算子自定义 forward 中使用了一些动态控制流检查模型实现避免在 forward 中使用 Python list 拼接或可变循环必要时用 torch.onnx.is_exporting 做条件判断ovc 转换后提示Node is not supportedONNX 中包含了不常见算子或者 opset 过高重新导出 ONNX 时降低--opset到 11检查 onnxsim 是否被正确执行IR 模型加载正常但推理结果全为 0 或 nan输入预处理与训练时不一致检查 mean、std、归一化顺序、输入尺寸和填充方式输出张量形状是[1, 85, 8400]而不是[1, 8400, 85]模型输出未做 decode 或输出布局不同在导出脚本中启用 decode或者在推理代码里做 transpose 和 decode 处理检测框明显偏移坐标不对后处理时没有除以缩放 ratio确认 ratio 计算方式在显示框之前把坐标映射回原图CPU 推理速度不理想IR 中保留了动态 shape 或开启了过多线程固定输入 shape用--config CPU_THREADS_NUM4等参数控制线程数6.2 几个特别容易忽视的细节第一个是 Windows 路径问题。YOLOX 导出脚本和 ovc 对中文路径的支持不太稳定项目目录最好全英文、不要带空格。我在一开始把项目放在带中文名的目录下ONNX 导出时报了一堆莫名其妙的编码错误后来挪到纯英文路径就一切正常了。第二个是 torch 与 onnx 版本的匹配。YOLOX 官方仓库偏向旧版本 PyTorch我一开始用了最新的 PyTorch 2.x导出时出现了一些弃用警告但整体还能用。如果你遇到导出后模型结构不对的情况可以先把 torch 降到 1.13 左右再试。第三个是 fp16 精度问题。ovc 默认会把权重压缩成 fp16 以减小模型体积、提速。在 yolox-s 上fp16 和 fp32 的精度差距很小但如果你对检测框坐标精度有苛刻要求或者发现少数框的坐标有抖动可以在转换时显式加上--compress_to_fp16false回到 fp32 精度。提示OpenVINO 的 IR 文件并不包含完整的预处理参数信息。也就是说拿到同一个 .xml/.bin不同人写出来的调用代码预处理可能完全不同。为了团队协作和后续维护建议把预处理和后处理配置以注释形式写清楚或者集中放在一个配置类里。6.3 一个经典的预处理顺序问题YOLOX 官方 demo 里对输入图像的处理是先 BGR 转 RGB、再 resize、再归一化。但有些自己写代码的同学喜欢先归一化再 resize这样做会导致插值计算在非整数域上进行最终像素值和训练时的分布不一致检测精度会有轻微下降。正确的顺序一定要先做几何变换resize、padding再做色彩空间转换和数值归一化。原因很简单训练时数据增强管线就是按照这个顺序来的推理时只有复现训练时的数据分布才能让模型发挥出应有的精度。7. 性能调优与部署形态扩展7.1 固定输入形状带来的收益我最初导出的模型是动态 shapeovc 转换后 IR 里的输入维度是?或[?, 3, ?, ?]。在 CPU 上推理时每次输入不同分辨率的图OpenVINO 都要重新做一次图编译或动态 shape 推断导致延迟波动大。后来我统一用 640x640 输入延迟明显稳定下来。如果你的业务必须支持多分辨率可以将输入固定为几个档位比如 416、512、640每一种分辨率导出一个 IR 文件推理时按需加载。这在工程上比单个动态模型更可控。7.2 多线程与批量推理YOLOX 模型本身不算大yolox-s 的参数量大约 9M在 Intel 现代 CPU 上单帧 640x640 的推理延迟可以做到几十毫秒级别。如果追求吞吐量可以用 OpenVINO 的异步推理接口同时提交多个请求提高 CPU 的利用率。在 Python 里使用异步接口比同步复杂不少需要借助openvino.runtime.AsyncInferQueue。如果你的应用是单路视频流用同步推理加适当后处理就够了如果是多路视频流优先上异步队列。7.3 将模型部署为本地服务模型转成 IR 之后可以脱离 Python 环境独立运行吗实际上运行 OpenVINO 推理仍然依赖 openvino 运行时库以及对应的 C 或 Python 绑定。如果你希望部署环境完全不装 Python需要写一个 C 程序链接 OpenVINO 的运行时库加载 IR 文件并完成推理。Windows 上 C 部署需要注意 Visual Studio 版本与 OpenVINO 运行时的匹配。比如 OpenVINO 2024 版本官方预编译库要求 VS2019 或 VS2022 的运行时环境。如果部署机上缺少对应 VC Redistributable启动程序时会直接报缺少 dll。8. 最后再分享两个小技巧第一个是保存 ONNX 时用 onnxsim 压缩一下。YOLOX 官方导出脚本默认会调用 onnxsim但我发现在某些情况下 onnxsim 会报警告然后跳过。建议导出后手动跑一次python -m onnxsim yolox_s.onnx yolox_s_sim.onnx这样可以把一些冗余节点、恒等算子清理掉转换 IR 时图和运行时都会更干净。第二个是调试时可以用一张固定的测试图把 PyTorch 模型直接推理的结果保存下来与 OpenVINO 推理结果逐框对比。我遇到的大部分所谓“转换问题”其实最后都出在预处理或后处理代码上和 OpenVINO 本身没有关系。先对比再定位能省下大把排查时间。我个人在跑完这套流程后最明显的感受是模型转换链路本身并不神秘本质上就是数据的静态化和格式的规范化。真正决定部署顺不顺利的往往是你对输入输出格式、数据分布、后处理逻辑的掌控程度。抓住这三个点YOLOX 转 OpenVINO IR 这道题基本上就解开了后续换模型、换设备也都能触类旁通。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑