尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

PyTorch模型转ONNX并用OnnxRuntime实现CPU推理部署实战

PyTorch模型转ONNX并用OnnxRuntime实现CPU推理部署实战 简介模型部署是深度学习从训练走向工程落地的关键一环。在工业视觉、目标检测等场景中训练好的PyTorch模型往往需要兼顾推理性能与跨平台灵活性。ONNX作为开放的模型交换标准配合OnnxRuntime这一高性能推理引擎能够在不依赖完整PyTorch环境的情况下实现高效的CPU推理加速。本文从模型导出的基本原理讲起梳理PyTorch权重转换为ONNX格式的完整流程深入解析预处理、推理、后处理等推理管线的搭建要点并针对线程配置、图优化、固定输入形状等性能调优手段给出实测数据。同时覆盖FastAPI服务化封装与常见部署问题的排查方法帮助开发者快速将视觉检测模型落地到生产环境为后续量化加速与多模型管理等方向打下基础。 最近有个模型部署任务项目代号 INSID3模型本身是内部训练的一个视觉缺陷检测模型第三个大版本。整个部署链路从 PyTorch 训练完的权重一直做到 Python OnnxRuntime 的线上推理服务。前后折腾了一周多踩了一堆文档里不会写的坑把这套流程捋顺了。这篇文章就是完整记录给后面要碰 OnnxRuntime 部署的人一个参考尤其是那些刚把模型训出来、正准备往工程化方向推的团队。先说清楚这篇文章适合谁看用 PyTorch 训完模型、想转 ONNX 用 OnnxRuntime 在 Python 环境里做推理的或者已经在用 OnnxRuntime 但遇到性能、兼容性问题的还有想做 CPU 推理部署、不想引入太重 C 依赖的。读完你至少能搞定三件事怎么把 PyTorch 模型正确导出成 ONNX怎么用 Python 写一套完整的预处理-推理-后处理流程以及怎么排查那些最常见的部署坑。1. 内容整体设计与思路拆解1.1 INSID3 是什么为什么选 OnnxRuntime简单交代一下项目背景。INSID3 是一个用于工业视觉缺陷检测的深度学习模型输入是一张工业相机采集的产品图像输出是缺陷的类别、位置和置信度。之所以叫“3”是因为这是第三个迭代版本前两版因为推理速度和部署灵活性不达标没有真正上线。模型训练阶段用的是 PyTorch单卡 GPU 训练mAP 指标在验证集上到了 0.85 左右。训练好之后面临一个问题怎么把它跑起来。我们有三个可选方案方案一PyTorch 直接推理。最简单但必须装完整的 PyTorch 环境光依赖就有几百 MB而且每次推理有 Python 解释器的开销并发高一点就吃力。方案二LibTorch C 部署。性能最好但要写 C 代码还要处理 OpenCV、TensorRT 等一堆依赖开发周期长团队没有专职 C 工程师的话很难维护。方案三ONNX OnnxRuntime。模型转成 ONNX 标准格式用 OnnxRuntime 在 Python 里调用底层是 C 实现性能接近原生又不用写 CCPU 和 GPU 都能跑。最终选了方案三。原因很直接OnnxRuntime 的 Python API 足够简单同时底层推理引擎是高度优化的 C 运行时在 CPU 上的推理速度比 PyTorch 的 eager 模式快很多内存占用也小。最关键的是 ONNX 是一个开放标准后续如果要在别的平台上跑比如手机端的 OnnxRuntime Mobile模型可以直接复用不用重新训练。1.2 部署链路的整体架构整个部署链路分四段模型转换PyTorch 权重导出为 ONNX 格式这一步是后面所有工作的前提。预处理读入图像、resize、归一化、维度变换得到模型需要的输入张量。推理OnnxRuntime 加载 ONNX 模型执行 forward 计算。后处理解析模型输出做 NMS 去重、阈值过滤把坐标映射回原图最终输出结构化结果。这个架构从设计上就是解耦的每一段都可以单独测试。实际项目里我还加了一层 FastAPI 做 HTTP 接口包装但核心逻辑就是以上四段。设计上有个值得说的点预处理和后处理全部放在 Python 侧实现而不是塞进 ONNX 图里。原因之一是便于调试某一步结果不对可以直接 print 出来看原因之二是灵活后续如果输入图像的尺寸变了不需要重新导出模型。代价是会有一些 Python 侧的耗时但在 CPU 推理场景下预处理的时间通常只有几毫秒相比模型推理动辄几十毫秒来说可以接受。2. 核心细节解析环境准备与模型导出2.1 Python 环境与 OnnxRuntime 安装先说环境。Python 建议用 3.8 到 3.11 之间的版本。如果部署机器是内网隔离环境提前把离线 wheel 包准备好很重要否则到现场发现装不了包会非常被动。安装过程看着简单但有一个常见的坑onnxruntime 和 onnxruntime-gpu 是两个不同的包不能同时装。CPU 环境装前者GPU 环境装后者。而且 GPU 版本并不是装了就能用它还需要 CUDA 和 cuDNN 的配合版本对不上会直接报错。# CPU 版本无 GPU 场景 pip install onnxruntime # GPU 版本NVIDIA GPU CUDA 场景 pip install onnxruntime-gpu还有一件事容易被忽视OnnxRuntime 不同版本对 ONNX operset 版本的支持范围不一样。导出模型时用的是高版本 opset比如 17如果 OnnxRuntime 版本太老会报“Unsupported operator”之类的问题。最稳妥的办法是安装完 OnnxRuntime 后打印版本号确认一下import onnxruntime print(onnxruntime.__version__) print(onnxruntime.get_available_providers())get_available_providers() 这条命令能列出当前环境可用的执行提供程序Execution Provider。CPU 环境一般只有 CPUExecutionProviderGPU 环境还会多一个 CUDAExecutionProvider。如果 GPU 包装了但 CUDA 版本不匹配CUDA 提供程序就不会出现在列表里这个时候要回头检查 CUDA 环境。2.2 PyTorch 模型导出 ONNX 的完整操作把 PyTorch 模型转成 ONNX核心是 torch.onnx.export 这个函数。但实际操作不是简单调一个 API 就完事有几个关键点必须处理。先给一份我实际用的导出代码import torch import onnxruntime from models import INSID3Model # 这里换成你的模型定义 model INSID3Model(weight_pathcheckpoint.pth) model.eval() # 构造一个假输入shape 要跟模型训练时一致 dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, insid3.onnx, opset_version17, input_names[images], output_names[detections], dynamic_axes{ images: {0: batch_size}, detections: {0: num_detections} } ) print(ONNX export done.)这里有三点值得展开说。第一dynamic_axes 的取舍。如果希望模型支持任意 batch size必须声明动态轴。但同时有一个隐藏代价动态 shape 会让 OnnxRuntime 在某些情况下无法做形状相关的图优化推理速度会略有下降。我们的场景里单张图推理是主流所以最初并不需要动态 batch。不过因为后处理 NMS 之后检测框数量是变长的输出层的 num_detections 维度必须设成动态这一步是躲不开的。第二opset_version 的选择。我用的是 17。opset 版本越高可用的算子越丰富模型越容易导出成功。但要注意opset 版本高不代表 OnnxRuntime 一定支持ONNX 是一个标准各个版本的 OnnxRuntime 对不同 opset 的支持程度不同。经验是先看看当前 OnnxRuntime 版本对应的最高 opset选择比它低 1-2 个版本作为导出配置兼容性最稳。第三model.eval() 必不可少。如果漏掉这一步模型里的 BatchNorm、Dropout 会处于训练模式导出的 ONNX 模型行为会不一致——最典型的问题是推理结果和训练时的验证结果对不上。另外模型前向函数里如果有 if 语句、循环等动态逻辑导出时可能被固化成一个分支路径导致部署时行为不正确。解决办法是导出前用 test 模式仔细比对待测图像的输出。3. 实操过程与核心环节实现3.1 OnnxRuntime 推理 API 使用要点导出模型之后核心工作就是写推理代码。OnnxRuntime 的 Python API 非常轻量核心对象就两个InferenceSession 和 OrtValue或者直接用 numpy 数组。下面是我最初版本的关键代码import onnxruntime as ort import numpy as np import cv2 # 创建推理会话 session ort.InferenceSession( insid3.onnx, providers[CPUExecutionProvider], sess_optionsort.SessionOptions() ) # 获取输入输出的名称和形状信息这个信息在调试时非常重要 input_info session.get_inputs()[0] output_info session.get_outputs()[0] print(Input name:, input_info.name, shape:, input_info.shape, type:, input_info.type) print(Output name:, output_info.name, shape:, output_info.shape) # 构造输入数据用随机数模拟方便验证流程 dummy_input np.random.randn(1, 3, 640, 640).astype(np.float32) # 推理 outputs session.run([output_info.name], {input_info.name: dummy_input}) print(Output array count:, len(outputs)) print(Output shape:, outputs[0].shape)session.get_inputs() 拿到的信息很有用。在很多实际场景里导出的 ONNX 模型输入名称不是你预想的名字shape 也可能因为你设置的 dynamic_axes 变成 None。如果 run 的时候传错名字会直接报 KeyError这个错第一眼看上去很懵但实际上就是名字对不上。关于 sess_options有几个参数值得调sess_options ort.SessionOptions() sess_options.intra_op_num_threads 4 sess_options.inter_op_num_threads 1 sess_options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess_options.enable_mem_pattern Trueintra_op_num_threads 控制单个算子内部的并行线程数inter_op_num_threads 控制算子之间的并行度。对于单张图片推理的场景一般 intra 设成 CPU 核心数的 70% 左右inter 设成 1 就够了。设太高反而会因为线程切换开销导致性能下降这个我在后面的性能调优部分会详细说。3.2 完整推理流程从图像到检测结果为了让这篇博文有直接参考价值我给出一个面向实际任务的完整推理代码。模型输入是 640x640 的 RGB 图像输出是 [1, 8400, 6] 形状的检测结果其中 6 表示 [x1, y1, x2, y2, conf, cls]这里是 yolov8 风格的输出INSID3 也沿用了这个结构。代码分为三个部分预处理、推理、后处理。import cv2 import numpy as np import onnxruntime as ort class INSID3Inference: def __init__(self, onnx_path, input_size640, conf_thres0.5, iou_thres0.45): self.input_size input_size self.conf_thres conf_thres self.iou_thres iou_thres # 会话配置 self.sess_options ort.SessionOptions() self.sess_options.intra_op_num_threads 4 self.sess_options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL self.session ort.InferenceSession( onnx_path, sess_optionsself.sess_options, providers[CPUExecutionProvider] ) self.input_name self.session.get_inputs()[0].name self.output_name self.session.get_outputs()[0].name def preprocess(self, img_bgr: np.ndarray) - np.ndarray: 预处理letterbox BGR转RGB 归一化 CHW h, w img_bgr.shape[:2] # 计算缩放比例 ratio min(self.input_size / h, self.input_size / w) new_w int(w * ratio) new_h int(h * ratio) # resize img_resized cv2.resize(img_bgr, (new_w, new_h), interpolationcv2.INTER_LINEAR) # 创建灰色画布多出来的区域填充灰色114 canvas np.full((self.input_size, self.input_size, 3), 114, dtypenp.uint8) # 将resize后的图像放在画布中央 x_offset (self.input_size - new_w) // 2 y_offset (self.input_size - new_h) // 2 canvas[y_offset:y_offset new_h, x_offset:x_offset new_w] img_resized # BGR转RGB归一化转换为CHW rgb cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB) rgb rgb.astype(np.float32) / 255.0 chw np.transpose(rgb, (2, 0, 1)) # (3, 640, 640) input_tensor np.expand_dims(chw, axis0).astype(np.float32) # (1, 3, 640, 640) return input_tensor, ratio, x_offset, y_offset def postprocess(self, outputs, ratio, x_offset, y_offset): 后处理解析输出、置信度过滤、NMS、坐标映射回原图 preds outputs[0] # (1, 8400, 6) preds preds[0] # (8400, 6) # 置信度过滤 scores preds[:, 4] mask scores self.conf_thres preds preds[mask] if len(preds) 0: return [] boxes preds[:, :4] scores preds[:, 4] cls_ids preds[:, 5] # NMS keep self._nms(boxes, scores, self.iou_thres) results [] for idx in keep: x1, y1, x2, y2 boxes[idx] # 坐标映射回原图尺寸 x1 (x1 - x_offset) / ratio y1 (y1 - y_offset) / ratio x2 (x2 - x_offset) / ratio y2 (y2 - y_offset) / ratio results.append({ box: [float(x1), float(y1), float(x2), float(y2)], score: float(scores[idx]), class_id: int(cls_ids[idx]) }) return results staticmethod def _nms(boxes, scores, iou_thres): 纯 numpy NMS 实现不依赖 torch/torchvision x1 boxes[:, 0] y1 boxes[:, 1] x2 boxes[:, 2] y2 boxes[:, 3] areas (x2 - x1) * (y2 - y1) order np.argsort(scores)[::-1] keep [] while order.size 0: i order[0] keep.append(i) if order.size 1: break xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter_w np.maximum(0.0, xx2 - xx1) inter_h np.maximum(0.0, yy2 - yy1) inter inter_w * inter_h iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou iou_thres)[0] order order[inds 1] return keep def infer(self, img_bgr): input_tensor, ratio, x_offset, y_offset self.preprocess(img_bgr) outputs self.session.run([self.output_name], {self.input_name: input_tensor}) return self.postprocess(outputs, ratio, x_offset, y_offset)先解释一下 preprocess 里为什么用 letterbox 而不是直接 resize。工业图像的宽高比往往不是 1:1直接拉伸会导致目标失真模型精度下降。letterbox 的做法是保持原图比例缩放到一个框内剩余区域用灰色填充这样模型看到的内容不会被扭曲推理精度更接近训练时的表现。填充值为什么用 114这不是随机选的。在大多数目标检测模型的训练和推理流程里YOLO 系列默认用 114 作为填充值或者用均值归一化后的 114/255代码里后面归一化会把它除到 0.45 左右这是一个约定俗成的经验值效果在多数场景下表现良好。如果你在训练时用了不同的填充值推理要跟训练保持一致否则会有一点点精度损失。3.3 NMS 为什么要用纯 numpy 实现项目初期我试图用 torchvision.ops.nms 做后处理因为训练时代码里就直接调的这个。但部署环境不一定装了 torchvision而 torchvision 在 CPU 环境下安装体积巨大、很容易遇到各种依赖冲突。后来改成了纯 numpy 实现只依赖 OpenCV 和 NumPy这两个在部署机器上几乎都有。这份 NMS 代码是从 Fast NMS 的思路改来的效率经过验证在 8400 个预测框中经过置信度过滤后通常只剩几十到几百个框纯 Python numpy 的 NMS 单帧耗时在 1-2 毫秒以内完全够用。如果过滤后仍有上万框也可以考虑用图片上下的坐标排序再做 NMS 的优化版本不过我们实际测试下来 8400 的规模没有性能压力。4. 性能优化与常见问题排查4.1 CPU 推理性能调优的四个重点INSID3 上线初期跑在 CPU 环境上CPU 推理性能是直接关系用户体验的环节。我实测中花了最多时间在下面四个地方。第一线程数设置。前面代码里已经给出intra_op_num_threads 是最影响单帧耗时的参数。我用一个 8 核的测试机做对比线程数设置为 2、4、8 分别测试 100 张图结果如下线程数单帧平均耗时ms1138282458863结论很直观线程数超过 4 之后性能不仅没有继续提升反而下降了。原因是过多的线程竞争会导致缓存命中率下降和上下文的切换开销而且 OnnxRuntime 内部的算子并行本身也有调度成本。实际部署时建议先用 coreutils 或者 Python 的 os.cpu_count() 拿到机器核心数然后按核心数的一半左右开始往上调多跑几个档位选最优值。第二GraphOptimizationLevel。默认是 ORT_ENABLE_ALL这个不要动。它会让 OnnxRuntime 对计算图做合并优化把很多小的算子合成一个更高效的算子最典型的是把 Conv BN Relu 合并成一个算子减少内核启动开销。如果你自己调试时发现能合并的算子没合并可以考虑安装 onnxconverter_common 做一次离线优化但大多数场景下 OnnxRuntime 的在线优化已经够用。第三固定输入形状。如果你的业务场景是固定尺寸输入比如所有图上送前都会 resize 到 640x640建议导出模型时不用 dynamic_axes直接固定 batch1。固定形状可以让 OnnxRuntime 在内存分配和算子选择上做出更优的决策。INSID3 第一版用了动态轴后来切成固定形状后单帧提速大约 8%。但如果你的输入尺寸变化很大这个优化就不适用。第四减少 Python 侧的数据拷贝。读图和预处理返回的 numpy 数组如果后续不需要再用尽量减少 .copy() 的调用。在 session.run 传 numpy 数组时OnnxRuntime 默认会将输入数据拷贝到内部缓冲区如果你用 OpaqueTensor 类型的 OrtValue 传数据可以减少一次拷贝但使用复杂度高。我的取舍是先用 numpy 数组保持代码可读性等到性能瓶颈确实出现在 IO 侧时再优化。4.2 高频报错与排查记录部署期间碰到的问题不少这里挑几个最具代表性的整理成表格方便大家直接对照排查。报错信息原因解决办法CUDAExecutionProvider is not in the available providersGPU 包和 CUDA 版本不匹配或环境变量未设置检查 CUDA 版本与 onnxruntime-gpu 的对应关系必要时设置 LD_LIBRARY_PATHUnexpected input data type. Actual: (tensor(float)) , expected: (tensor(double))输入 numpy 数组是 float64预处理阶段加 .astype(np.float32)这个错在第一次跑的时候很容易犯KeyError: images传入的输入名跟 ONNX 图里的名字不一致用 session.get_inputs()[0].name 打印真实名字不要凭记忆写Conv activation shape mismatch模型输入尺寸和固定 anchor 配置不匹配重新导出模型调整输入为训练时实际尺寸Failed to load library ... cudnn64_8.dllGPU 环境缺 cuDNN 库安装对应版本的 cuDNN或改用 CPU 版先跑通流程还有一类问题我特别说一下因为它容易被忽略模型本身能加载但推理结果和训练时验证结果不一致。这通常在导出后第一次跑真实图片时发现。排查思路有三个方向一是检查预处理是否跟训练保持一致特别是归一化的 mean/std二是检查 model.eval() 是否调用了三是检查动态控制流是否在导出时被错误固化。这三个依次排查绝大多数精度问题都能解决。4.3 Batch 推理的取舍做服务的时候自然想到一个优化点能不能一次处理多张图用 batch 1 跑我实测过batch1 和 batch4 在 CPU 上的单图平均耗时其实是下降的因为算子并行可以摊薄固定开销但 batch4 内存占用会上升且增加了后处理的复杂度。对于工业现场相机一秒拍 1-2 张的场景单张推理完全够不需要引入 batch。如果你的场景是批量离线检测可以用 batch8 或 batch16 跑但后处理要改成批量读取输出数组逻辑复杂不少。5. 服务化集成与部署实践5.1 用 FastAPI 包装一次推理服务模型跑通到单张图片推理之后下一步就是把它封装成服务。我选 FastAPI理由很简单它是目前 Python 生态里异步和文档支持最好的 Web 框架之一自动生成 Swagger 接口文档方便测试和对接。一个最简版本的封装逻辑如下核心就是把 INSID3Inference 实例作为单例加载接口层只负责接收图片、调用推理、返回 JSONfrom fastapi import FastAPI, UploadFile, File from insid3_infer import INSID3Inference import cv2 import numpy as np app FastAPI() model INSID3Inference(insid3.onnx) app.post(/detect) async def detect(file: UploadFile File(...)): img_bytes await file.read() img_array np.frombuffer(img_bytes, dtypenp.uint8) img_bgr cv2.imdecode(img_array, cv2.IMREAD_COLOR) if img_bgr is None: return {error: invalid image} results model.infer(img_bgr) return {results: results}有两个服务化的细节值得关注。第一不要每次请求都重新加载模型。模型加载涉及 ONNX 图的解析和图优化这通常需要几百毫秒甚至几秒。我的代码里 model 在模块导入时就初始化了是一个全局单例所有请求共享这一个 InferenceSession。多个线程同时调用 session.run 是线程安全的OnnxRuntime 内部会做并发控制不用担心。第二UploadFile 拿到的 bytes 要先用 np.frombuffer 转成 numpy 数组再用 cv2.imdecode 解码。这个顺序不要搞反。如果直接向 cv2.imdecode 传 bytes 是没有问题的但搭配 FastAPI 的 UploadFile 时拿到的是 async 对象必须先 await 再处理。5.2 部署环境里的模型文件管理模型文件 insid3.onnx 本身大约有 40 MB项目用 Docker 镜像打包部署。这里有个技巧ONNX 模型文件比较稳定不适合每次代码更新都重新打进镜像可以在 Dockerfile 里用多阶段构建或者把模型文件挂载到容器的数据卷里。我用的是后者更新模型权重时只需要替换挂载目录里的文件再重启容器不用重新构建镜像。镜像里的 Python 依赖用 pip freeze 生成 requirements.txt 固定版本。特别要注意 onnxruntime 的版本要固定因为不同版本之间的图优化逻辑差异很大升级 onnxruntime 后可能性能或结果有变化。在没做充分回归测试之前不要轻易升级。6. 踩坑实录与经验心得6.1 一个隐蔽的精度问题预处理和图优化有一次在验证部署模型的精度时发现 ONNX 模型在单张测试图上输出的置信度和 PyTorch 模型差了 0.02。这个差异很小但多测几张图后发现这不是随机波动而是系统性偏差。排查了很久最后发现问题出在导出的动态控制流上。模型里有一个自定义算子在推理时用于根据 mask 决定是否跳过某些特征这个 if 操作在导出时被 torch 固化成了固定路径导致行为不一致。解决办法是在 torch.onnx.export 里加了一个 symbolic 函数的映射把这个自定义算子替换为 ONNX 支持的基础算子组合。这个问题的教训是导出 ONNX 前一定先检查模型前向代码里有没有 Python 原生的 if/else、for 循环等控制流。如果有优先改成 torch.where、torch.nonzero 等支持 ONNX 导出的操作。否则你可能会得到一个“能跑但结果不对”的模型这类问题比直接报错更浪费时间。6.2 OnnxRuntime 与 OpenCV 的图像格式问题还有一个低级但容易犯的错误OpenCV 读图默认是 BGR而模型训练时用的是 RGB。如果不做转换模型看到的颜色通道是反的检测效果会很差。这个问题在训练阶段因为有数据加载器的处理通常不会暴露但部署到推理流程后只要少写一行 cv2.cvtColor精度立刻掉十几个点。我的建议是把这个转换写在 preprocess 的固定流程里不要留给调用方。同时代码注释里明确注明“输入是 BGR模型期望 RGB”避免后来的人改代码时把这个转换去掉。6.3 输出张量维度的“约定”ONNX 模型的输出张量 shape 并不是标准化的完全取决于你训练时怎么定义模型的 forward 输出。我遇到过几种坐标系表示YOLOv5 用的是 xywh 归一化坐标YOLOv8 用的是 xyxy 像素坐标还有的模型输出的是 stride 为 32 的 feature map需要逐层解码。部署时候最耗时间的反而是这些后处理代码的调试。我的建议是拿到 ONNX 模型后第一步先用一个小的测试图跑一遍把输出数组的 shape、数值范围打印出来对着训练代码的输出格式做比对确认坐标系和缩放方式再写后处理。不要想当然。INSID3 就是因为训练时已经统一成了 xyxy 格式后处理才没有额外坑。7. 后续优化方向INSID3 目前已经在生产环境稳定跑了近一个月CPU 单帧推理耗时从最初的 140 ms 优化到 60 ms 左右满足工业现场的实时性要求。接下来有几个明确的方向可以继续推进。第一模型量化和加速。当前模型权重是 FP32可以尝试转成 FP16在 GPU 上或用 dynamic quantization 转成 INT8在 CPU 上。量化后的模型体积可以缩小到原来的四分之一推理速度在 CPU 上还能再提升 30%-50%。只是量化之后需要做充分的精度验证特别是工业缺陷检测这种对微小特征敏感的任务量化掉点风险要评估清楚。第二多模型并发管理。如果后续要同时跑多个 OnnxRuntime 模型比如在检测之后串联一个分类模型可以做一个 InferenceSession 池避免多个请求反复创建和销毁会话。第三尝试 TensorRT。如果部署环境换成 GPUTensorRT 的加速效果比 OnnxRuntime 的 CUDA 后端更明显。ONNX 模型可以作为 TensorRT 的输入不用重新训练。只是 TensorRT 的部署复杂度高不少缓存、版本匹配、动态形状都是坑建议等 CPU 方案确实撑不住的时候再考虑。最后说一个小技巧——如果真的要在生产环境用 OnnxRuntime建议把所有跟模型有关的信息版本、opset、输入输出名称、shape都写到一个 config 文件里不要散落在代码各处。这样不管是升级模型还是排查线上问题都能快速定位信息不用每个文件翻一遍。这点小习惯遇到线上事故的时候能帮你节省不少排查时间。本文还有配套的精品资源点击获取
返回列表