简介:ONNX Runtime 1.23.1官方预编译的Windows x64 CPU版本安装包,面向需要在Windows环境下快速集成ONNX推理能力的C/C++开发者,尤其适合官方下载渠道不稳定或需要离线备份的用户。包体共26个文件,整体约74.48MB,目录结构简洁:14个头文件完整覆盖C/C++ API、自定义算子、训练接口、会话配置等声明,可帮助开发者快速了解接口调用方式;配套的dll动态链接库与lib导入库用于编译链接,pdb调试符号便于排查问题,还附带详细的README、第三方声明、许可证、版本号与提交标识等文档,合规性较好。开发者拿到手后可在Visual Studio等环境中直接引用,免去自行编译ONNX Runtime的复杂过程,快速搭建本地推理服务或离线模型部署流程;也可将压缩包留存作为固定版本备份,便于日后环境重建时使用。目前已有47人学习/下载,适合刚接触ONNX Runtime的开发者以及需要稳定版本进行项目集成的资深工程师。
1. onnxruntime-win-x64-1.23.1.zip:一个被当成安装包、其实是推理引擎的 zip
拿到 onnxruntime-win-x64-1.23.1.zip 这个文件名,很多从 PyTorch 转过来的同事第一反应是“装个框架”,解压后却发现里面只有一堆 dll 和头文件,没有任何安装向导。这个 zip 其实是 onnxruntime 在 Windows x64 下的发布产物:它把 C++ 动态库、头文件和链接需要的 lib 整合在一起,专门用来在 CPU/GPU 上跑 ONNX 推理,不负责训练。我去年把 Windows 推理机从旧版本切到 1.23.1 后,最直观的感受是算子覆盖更全、OPENMP 的线程控制更稳,单条样本的推理耗时也比之前降了一档。这篇文章按我实际部署的顺序写:先讲透这个 zip 里到底是什么,再给出可复现的安装与最小推理步骤,最后把 Windows 上最容易翻车的几个坑列清楚。适合要落地 ONNX 模型、但不想碰训练框架的读者,也适合正在评估要不要用 onnxruntime 做生产推理的人。
2. 拆开 zip 看门道:onnxruntime、onnx 与 win-x64 后缀分别说了什么
2.1 onnxruntime 和 onnx 的区别:一个是菜谱,一个是厨房
很多检索者会把 “onnxruntime 和 onnx 区别 概念” 当成同一个东西,这是第一个需要掰开的概念。onnx 是一种开放模型格式,它定义的是计算图的结构:每个算子是什么、张量怎么流动、权重存在哪。你可以用 Netron 打开一个 .onnx 文件,看到的是静态的“菜谱”。而 onnxruntime 是执行这个菜谱的“厨房”——它读取计算图,做图优化、算子融合、内存复用,然后调用 CPU/GPU 的底层指令真正跑出数值结果。
明白了这个区分,也就明白为什么 onnxruntime-win-x64-1.23.1.zip 里没有任何训练组件:它不需要反向传播、不需要优化器、不需要自动求导。它只需要把前向计算跑快。图优化是 onnxruntime 的核心价值之一,比如把 Conv + BatchNorm + Relu 这种经典三段融合成一个算子,减少张量在内存里的来回搬运;再比如做常量折叠,把输出只依赖常量的子图在加载期就算完,运行期直接读结果。这些优化在 PyTorch 的 eager 模式里很难做得彻底,也是我建议推理阶段单独走 onnxruntime 而不是继续用训练框架跑 forward 的主要原因。
另一个直接相关的问题是:既然已经有了 ONNX 格式,为什么还要 onnxruntime?ONNX 本身只是一份协议,不同厂商可以各自实现自己的 runtime。onnxruntime 在 x64 Windows 上给开发者提供的是一套一致的内存管理、线程池和 EP(Execution Provider)抽象。你可以先写一套调用代码,之后想从 CPU 切到 CUDA 或 TensorRT,只改 providers 参数,业务代码不用动。这个“换硬件不换代码”的特性,在实际交付时比纸面上的推理速度差异更重要。
2.2 版本号 1.23.1:什么时候值得跟着升级
onnxruntime 的版本号节奏比较快,release 通常按月或按里程碑推进。1.23.1 属于 1.23 系列的小修复版本,适合在 Windows x64 生产环境接手。选择版本时,先看两个指标:一是对 ONNX opset 的兼容范围,二是你手头模型里用到的算子在不在这张“支持列表”里。常见做法是导出模型时就固定一个目标 opset,比如 17 到 21 之间,跑一圈 onnxruntime 的模型转换测试,确认没有 Unsupported Operator 再定版本,而不是盲目追新。
1.23 系列相对老版本的一个实用改进是对更多算子的 CPU 实现做了优化,尤其是 Transformer 类结构里常见的 Attention、LayerNormalization,在 x64 Windows 上跑 NLP 模型时能吃到红利。但版本升级也意味着动态库体积变大、依赖的 Visual C++ Runtime 版本变高。如果目标机器是封闭内网的老系统,升级前最好先确认 UCRT(Universal C Runtime)版本,否则很容易掉进后面讲到的 dll 缺失问题。
顺带说一句:文件名里的 win-x64 直接说明这是面向 Windows x64 指令集的发布物。arm64 和 x64 有什么区别,很多顺手下载的人并不在意,但它在部署时有硬性影响——arm64 版跑的是 AArch64 指令,x64 的动态库无法在 ARM Windows 上加载,反之亦然。判断机器架构最稳的方式是打开命令行跑echo "$env:PROCESSOR_ARCHITECTURE",看到 AMD64 再下载 x64 包,看到 ARM64 就去取对应版本。
2.3 win-x64 与 zip 的发布形态:为什么官方偏向“免安装”
这个 zip 不是安装包,而是一个免安装分发物。解压后常见的文件结构大致是 include/ 下放 C++ 头文件,lib/ 下放 onnxruntime.lib 和 onnxruntime.dll,另有 providers 相关 dll。官方同时提供 NuGet 包和 Python wheel,但 zip 形态对两类人最有用:一类是要用 C++ 集成、想自己控制 dll 放置目录的桌面端开发者;另一类是离线内网部署,无法执行 pip install,需要一个可以随程序一起拷贝的分发目录。
zip 免安装的代价是你要自己维护运行环境。动态库的搜索路径、Visual C++ Runtime 的缺失、DLL 目录下有没有多余的旧版本,这些问题在安装包里会被安装器掩盖,在 zip 部署时全部暴露出来。这时候就体现出一个工程习惯:解压后不要乱动目录结构,用完整个onnxruntime-win-x64-1.23.1文件夹直接拷贝到目标机器,保持 include 和 lib 的相对位置一致,能少踩很多坑。
这个包还有个容易被忽略的细节:Windows x64 zip 里虽然带了 onnxruntime.dll,但它不一定包含 GPU 相关的 Execution Provider。如果你计划用 CUDA 跑,需要另外准备 CUDA EP 的动态库,并装好和当前 onnxruntime 版本配套的 CUDA / cuDNN。先把 CPU 推理跑通,再谈 GPU,是我一贯的顺序。
3. 在 Windows x64 上跑通 onnxruntime:解压、安装与最小推理
3.1 把 onnxruntime-win-x64-1.23.1.zip 解压成可用的动态库
在 Windows 上解压这个 zip,不需要第三方工具。PowerShell 的Expand-Archive是内置命令,Windows 10 1803 之后的系统也自带tar,两条路都可以走。我一般会把解压目标固定成一个纯英文路径,比如D:\ort\onnxruntime-win-x64-1.23.1,避免日后的 C++ 工程因为中文路径出现头文件找不到这类玄学问题。
# 解压到指定目录,-Force 表示目录已存在时覆盖 Expand-Archive ` -LiteralPath "D:\downloads\onnxruntime-win-x64-1.23.1.zip" ` -DestinationPath "D:\ort" ` -Force # 验证解压结果 Get-ChildItem "D:\ort\onnxruntime-win-x64-1.23.1"-LiteralPath在这种带点和数字的文件名里更稳妥,避免通配符展开的意外。-DestinationPath指向的是外层目录,解压后会生成以原 zip 名为名的子目录。执行完列出目录,你至少应该看到 include 和 lib 两个文件夹。如果你手头是 cmd 环境,也可以用tar -xf onnxruntime-win-x64-1.23.1.zip -C D:\ort,同样的效果。
这里有一个值得养成的习惯:解压后立刻校验文件大小和哈希。下载中断产生的半截 zip 是最常见的部署翻车原因,光看文件存在没有任何意义。生产环境里最好保留官方发布的 SHA256 值,解压前先比对,而不是等加载 dll 报错时再回头怀疑压缩包。
3.2 用 Python 还是用 C++:zip 里的动态库给谁用
对于快速验证模型效果,Python 是效率最高的路径。但要注意,pip install onnxruntime安装的 wheel 内部其实也带了一份 onnxruntime dll,和这个 zip 是两套独立产物。zip 的价值主要体现在两处:一是离线环境没有 PyPI 可用时,你可以把 zip 里的 dll 手动放进 Python 的onnxruntime/capi目录,替代 pip 安装;二是 C++ 工程直接链接这里的 onnxruntime.lib,用动态库而不依赖 NuGet。
我建议的判定标准很简单:如果业务代码是 Python,直接用 pip 装,zip 只作为离线备份;如果业务代码是 C++/C#/Delphi 这类原生程序,才需要把 zip 当作正式依赖。想确认 zip 里的动态库能被 Python 识别,可以解压后先看lib\onnxruntime.dll的位数——用 PowerShell 的Get-Item拿不到位数,直接运行dumpbin /headers或corflags更直观,但最省事的办法还是看 zip 文件名里的 x64 后缀。
3.3 Python 最小推理代码:从会话创建到输出结果
跑通 onnxruntime 的最小闭环,不需要训练、不需要 GPU,只需要一个能加载的 onnx 文件和下面这段代码。我用的是 CPU Execution Provider,这是 x64 zip 默认自带且最稳的路径。
import onnxruntime as ort import numpy as np # 1. 创建推理会话,只指定 CPU EP sess = ort.InferenceSession( r"D:\models\resnet50.onnx", providers=["CPUExecutionProvider"], ) # 2. 打印输入输出信息,确认模型结构有没有问题 inputs = sess.get_inputs() outputs = sess.get_outputs() print("input:", inputs[0].name, inputs[0].shape, inputs[0].type) print("output:", outputs[0].name, outputs[0].shape, outputs[0].type) # 3. 构造假数据跑一次,验证会话可用 rng = np.random.default_rng(0) fake_input = rng.normal(size=(1, 3, 224, 224)).astype(np.float32) res = sess.run( None, # 输出全部节点 {inputs[0].name: fake_input}, ) print("result shape:", res[0].shape)InferenceSession的第一个参数是 onnx 文件的绝对路径;providers是关键参数,1.23 版本对空列表会自动 fallback 到 CPU,但我从不依赖这个隐式行为,总是显式写明 CPUExecutionProvider。sess.run的第一个参数传None表示要全部输出节点,第二个参数是输入映射,key 必须是get_inputs()[0].name返回的名字,不能用模型文件里的张量名随便猜。之所以用np.random.default_rng(0).normal而不是np.random.randn,是让随机数据可复现,排错时能保证两次跑同一个输入。
3.4 C++ 最小调用骨架:链接动态库的正规姿势
如果业务端是 C++,重点在于链接方式。Visual Studio 里配置好 include 目录为include,库目录为lib,然后链接onnxruntime.lib。运行时要保证onnxruntime.dll能被找到——要么放在 exe 同目录,要么把lib目录加到 PATH。下面的代码只做了会话创建和输入名读取,足够验证动态库是否工作。
#include <onnxruntime_cxx_api.h> #include <iostream> int main() { // 日志等级 WARNING,避免每个算子都打日志 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test"); Ort::SessionOptions opts; opts.SetIntraOpNumThreads(4); // 宽字符路径,避免中文路径编码问题 Ort::Session session(env, L"D:\\models\\resnet50.onnx", opts); // 拿到输入名,验证会话确实初始化成功 Ort::AllocatorWithDefaultOptions alloc; auto input_name = session.GetInputNameAllocated(0, alloc); std::cout << "input: " << input_name.get() << std::endl; return 0; }Ort::Env是进程级单例,整个程序里创建一个就够,不要每个请求都新建。SetIntraOpNumThreads(4)设的是算子内部并行度,在 CPU 推理场景影响最大。会话创建成功后,GetInputNameAllocated返回的是智能指针包装的字符串,不用手动释放内存。如果你在链接阶段报LNK2019 unresolved external symbol,先去查是不是没有正确加载 onnxruntime.lib,或者工程运行时库设置和 dll 的 Release/Debug 不一致,这两个是 C++ 接入时最高频的问题。
4. 模型转换与推理参数:从 PyTorch 导出到 onnxruntime 的 4 个必调点
4.1 导出阶段就排雷:opset_version、dynamic_axes、模型简化
推理跑不跑得动,一半在导出时就已经注定。PyTorch 导出 ONNX 最常见的写法是torch.onnx.export,但很多人只传前三个参数就完事,结果到了 onnxruntime 这边要么算子不支持,要么输入尺寸固定死。下面是带全参数的导出代码:
import torch import torchvision.models as models model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, "resnet50.onnx", opset_version=18, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, do_constant_folding=True, )opset_version=18在 onnxruntime 1.23.1 上被支持得很成熟。如果你模型里用了很新的算子,再往上调 opset,但每调高一档都要用 onnxruntime 实测一遍。dynamic_axes只动态化了 batch 维度,height 和 width 仍然固定为 224;如果图像尺寸多变,需要把{1: "height", 2: "width"}也加进去,但动态维度越多,onnxruntime 的 shape inference 越容易出幺蛾子,所以能固定就固定。do_constant_folding=True会把权重里的常量子图提前算掉,对推理速度有正收益。
导出后强烈建议再做一次模型简化。常见做法是pip install onnxsim,然后用python -m onnxsim resnet50.onnx resnet50_sim.onnx。它会把冗余的 Identity、Reshape 等节点清掉,减小模型体积,也减少 onnxruntime 加载时的优化时间。1.23.1 对简化后的模型兼容性很好,遇到算子报错时先简化再排查,很多时候报错就消失了。
4.2 会话三件套:线程数、执行模式、图优化等级
onnxruntime 的性能差距往往不在框架本身,而在会话参数。我最常用的 SessionOptions 配置如下:
import onnxruntime as ort so = ort.SessionOptions() so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL so.intra_op_num_threads = 4 so.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL so.enable_profiling = False sess = ort.InferenceSession( r"D:\models\resnet50_sim.onnx", sess_options=so, providers=["CPUExecutionProvider"], )graph_optimization_level默认就是 ORT_ENABLE_ALL,没必要关。但排查算子报错时,我偶尔会临时改成ORT_DISABLE_ALL,确认问题是否源于图优化中的算子融合,这只作为定位手段,不作为生产配置。intra_op_num_threads建议先按物理核心数设,再上下浮动测试,不要盲目设成逻辑线程数。超线程让逻辑核翻倍,但算子内部的 OpenMP 并行在 8 核 16 线程的机器上,设 16 往往比设 8 还慢,上下文切换吃掉太多收益。ORT_SEQUENTIAL是串行执行图中节点,对大多数 CNN 模型比ORT_PARALLEL更稳定;并行图模式只在线性结构很弱、分支很多的计算图上才有优势。
enable_profiling保持 False,等需要定位单算子耗时再开。还有几个容易被忽略的参数:so.register_custom_ops_library用来自定义算子;so.add_session_config_entry("session.use_deterministic_compute", "1")可以让多次推理结果一致,代价是可能损失部分算子优化,仅供需要严格复现结果的场景。
4.3 输入动态维度和 NCHW 布局的坑
onnxruntime 对输入张量的处理比 PyTorch 严格。PyTorch 里你可以随意 reshape、permute,但 onnx 模型里的 shape 是静态推导出来的,如果你导出时没有声明 dynamic_axes,运行时输入尺寸必须和导出一致,否则直接抛 shape mismatch。最常见的报错是 “Expected shape (1,3,224,224) but got (8,3,224,224)”。
遇到这种已经固定维度、又需要变 batch 的模型,我一般直接在导出端解:重新执行 4.1 的导出代码,给 batch 加上 dynamic_axes。如果模型是第三方给的、没有导出源码,只能做输入 padding:把不同尺寸的图统一 resize 或 padding 到固定大小,再配合dtype: np.float32输入。这也解释了为什么很多 C++ 部署服务里图片预处理都写死在模型外——onnxruntime 不会好心地帮你自动 resize。
至于 NCHW 布局,onnx 里视觉模型的卷积权重默认是 NCHW,而 OpenCV 读进图片是 HWC。很多新手把 OpenCV 的 HWC 张量直接喂给 onnxruntime,得到的结果要么形状报错、要么数值全错但形状刚好对上。正确的预处理应该包含np.transpose(img, (2, 0, 1))这一步,必要时还要做img.astype(np.float32) / 255.0的归一化。这些操作必须在模型外完成,因为 onnxruntime 不会自动转换布局。
4.4 用 onnxruntime 动态库时最容易误用的两处配置
第一处是把intra_op_num_threads和inter_op_num_threads当成一回事。前者控制单个算子内部的并行度,后者控制图中多个无依赖算子的并行度。对大多数模型,inter_op_num_threads默认值和intra_op_num_threads保持一致即可,不需要专门调。第二处是盲目开启ORT_PARALLEL执行模式指望所有算子同时跑,实际效果经常是线程池抢资源,带来额外的同步开销和不确定性。并行执行模式在 CPU 上的收益非常依赖模型拓扑,通常只在模型有大量并行分支时才有价值。先用 ORT_SEQUENTIAL 拿到一个稳定的基线,再对比并行模式,才是理性的调优顺序。
5. Windows 部署 onnxruntime 的 5 条避坑记录
5.1 加载报缺 api-ms-win-*.dll,程序起不来
现象:Python 端import onnxruntime报 “DLL load failed while importing onnxruntime_pybind11_state”;C++ 端报 “找不到 api-ms-win-core-path-l1-1-0.dll”,程序还没跑 main 就崩了。
原因:onnxruntime 1.23.1 依赖较新的 Universal C Runtime,而目标机器是精简版 Windows 或多年没打补丁的老系统,系统目录里缺 UCRT 相关的 api-ms-win-* 转发 dll。这和 onnxruntime 本身没有关系,是系统运行库版本不匹配。
解决:先装 Visual C++ Redistributable x64(VC_redist.x64.exe),装完重启再试。如果还报 api-ms-win 系列缺失,更彻底的方案是打系统更新补丁,或手动安装对应 KB 的 UCRT 更新。部署到内网机器时,把 VC_redist 一并拷贝进安装包,并设计一个启动前自检脚本,用where api-ms-win-core-path-l1-1-0.dll之类的方式探活,而不是等程序闪退后再人工排查。
5.2 zip 解压到一半报 could not find EOCD 或文件伪加密
现象:Expand-Archive解压到一半报 “Invalid zip archive: could not find EOCD”;用 7-Zip 打开能列目录但解压个别文件提示密码。在 MISC 题目包里常见的 zip 伪加密,在真实部署包偶尔也会遇到——压缩包制作者只是把 general purpose bit 置位,并没有真正加密。
原因:EOCD 是 zip 中央目录末尾的记录,找不到 EOCD 通常是下载不完整或传输中断;伪加密则是文件头标志位被改,导致解压器认为需要密码,实际上数据本身没有加密内容。
解决:下载完先校验 SHA256,这是最硬的一关。如果哈希对不上,直接重新下载,不要在坏包上浪费时间。哈希没问题但解压报伪加密,用 7-Zip 解压时若弹出密码框,先不输入密码,点确定试一次——伪加密的文件往往能直接解出来。真加密的 zip 是另一回事,正常业务包不会加密码;网上付费移除 zip 密码的工具大多是破解弱口令的脚本,生产环境不该走这条路。血泪教训:因为疑似“缺密码”而放弃一个模型包,结果对方其实是拿 zip 伪加密防迅雷盗链,多问一句比折腾工具省几天时间。
5.3 模型在 Netron 里正常,加载却报 Unsupported Operator
现象:onnx 文件在 Netron 里每个算子都认识,但 onnxruntime 创建会话时报 “Unsupported operator” 或 “Invalid Graph”,具体算子名指向某个较新的 Transformer 算子。
原因:Netron 只是可视化工具,它对算子的识别范围比 onnxruntime 的算子实现宽;或者导出时的 opset 版本高于 onnxruntime 1.23.1 支持的算子集范围,导致某个新算子没有对应 kernel。还有一种常见原因:模型来自 PyTorch 自带导出但没做简化,留下了自定义符号节点。
解决:先把导出端的opset_version降一档重新导出,比如从 21 降到 18,多数兼容问题能消掉。然后跑onnx.checker.check_model和 onnxsim 做一遍静态分析。如果确认是自定义算子,要么在导出前把自定义逻辑拆成 onnxruntime 支持的基础算子,要么用RegisterCustomOps自己实现这个 kernel——后者的成本通常很高,不建议刚上手就碰。判断优先级的标准是:报错算子出现在模型的哪一段,影响面大就先改导出,影响面小就换支持模型。
5.4 动态维度失效:batch 变了直接报错
现象:同一份 onnx 模型,单张推理正常,改成 batch=8 后 onnxruntime 报 shape mismatch,日志提示输入张量维度必须等于 1。
原因:导出时没有声明dynamic_axes,模型里的输入 shape 被静态固定成(1,3,224,224)。onnxruntime 不会对输入做自动广播或 reshape,差一个维度都不接受。
解决:回到导出端给 batch 维度加 dynamic_axes(参考 4.1 的代码)。如果是别人给的模型,检查输入 size 是否包含动态维度;确实没有,就只能在预处理里做固定尺寸 padding。顺带一个容易被忽略的点:动态维度只给了 batch,不代表 height 和 width 也动态,想支持多尺寸输入要把这三维全部声明,代价是可能触发部分算子不做常量折叠。取舍思路是:模型服务只面向固定输入就先固定,生产环境里动态维度的维护成本远高于收益。
5.5 线程数设太高,推理反而变慢
现象:intra_op_num_threads=16设到逻辑线程数,单条推理耗时不降反升,CPU 使用率接近 100%,服务 QPS 掉一半。
原因:线程池过度订阅,多个算子并行时线程切换开销大过计算开销。在超线程机器上,逻辑核和物理核的并行收益不对等,OpenMP 甚至会触发 CPU 降频保护。我在 8C16T 的 Windows 机器上实测,intra_op_num_threads 从 8 提到 16,ResNet50 单条推理反而慢了 12%。
解决:先设成物理核数,再测相邻的两个值,比如物理核是 8,就对比 6、8、10 三档。机器上如果还跑 web 服务、消息队列等进程,线程数再减 1 到 2,给其他程序留余量。用 4.2 的配置跑 100 次取平均耗时,不要拿单次最快值当结论。这个性能调优过程没有银弹,每台机器的内存带宽和散热策略都不一样,必须实测定参数。
6. 用 profiling 验证 onnxruntime 是否真的更快
性能优化不能靠感觉,1.23.1 自带 profiling 钩子,它会把每个节点的耗时、算子类型、输入输出张量形状写到 json 文件里。这个 json 可以直接拖进浏览器 perfetto 或 chrome://tracing 里看,最直观地找出耗时大户到底是谁。
import onnxruntime as ort so = ort.SessionOptions() so.enable_profiling = True # 只在这台测试机开,生产务必关掉 sess = ort.InferenceSession( r"D:\models\resnet50_sim.onnx", sess_options=so, providers=["CPUExecutionProvider"], ) # 跑若干次推理,把 profiling 写入文件 for _ in range(20): sess.run(None, {sess.get_inputs()[0].name: fake_input}) prof_file = sess.end_profiling() # 返回生成的 json 路径 print(prof_file)end_profiling一旦调用,会话的 profiling 自动关闭。json 里 Convention 字段标注了算子耗时,我通常先按 total_time 降序排前 5 个算子,再回模型图上确认这些算子是否串联在主干路径。如果排在前面的是常见的 Reshape、Transpose 这类搬运算子,优先在导出阶段把它干掉,比调线程数收益大得多。
实测验证的正确姿势是固定同一份输入数据,在同一个模型上对比两个版本或两套参数,各跑 100 次,统计 P50 和 P95 耗时,而不是只看第一次的耗时。第一次调用会有图优化、内存分配等冷启动开销,混在一起会得出误导性的“新版更慢”结论。我自己的习惯是 warmup 20 次后再正式计时,数据才有参考价值。
onnxruntime 的 value_info 也是排查利器:session.get_inputs()[0].shape如果返回 None,说明这个维度的 shape 是动态的,运行时才会解析,这类节点往往也是 profiling 里的耗时怪。把 profiling json 和 shape 信息结合起来看,比在代码里加计时点定位准得多。
这套验证流程帮我挡过不止一次“升级却倒退”的翻车事故。希望帮到你。
本文还有配套的精品资源,点击获取