尧图精选

三种ONNX转TensorRT方法对比:trtexec、OnnxParser与Polygraphy实战

🕒 发布时间:2026/10/1 16:30:49 📁 来源:尧图网络
做模型部署的几乎没有谁没跟 ONNX 和 TensorRT 打过交道。训练好的模型不管是 PyTorch、Paddle 还是其他框架导出的通常会先统一成 ONNX 这个中间格式再落到不同的推理后端上。而如果你手头正好是 NVIDIA 的显卡想要把推理性能压榨到极限那 ONNX 转 TensorRT 就是绕不开的一步。这篇文章把我实际用过、也在多个项目里验证过的三种 ONNX 转 TensorRT 的方式一次性讲清楚trtexec 命令行、TensorRT Python API 的 OnnxParser、Polygraphy 辅助工具链。最终目标只有一个——在 Python 里把 engine 稳稳跑起来顺利出结果。适合刚接触 TensorRT 的部署新人也适合已经会用 trtexec、但想在 Python 里做动态 batch 和 INT8 量化控制的老手。先说我的结论这三种方式不是三选一的关系而是不同阶段、不同场景下的组合。trtexec 适合批量出 engine、快速拿性能基线Python API 适合把转换逻辑写进服务端脚本一键完成从 onnx 到 engine 再到推理Polygraphy 更像 NVIDIA 给你配的“调试放大镜”转换之外还能做精度比对和性能剖析。下面直接进入正题。1. 为什么大家都在折腾 ONNX 转 TensorRT1.1 ONNX 只是“中间格式”不是终点ONNXOpen Neural Network Exchange的设计初衷是做一个“模型界的通用语言”训练框架导出 ONNX推理框架读入 ONNX。这个设计很聪明但说到底它只是把模型描述成了一张计算图并没有规定这张图到底怎么被执行。同一个 ONNX交给 ONNX Runtime 跑交给 OpenVINO 跑交给 TensorRT 跑得到的性能和能力完全不同。打个比方ONNX 相当于一份通用的施工图纸图纸上画好了每一道工序算子但真正施工时用哪支施工队、用几台设备、怎么安排流水线图纸管不着。TensorRT 就是 NVIDIA 平台上那支最了解 GPU 的施工队它可以根据图纸重新安排施工顺序、合并工序、挑选最顺手的工具甚至用更省料的工艺FP16/INT8把活干完。所以在实际项目里训练完 PyTorch/Paddle 模型后最常见的链路是导出 ONNX → 验证 ONNX 结果 → 转 TensorRT engine → 在 Python 或 C 里推理。我做 YOLO 检测、OCR 识别像 PP-OCR、人像抠图rmbg 这类模型的部署时全是这条路只是中间“转”这一步用的工具不太一样。上面热搜里的“onnx runtime / ncnn”“onnx转rknn int8”这些词其实也都是围绕同一条主线的不同分支。1.2 TensorRT 到底帮你做了什么很多人以为 TensorRT 只是“把模型读进去再存出来”其实它最少干了四件事图优化Layer Fusion把 Conv BN ReLU 这类连续算子合并成一个算子减少 kernel 启动次数和中间显存读写。一个 100 层的网络融合之后实际执行的层数可能只剩几十层。Kernel 自动调优Auto-tuning同一个 Conv在不同 GPU 上可能有几十种 CUDA kernel 实现不同的 tile 切分、不同的 shared memory 使用策略构建引擎时它会跑一遍性能测试选出当前 GPU 上最快的那一版。数值精度优化把模型从 FP32 降到 FP16 或 INT8。带宽减半甚至减到四分之一很多模型的 FP16 推理能比 FP32 快一倍以上INT8 还能更夸张。显存与执行优化提前规划好每一层的输入输出 buffer重复利用显存减少运行时分配带来的碎片和开销。这也是为什么 TensorRT 的 engine 文件跟 GPU 架构、CUDA 版本、TensorRT 版本强绑定它在构建时就把“施工方案”针对目标 GPU 定死了。换个显卡看起来只是换张图实际连 kernel 都要重新选一遍。1.3 三种转换方式的选型思路既然都能转为什么还要分三种因为使用场景不一样。方式典型场景上手难度可控性trtexec 命令行批量出 engine、CI 自动化出包、快速拿性能基线低中靠参数Python OnnxParser把转换集成进 Python 服务脚本、需要动态 shape / 量化控制中高Polygraphy 工具链精度比对、多后端对比、排错、性能剖析中高高CLI Python API自己一个人写 demo怎么快怎么来一般 trtexec 就够了。但一旦进入工程化你会发现转换逻辑需要跟训练脚本、评测脚本串起来这时候 OnnxParser 的 Python 方案更合适。Polygraphy 则是在“转出来的 engine 结果对不对、快没快”这种灵魂拷问面前帮你把答案量化出来的工具。看完下面三节的完整演示你基本就知道自己的项目该用哪个了。2. 动手前的环境准备版本对齐比什么都重要2.1 GPU 驱动 / CUDA / cuDNN / TensorRT 版本匹配转 TensorRT 前先别急着写代码先把环境对齐否则后面报的错会让你怀疑人生。TensorRT 的依赖链是GPU 驱动 → CUDA → cuDNN → TensorRT → Python 环境。每一层都要匹配尤其要注意两个点。第一驱动版本决定你能用哪一版 CUDA。驱动新不代表 CUDA 工具包也新但驱动不够新时新版 CUDA 根本起不来。第二TensorRT 官方文档里会给出每个版本支持的 CUDA/cuDNN 组合比如 TensorRT 10.x 通常要求某个 CUDA 大版本区间具体组合要对着官方表看。如果你在新出的 RTX 50 系显卡上做部署这类新卡往往要求较新的 CUDA 版本比如 12.8 及以上和对应的最新 TensorRT 版本直接用 TensorRT 10.x 会比较稳。我自己踩过最狠的一次坑在一台机器上构建好的 engine拷到另一台同型号 GPU 的机器上反序列化直接失败。排查半天才发现是两台机器的驱动、CUDA 版本不一致。所以开工前先跑一遍版本检查nvidia-smi nvcc --version python -c import tensorrt as trt; print(trt.__version__)注意 tensorrt 的 Python 包版本要跟系统里实际安装的 TensorRT 库一致否则会出现libnvinfer.so找不到或者版本对不上的报错。2.2 Python 环境与 tensorrt 包安装环境这块正常情况下以下命令能解决大部分问题pip install tensorrt pip install onnx pip install pycuda如果网络慢就把 pip 源切成你习惯的国内镜像能省不少时间。这里有三点要特别说明tensorrt 这个包在不同版本下的安装方式不太一样。有些版本直接pip install tensorrt拿到的就是完整运行库外加 Python 包有些版本需要先从官方下载适配包解压后再把 Python 包装入虚拟环境。具体以你安装版本的官方说明为准。pycuda 是让 Python 能完成 GPU 显存分配和数据拷贝的桥梁。TensorRT 只负责推理数据从 CPU 到 GPU、再从 GPU 取回来这步需要 PyCUDA 或者 Numba 之类的 CUDA 工具配合。Windows 上做 TensorRT 部署pip 支持和运行时行为都不如 Linux 顺手很多坑都出在 DLL 路径和 CMake 依赖上。能用 Linux 容器做部署验证会省很多事。装完之后顺手验证import tensorrt as trt import onnx import pycuda.autoinit print(tensorrt, trt.__version__) print(onnx, onnx.__version__)三行都能过环境基本就 OK 了。2.3 模型导出 ONNX 时的两个坑opset 和动态维度环境准备好之后手头的模型得先导出成 ONNX。这一步虽然看起来跟 TensorRT 无关但实际决定后面转换顺不顺有两个坑最典型。第一个是算子集版本opset。PyTorch 导出时如果不指定 opset会自动用当前版本支持的最高值但 TensorRT 的 OnnxParser 对每个算子都有个支持范围opset 太高它可能不认某些新算子opset 太低有些算子组合又没法表达。我的经验是导出时显式指定 opset比如 17 或 19在 TensorRT 10.x 上兼容性都不错。import torch torch.onnx.export( model, dummy_input, model.onnx, input_names[input], output_names[output], dynamic_axes{ input: {0: batch, 2: height, 3: width}, output: {0: batch} }, opset_version17, )第二个是动态维度。大多数部署场景不会接受固定尺寸OCR、检测、抠图这类任务的输入尺寸更是常变。所以在导出时就把dynamic_axes声明好后面转 TensorRT 时才能构建动态 shape 的 engine。如果导出时把维度写死了后面想改成动态就得重新导出来回折腾很浪费时间。像 YOLO 类模型的输入经常是 640x640但 batch 数可能从 1 到 16 波动这种就非常适合一开始就把 batch 维标成动态。3. 方式一trtexec 离线转换Python 加载 engine 推理3.1 trtexec 命令生成 enginetrtexec 是 TensorRT 自带的可执行文件装完 TensorRT 以后在安装目录的 bin 下面能找到。它的作用有两个一是把 ONNX 转成 engine二是对生成好的 engine 做性能测试。最基础的转换命令是trtexec --onnxmodel.onnx --saveEnginemodel.engine不带精度参数时默认构建 FP32 engine。想用 FP16 就加一个--fp16trtexec --onnxmodel.onnx --saveEnginemodel_fp16.engine --fp16如果模型输入是动态 shape导出时声明过 dynamic_axes那必须显式给出范围否则 trtexec 会直接报错或者只按某个默认尺寸构建trtexec --onnxmodel.onnx \ --minShapesinput:1x3x640x640 \ --optShapesinput:8x3x640x640 \ --maxShapesinput:16x3x640x640 \ --saveEnginemodel_dynamic.engine \ --fp16这里input必须跟 ONNX 里的输入名完全一致尺寸格式是 NCHW维度之间用x分隔。trtexec 跑完以后还会自动做一组性能测试打印平均延迟、吞吐量等指标这些数据在验证加速效果时直接能用等于白送一个 benchmark。3.2 常用参数说明别只知道 --fp16我长期用 trtexec 之后真正高频的参数其实就下面几个参数作用补充说明--onnx指定输入 ONNX 文件路径里别带中文和空格--saveEngine保存生成的 engine不指定的话只做性能测试不落盘--fp16/--int8开启低精度构建int8 需要校准数据或校准缓存--minShapes/--optShapes/--maxShapes动态 shape 范围三者缺一不可--memPoolSize设置显存池大小TRT 8.5 以上写法老版本是--maxWorkspaceSize--verbose输出详细日志报错时用来定位具体网络层--buildOnly只构建不运行性能测试适合批量出 engine 时用--plugins加载自定义插件 so 文件用到自定义算子的模型必加这里提醒一句--int8不是帮你自动量化而是让构建器以 INT8 精度为目标去选择 kernel同时要求你提供校准数据。如果模型没有适合 INT8 的层或者校准数据没准备好构建会失败。新手建议先跑通 FP16再碰 INT8。3.3 Python 端加载 engine 并完成推理trtexec 只负责把 engine 造出来真正跑到业务里还是在 Python 中加载。Python 加载 engine 的流程比较固定核心是四步反序列化 engine、创建执行上下文、分配显存并拷贝输入、执行并取回输出。import tensorrt as trt import numpy as np import pycuda.driver as cuda import pycuda.autoinit TRT_LOGGER trt.Logger(trt.Logger.WARNING) def load_engine(engine_path): with open(engine_path, rb) as f: runtime trt.Runtime(TRT_LOGGER) return runtime.deserialize_cuda_engine(f.read()) def infer_engine(engine, feed_dict): context engine.create_execution_context() names list(engine) bindings [] host_buffers {} for name in names: mode engine.get_tensor_mode(name) if mode trt.TensorIOMode.INPUT: data feed_dict[name].astype(np.float32) context.set_input_shape(name, data.shape) device_ptr cuda.mem_alloc(data.nbytes) cuda.memcpy_htod(device_ptr, data.ravel()) bindings.append(int(device_ptr)) else: shape tuple(context.get_tensor_shape(name)) out np.empty(shape, dtypenp.float32) device_ptr cuda.mem_alloc(out.nbytes) bindings.append(int(device_ptr)) host_buffers[name] (device_ptr, out) context.execute_v2(bindings) results {} for name, (device_ptr, out) in host_buffers.items(): cuda.memcpy_dtoh(out, device_ptr) results[name] out return results这段代码有两个关键细节。第一bindings列表的顺序必须跟list(engine)的 binding 顺序一致我这里是按 engine 迭代顺序 push 的所以不会错。第二动态 shape 的输入必须在执行前调用context.set_input_shape而且输出 buffer 的大小也要在设置 shape 之后再取否则拿到的可能是 0 或默认值。如果你用的是 TensorRT 10 的较新版本官方新接口是execute_async_v3(stream, tensor_addresses)可以直接传{输入名: 指针}字典不用再手工维护 binding 顺序老接口execute_v2也依然能用不用急着改。4. 方式二纯 Python 用 OnnxParser 直接构建引擎4.1 核心 API 链路与每个组件的作用第二种方式不经过 trtexec而是在 Python 里直接构建 engine。核心链路是Builder → Network → OnnxParser → BuilderConfig → serialized Engine。我解释一下每一步的含义理解了这条链后面调参就有方向了。Builder引擎构建器的总指挥负责统筹整个构建过程。NetworkTensorRT 内部的网络定义相当于一张还没优化的计算图。OnnxParser把 ONNX 文件解析进来转换成 TensorRT 的 Network 结构。BuilderConfig构建配置包括精度、显存池、动态 shape 范围等。最后 Builder 根据 Network 和 Config 产出序列化后的 engine 字节流。这条链路的优势是转换过程完全可控。你可以在构建前检查网络的输入输出名可以针对某个输入单独配置优化 profile可以在代码里根据模型类别决定开不开 FP16甚至可以直接修改网络层。这是 trtexec 给不了的灵活度。4.2 完整代码onnx 到 engine 一条龙下面这段代码我基本是复制到各个项目里当公共函数用的可以直接抄import tensorrt as trt TRT_LOGGER trt.Logger(trt.Logger.WARNING) EXPLICIT_BATCH 1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) def build_engine_from_onnx(onnx_path, engine_path, fp16False, workspace_size1 30, dynamic_shapesNone): builder trt.Builder(TRT_LOGGER) network builder.create_network(EXPLICIT_BATCH) parser trt.OnnxParser(network, TRT_LOGGER) with open(onnx_path, rb) as f: if not parser.parse(f.read()): print(解析 ONNX 失败错误信息) for i in range(parser.num_errors): print(parser.get_error(i)) return False config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, workspace_size) if fp16 and builder.platform_has_fast_fp16: config.set_flag(trt.BuilderFlag.FP16) if dynamic_shapes: profile builder.create_optimization_profile() for name, (min_shape, opt_shape, max_shape) in dynamic_shapes.items(): profile.set_shape(name, min_shape, opt_shape, max_shape) config.add_optimization_profile(profile) serialized builder.build_serialized_network(network, config) if serialized is None: print(构建 engine 失败) return False with open(engine_path, wb) as f: f.write(serialized) print(engine 已保存到, engine_path) return True调用示例build_engine_from_onnx( yolo12.onnx, yolo12_fp16.engine, fp16True, dynamic_shapes{ input: ([1, 3, 640, 640], [8, 3, 640, 640], [16, 3, 640, 640]) } )有几个点说明一下。EXPLICIT_BATCH这个 flag 在 TRT 8/9/10 里都兼容导出 ONNX 时如果用 opset 16默认就是显式 batch 模式这里保持一致。builder.platform_has_fast_fp16判断当前 GPU 是否支持 FP16 加速不支持的话就算定了 FP16 flag构建时也可能失败或退化为 FP32加这层判断在边缘设备上更安全。TRT 8.x 里旧接口是builder.build_engine(network, config)返回trt.HostMemory跟上面代码略有差异TRT 10.x 推荐用build_serialized_network我这套代码是基于 TRT 10.x 写的。4.3 动态 Shape 的配置细节动态 shape 是 Python 方式最值得掌握的部分。优化 profile 里有三个值min、opt、max。它们的含义是min 是允许的最小输入尺寸运行时不能小于它opt 是优化目标尺寸TensorRT 的 kernel 选择会优先照顾这个尺寸max 是允许的最大输入尺寸运行时不能大于它。选值的时候有个关键经验opt 一定要设成线上最常用的尺寸而不是取 min 和 max 的中间值。比如检测服务平均每张请求都是 640x640那 opt 就设成 8x3x640x640假设 batch 8哪怕 min 是 1、max 是 16。因为 TensorRT 的性能优化以 opt 为基准opt 设错了engine 在真实业务尺寸上可能不是最优实现跑出来的速度还不如不换 TRT。运行时动态 shape 的用法跟第 3 节一样必须在执行前调用context.set_input_shape。如果网络有多个动态输入比如两路视频流的模型每个输入都要单独设置 shape。用 profile 构建时还可以通过set_optimization_profile_async在运行时切换 profile不过这个场景比较少见一般业务一个 profile 就够。5. 方式三Polygraphy 和它的兄弟们5.1 用 Polygraphy 转 engine 并做精度比对Polygraphy 是 NVIDIA 官方维护的一个工具包定位是“帮助开发者解析、比较、调试和转换模型”。它包装了 TensorRT、ONNX Runtime 等多个后端所以既能转换也能做精度比对和性能压测。安装很简单pip install polygraphy转换命令跟 trtexec 非常像polygraphy convert model.onnx --output model.engine --trt --fp16动态 shape 加三个开关polygraphy convert model.onnx --output model.engine --trt --fp16 \ --trt-min-shapes input:[1,3,640,640] \ --trt-opt-shapes input:[8,3,640,640] \ --trt-max-shapes input:[16,3,640,640]更实用的是它的run命令可以直接拿同一份 ONNX 在两个后端上跑对比输出差异。比如我想验证 FP16 engine 和原始模型在数值上差多少一条命令就能出结果polygraphy run model.onnx --trt --onnxrt --fp16 \ --trt-min-shapes input:[1,3,640,640] \ --trt-opt-shapes input:[8,3,640,640] \ --trt-max-shapes input:[16,3,640,640] \ --atol 1e-3 --rtol 1e-3--onnxrt表示用 ONNX Runtime 作为参照后端--atol和--rtol是允许的绝对误差和相对误差。输出里会明确告诉你哪里 mismatch、数值差多少。我在排查 FP16 精度下降问题时全靠这一招定位到出问题的层。跟 Polygraphy 经常搭配的还有 onnx-graphsurgeon它可以对 ONNX 图做精细裁剪和修改比如把某些 TensorRT 不支持的子图替换成自定义算子属于进阶玩法。5.2 顺带聊聊 torch2trt 和 ORT-TRT除了这三种方式还有两个方案经常被拿来比较我觉得值得说清楚。torch2trt第三方开源项目模型还在 PyTorch 里时可以直接转成 TensorRT engine省掉导出 ONNX 的环节。对快速验证很友好但项目维护节奏不稳定实现跟官方 TensorRT 的版本绑定比较紧版本一升级就可能出问题生产环境我基本不用。ONNX Runtime TensorRT EP严格来说不是“转换”而是在 ONNX Runtime 推理时把计算图交给 TensorRT 执行。好处是不用管 engine 文件、不用改推理代码适合快速引入坏处是图切分和调度有额外开销控制力也不如直接使用 TensorRT性能上限通常比显式构建 engine 低一些。用起来就是指定一下 providerimport onnxruntime as ort sess ort.InferenceSession( model.onnx, providers[TensorrtExecutionProvider, CUDAExecutionProvider] )我的实际分工是日常开发用 Polygraphy 做验证和精度比对线上服务用 trtexec 或 Python 方式构建好的 engineORT-TRT 只出现在一些不太在意极致性能、只想快速上线 ONNX 模型的场景里。5.3 三种方式的优缺点对比表格说话对比维度trtexecPython OnnxParserPolygraphy转换入口命令行一行Python 代码命令行 / API动态 shape命令行参数Python 配置命令行参数集成进业务脚本需要 subprocess 调命令直接 import可调用 API精度比对不支持要自己写内置 run 对比性能剖析内置测试要自己打点内置插件支持参数指定 soPython 加载支持新手上手最容易中等中等偏上如果你只打算记住一句话临时验证用 trtexec工程化封装用 Python OnnxParser精度出问题或要做多后端验证时上 Polygraphy。6. 常见问题与排查技巧实录6.1 高频报错与解决办法速查表报错或现象常见原因解决办法Unsupported operator/ 解析失败ONNX opset 过高或算子不在支持范围降低 opset 重新导出检查是否有 TRT 不支持的算子构建失败日志提示No implementation该层在当前精度 / 架构下没有可用 kernel关闭该层低精度尝试只开 FP16 不开 INT8升级 TRTdeserialize_cuda_engine报错engine 与当前 GPU / CUDA / TRT 版本不匹配在目标机器上重新构建set_input_shape失败输入 shape 超出 profile 范围确认 min/opt/max 覆盖实际使用尺寸输出 buffer 尺寸为 0动态 shape 下未先 set_input_shape 就取 shape先设 shape 再分配输出 buffer运行时报显存不足workspace 过大或 batch 过大调小--memPoolSize/workspace_size减小 batchINT8 构建报校准相关错误没提供校准数据准备校准集或提供已有 calibration cache加载插件 so 失败插件路径不对或与 TRT 版本不符用--plugins指定正确路径确保插件版本匹配6.2 精度不对、性能没提上去怎么办两个最常被问的问题统一说下排查思路。第一个是精度问题。FP16 后结果跟原始模型差很多大多数时候不是框架的错而是模型对低精度太敏感。先别急着骂 TensorRT用 Polygraphy 的 run 对比一下 ONNX Runtime 和 TensorRT 的输出找出差异最大的输出节点再用--layer-precision之类的参数把敏感层强制保留 FP32。如果连 ONNX Runtime 和 TensorRT 的 FP32 结果都对不上那要先怀疑 ONNX 导出时某些算子语义变了。第二个是性能没提上去。我见过很多次FP16 跑出来的速度跟 FP32 几乎一样甚至更慢。排查方向先看输入尺寸是不是每次都在变动态 shape 下 engine 可能每次都选不是为当前尺寸调优的 kernel再看是不是把第一个 batch 的冷启动延迟算进了耗时实际部署要加 warmup跑几十次再统计最后确认是否真的走了 GPU有的人装的环境里 TensorRT 一直跑在 CPU 回退路径上这种日志里通常会有 warning很容易漏看。6.3 我给新手的几条实操建议最后给几条我摔过跟头换来的建议永远从 FP32 开始。先用最简单的方式把流程跑通确认能出正确结果再逐步开 FP16、INT8。每步都保留 baseline出问题才有对比。每次构建 engine 都把版本信息打出来。日志里记下 TensorRT 版本、CUDA 版本、GPU 型号、输入 shape 范围排查反序列化失败时会省下大量时间。把转换函数封装成公共模块。像第 4 节那样的build_engine_from_onnx固定好输入参数格式所有模型共用减少复制粘贴带来的低级错误。动态 shape 的 opt 值就设线上真实尺寸别偷懒随便填。别为了精度问题直接放弃 FP16先定位敏感层再逐层回退 FP32往往性能和精度都能兼顾。我在实际项目里最深的体会是TensorRT 的学习曲线不在 API而在“版本和硬件”这两条暗线上。API 五分钟就能学会但版本不匹配、GPU 架构不同带来的问题能让人排查几天。所以我现在每到一个新环境第一件事就是统一版本清单再谈转换。这篇文章里写的三种方式我都分别上过生产trtexec 在 CI 里批量出包Python OnnxParser 在服务端做动态构建Polygraphy 在每次改精度配置后做回归校验分工明确互不冲突。最后再分享一个小技巧把构建好的 engine 连同输入输出信息名称、类型、shape一起写个小清单存下来下次写推理代码时直接查不用再去翻原始模型。很多“代码写一半发现 tensor name 忘了”的尴尬都能避免。如果你也在折腾 ONNX 转 TensorRT希望这篇能让你少踩几个坑。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →