模型推理加速:TensorRT与ONNX Runtime选型、转换与排坑 📅 发布时间:2026/9/18 11:26:40 👁 浏览次数: 训练一个模型只是前半场真正让模型落地产生价值的后半场是推理。我刚接触部署那阵子也吃过不少亏模型在 PyTorch 里跑得飞起一上生产环境就延迟暴涨、显存失控换成 TensorRT 又发现导出的 ONNX 一堆报警算子不支持、动态维度对不上、量化后精度崩掉。踩过几轮坑之后我慢慢摸出了一套比较稳的套路先用 ONNX Runtime 保证链路正确性再根据硬件条件和延迟要求决定是否用 TensorRT 做进一步加速。今天这篇就把 TensorRT 与 ONNX Runtime 的选型、转换、调参、排坑一次讲透适合刚接触推理部署的算法工程师也适合要在 C、Java、边缘设备上落地模型的后端同学。1. 两个引擎的差异不只是“快一点”1.1 TensorRT为单一硬件做的“特调”TensorRT 是 NVIDIA 推出的推理加速器它最核心的思路是“针对当前这块 GPU 深度特调”。你给它一个训练好的模型它会做图优化、层融合、精度校准、kernel 自动选择甚至把多个算子合并成一个更高效的 CUDA 核最后编译成 engine 文件。这个 engine 跟显卡架构强相关换一张卡、换一个驱动版本很可能要重新构建。我最早用 TensorRT 时对“层融合”没什么概念以为它就是简单的算子替换。后来用 Nsight 看了一遍前向图才明白很多小算子会被合并比如 Conv BN ReLU 变成一条融合算子卷积的 im2col 也被专门优化。这种特调带来的收益非常直接我的一个 YOLO 检测模型在 FP16 下延迟降到了 PyTorch 的 1/3 左右显存占用也小了一大截。不过特调的代价是“挑剔”。TensorRT 对算子支持有边界导出 ONNX 用的算子版本太新、包含某些自定义 op或者动态 shape 设计不合理都可能 build 失败。它也不是越新越好老的 engine 在新驱动上可能反序列化失败你必须维护版本矩阵的一致性。1.2 ONNX Runtime跨平台推理的通用底座ONNX Runtime 走的是另一条路尽量做“通用”。它由微软主导核心是把模型经过 graph optimization 和多个执行后端Execution ProviderEP分发到不同硬件上。CPU 用默认 EPGPU 可以用 CUDA EP也可以在 NVIDIA 卡上直接挂 TensorRT EP还能在 Windows 上走 DirectML、在 ARM 上走 NNAPI。ONNX Runtime 的最大价值在于“链路先行”。模型训练完torch.onnx.export 导出 ONNXONNX Runtime 就能把前向结果跑出来验证输出与 PyTorch 对齐。这个阶段不用纠结硬件加速先把输入输出、预处理、后处理全部跑通再去考虑性能优化。我很多项目的第一步其实都是 ONNX Runtime原因很朴素它开箱即用、跨平台绑定齐全Python、C、Java、C# 都有官方 API。团队里后端用 Java 的同学也能在不动模型文件的情况下直接接入推理接口模型运维成本低很多。对于批量离线任务、CPU 部署、以及对延迟没那么极端要求的服务ONNX Runtime 完全够用。1.3 选型决策查这张表基本就够了很多同学喜欢问“到底该用哪个”我的答案取决于场景。下面是一张我常给团队看的对比表基本覆盖了选型要权衡的核心维度。维度TensorRTONNX Runtime适用硬件NVIDIA GPUCPU、GPU、NPU、移动端加速方式图融合、低精度、kernel 特调执行后端调度、图优化模型格式engine / onnxonnx跨平台能力弱依赖 NVIDIA 环境强多平台多语言绑定部署复杂度高版本敏感低开箱即用动态 shape需配置 Optimization Profile支持较好延迟敏感场景非常适合可选 TensorRT EP 提升快速原型验证不推荐推荐很多项目其实不是“二选一”而是“两步走”。我自己的标准是先用 ONNX Runtime 把模型跑通再用 TensorRT 做性能压测如果延迟达标就不折腾第二套系统。如果必须上 TensorRT尽量把 ONNX 导出和 build engine 的流程脚本化避免手工点来点去。提示这里说的“TensorRT EP”是 ONNX Runtime 的一个执行后端你可以只写 ONNX 模型让 ONNX Runtime 在底层调用 TensorRT 做加速。好处是上层代码兼容坏处是能控制的参数没有直接用 TensorRT 那么细。适合想先尝到 TensorRT 性能、又不想重构链路的人。2. 模型转换全流程从训练权重到可用引擎2.1 ONNX 导出的几个容易踩的细节把 PyTorch 模型导出为 ONNX看起来是跑一行 torch.onnx.export实际上有几个细节没注意后面部署全是坑。第一个是动态维度。图像模型的 batch 维度往往需要动态检测模型还可能输出动态数量目标TensorRT 又要求预先定义 min/opt/max 三个档位。所以导出时我会把 dynamic_axes 固定好比如 batch 维度设为 dynamic如果输出维度也随输入变化必须把对应轴也标出来。第二个是算子版本。ONNX opset 版本不要设太低否则某些算子被强制拆散比如torch.where在低版本下可能输出一堆 Cast、Greater 子图TensorRT 优化起来很吃力。一般来说 opset 设 11 以上遇到较新的模型我会直接上 opset 19ONNX Runtime 和 TensorRT 现在都支持得不错。第三个是简化问题。PyTorch 导出有时会带出很多冗余节点比如不必要的 Reshape、Transpose。建议用onnx-simplifier清理一遍pip install onnxsim后执行python -m onnxsim model.onnx model_sim.onnx对后续转换帮助很大。第四个是验证。导出后一定要用 onnxruntime 跑一遍结果和 PyTorch 输出做数值对比误差超过可接受范围就要检查预处理和输入输出格式。不要等部署到生产环境才发现精度不对那时候定位问题非常痛苦。import torch import torch.onnx model torch.load(model.pt, map_locationcpu) model.eval() dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, model.onnx, opset_version17, input_names[images], output_names[outputs], dynamic_axes{ images: {0: batch}, outputs: {0: batch}, }, )这段代码基本是我导出的固定模板。dynamic_axes把 batch 设为动态输入尺寸也一并声明之后在 TensorRT 里配置 profile 时就不用重新导出了。2.2 先从 ONNX Runtime 跑通再谈优化拿到简化后的 ONNX我的第一件事永远是写一段最小推理脚本验证链路。这一步能帮你确认模型文件是否完整、输入输出是否正确、预处理与训练时是否完全一致。import onnxruntime as ort import numpy as np sess ort.InferenceSession(model_sim.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider]) input_name sess.get_inputs()[0].name input_shape sess.get_inputs()[0].shape x np.random.randn(1, 3, 640, 640).astype(np.float32) out sess.run(None, {input_name: x})[0] print(out.shape, out[0, :5])这里值得注意的一点是providers的顺序。ONNX Runtime 会按优先级选择可用的 EP如果CUDAExecutionProvider没装上它才会 fallback 到 CPU。所以我把 CUDA 放前面同时保留 CPU 作为兜底。用 CPU 跑一次、再用 CUDA 跑一次对比两次输出能快速判断是不是 GPU 后端引入了数值误差。跑通之后再去看性能。如果 CPU 部署的延迟已经满足需求就直接进入服务化如果不行再考虑 TensorRT 或者优化前后处理。这种“先正确、后优化”的顺序帮我挡掉了大量无效排查。2.3 TensorRT 转换trtexec 与 API 两种路线TensorRT 转换模型有两种主流方式一种是官方命令行工具trtexec一种是 C/Python API。trtexec 负责快速验证和生成 engine适合脚本化API 方式则是把它嵌入到自己的部署程序里做到“拿到 ONNX 就地构建 engine”。先说 trtexec 的常用命令。假设你已经安装了 TensorRT把trtexec所在目录加进 PATH然后执行trtexec --onnxmodel_sim.onnx \ --saveEnginemodel.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:8x3x640x640 \ --maxShapesimages:16x3x640x640--fp16开启半精度通常能带来明显加速且精度损失很小。--minShapes/optShapes/maxShapes对应动态 shape 的三个档位必须覆盖你生产环境实际会出现的输入尺寸。--saveEngine保存构建后的 engine下次部署直接 load不用重新 build省下大量时间。API 路线也值得掌握因为工程里经常要在运行时判断有没有现成 engine没有才现场 build。C 里大致流程是创建 builder、构建 config、填充配置、解析 ONNX、buildSerializedNetwork、write 到文件。Python 里则简单一些核心代码类似下面这样import tensorrt as trt logger trt.Logger(trt.Logger.WARNING) builder trt.Builder(logger) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, logger) with open(model_sim.onnx, rb) as f: parser.parse(f.read()) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) config.set_flag(trt.BuilderFlag.FP16) serialized builder.build_serialized_network(network, config) with open(model.engine, wb) as f: f.write(serialized)这里的set_memory_pool_limit是设置构建时的工作空间上限单位是字节我一般会先给 1GB如果 build OOM 再调大。FP16 标志开启后TensorRT 会在精度允许范围内把支持的计算降到半精度。注意TensorRT 版本和 GPU 架构匹配很重要。RTX 50 系是 Blackwel l 架构计算能力是 sm_120旧版 TensorRT 不一定能正确 build。我迁移到 50 系卡时直接用 TensorRT 10.8 以上版本旧版本在 build 阶段会直接报 “unsupported compute capability”。如果你们生产环境还有老卡最好给不同架构分别保存 engine不要想靠一个 engine 到处跑。2.4 Java 调用 ONNX Runtimermbg-2.0 抠图实战很多后端场景避不开 Java。之前有人问我 Java 能不能跑 ONNX我拿 rmbg-2.0 人物抠图做过一次实际部署结论是可以而且没有想象中麻烦。rmbg-2.0 是 U2Net 风格的人像抠图模型输入是 3×1024×1024 的 RGB 图像输出是高 1 通道的 alpha matte。用 Java 调用时关键是三件事加载 native 库、把 BufferedImage 转成模型输入、把输出转成透明 PNG。import ai.onnxruntime.*; OrtEnvironment env OrtEnvironment.getEnvironment(); OrtSession session env.createSession(rmbg2.onnx, new OrtSession.SessionOptions()); // 图像预处理缩放、归一化、HWC - CHW、转 float float[] input preprocess(image, 1024, 1024); OnnxTensor tensor OnnxTensor.createTensor(env, input, new long[]{1L, 3L, 1024L, 1024L}); OrtSession.Result result session.run(Collections.singletonMap(input, tensor)); float[][][] alpha ((OnnxTensor) result.get(0)).getFloatValue(); // [1, 1024, 1024]Java API 里最容易踩的问题是内存释放。每次run产生的OnnxTensor都会占用 native 内存如果不执行tensor.close()、result.close()长期跑下来会被 OOM 砸脸。我处理高并发抠图时是先算 alpha matte再在 Java 侧叠加到原始图像然后把输出 image 交给下游整个过程保持 native 对象生命周期最短。另外注意预处理细节图像要先转成 RGB再缩放再做 mean/std 归一化顺序错了抠图效果会明显变差。rmbg-2.0 不同版本输出可能带 sigmoid 也可能不带一定要在推理后做 softmax 或直接 clamp不能默认网络已经帮你处理好了。3. 推理参数与性能调优实战里最重要的东西3.1 conf 和 NMS 参数训练时和推理时不是一回事经常有人问“模型训练出来之后推理用的 conf 参数到底是什么”。这个 conf 是 confidence score表示检测框内存在目标的可信度。推理时模型会输出大量候选框每个框带一个类别概率conf 阈值就是用来做初筛的低于阈值的框直接丢掉。YOLO 类模型里conf 一般取 0.25 到 0.5 之间。我通常先设 0.25 看召回率如果误检太多就往 0.4、0.5 调。注意它和训练时的 loss 里那个 conf head 是两码事训练时学习的是目标是否存在推理时这个阈值只是工程上的取舍参数。和 conf 配套的是 NMS 的 IoU 阈值。NMS 会把重叠严重的框合并IoU 阈值越高保留的框越多目标密集场景下容易重复框阈值越低抑制越强但会误删相邻目标。我用检测模型做密集人群计数时会单独调低 IoU 到 0.35 以下避免两个人贴太近只出一个框。除了这两个参数还有max_det这类上限参数。生产环境里必须限制最大输出框数否则极端场景下输出数组会异常膨胀后处理直接被打满。像 YOLOv12 的 C 部署里TensorRT 的 output binding 大小就得按最大可能框数来分配小了会截断大了浪费显存这个后面还会再讲。3.2 dynamic shape、workspace 与显存策略TensorRT 的动态 shape 不是完全“动态”它需要在构建时给出 min、opt、max 三个档位。运行时如果输入 shape 落在三档之间TensorRT 会用 opt 对应的 kernel 去做 padding 或优化所以 opt 值要贴近真实负载分布不要随意拍脑袋。我一般根据生产数据分布来统计比如图片服务里 90% 的请求是 640×640只有少数是 1280×1280那 opt 就设 640×640min 设 1×640×640max 设 16×1280×1280。这样的话小 batch 请求能走优化好的 kernel大 batch 也能兜底。workspace 大小影响的是算子融合的空间上限。给大了TensorRT 能用显存换时间选择更激进的融合策略给小了部分优化被打回原形。我建议用set_memory_pool_limit设置一个合理上限但不要无限给避免多路并发时显存爆炸。推理时显存管理的核心原则是“复用”。每次前向分配的输入输出 buffer 应该提前分配好不要每帧 new 一个std::vector。C 部署里我用cudaMalloc一次性分配把指针传给enqueueV3跑完一轮再复用同一块显存。3.3 FP16、INT8 与低比特量化路线FP16 在很多模型上是“免费午餐”速度提升明显、精度几乎不掉。我默认对视觉模型开启 FP16一般检测精度下降在 0.5 个 mAP 以内。但如果你的模型对数值非常敏感比如某些超分模型、文字识别模型就要好好对比 FP16 和 FP32 的输出误差。INT8 的收益更大坑也更多。TensorRT 的 INT8 需要校准数据校准集通常是 100 到 500 张左右最好和实际业务分布一致。它背后的原理是统计激活值的分布然后决定每一层的量化范围。校准集如果只有几十张或者和线上数据差太远量化后精度很可能崩。我曾在车牌识别场景用公开数据集校准上线后发现识别率掉了一大截后来换成业务真实截图重新校准才恢复。低比特路线里现在还常看到 QBF16 这类写法本质是把权重进一步量化再用混合精度做反模糊优化。无论叫什么核心还是“量化范围怎么选、哪些层要回退到更高精度”这些在 TensorRT 的 per-channel 量化和层敏感度分析里都能找到影子。我的建议是先跑 FP16收益不够再上 INT8不要一上来就追求最小模型。3.4 性能测试要怎么做才靠谱性能测试如果乱做数字完全不可信。我见过不少同学用 Python 里纯 for 循环计时把 CPU 上的预处理时间也算进 GPU 推理时间最后得出的延迟结论毫无参考价值。正确的做法是分开测模型推理时间用 CUDA event 来测端到端时间才包含前后处理和 IO。CUDA event 能精确测量 GPU 时间示例代码如下cudaEvent_t start, stop; cudaEventCreate(start); cudaEventCreate(stop); // warmup 10 次让 TensorRT/ONNX Runtime 把 kernel 都加载完 for (int i 0; i 10; i) infer(); cudaEventRecord(start); for (int i 0; i 1000; i) infer(); cudaEventRecord(stop); cudaEventSynchronize(stop); float ms; cudaEventElapsedTime(ms, start, stop); printf(avg %.3f ms\n, ms / 1000);warmup 非常关键。TensorRT 第一次推理要加载 kernel、分配 buffer如果不 warmup 直接计时第一个数字会比平均慢好几倍。我实际项目里还会统计 P99 而不是只报均值因为生产环境最怕的是长尾延迟均值好看但 P99 高照样会拖垮调用方。4. 真实部署场景拆解从显卡到 MCU再到 LLM 推理服务4.1 YOLOv12 在 RTX 5070 上的 TensorRT C 部署YOLOv12 转到 TensorRT 推理流程本身不算复杂但细节不少。先把模型导出成 ONNX这里推荐把后处理拆掉只保留 backbone neck head 输出原始特征然后在 C 侧自己写解码和 NMS。好处是 TensorRT 只需要优化前向网络减少算子不兼容风险后处理逻辑也能完全掌控。用 trtexec 或 API 构建 engine 后在 C 端加载流程如下std::ifstream file(yolov12.engine, std::ios::binary); std::vectorchar data(std::istreambuf_iteratorchar(file), {}); auto runtime std::unique_ptrtrt::IRuntime(trt::createInferRuntime(logger)); auto engine std::unique_ptrtrt::ICudaEngine(runtime-deserializeCudaEngine(data.data(), data.size())); auto context std::unique_ptrtrt::IExecutionContext(engine-createExecutionContext());这里有个容易被忽略的核心点输出 buffer 的尺寸要通过engine-getTensorShape(name)查询得到再根据maxShapes下的最大值分配。YOLOv12 的输出通常是[1, num_anchors, num_classes 5]这种结构如果按 batch1 固定分配一旦输入尺寸超过预期内存越界会发生得非常诡异。推理调用我建议用 TensorRT 10.x 的enqueueV3它的参数是 tensor 名字加 void 指针比旧版executeV2更直观void* buffers[] {input_d_ptr, output_d_ptr}; context-setTensorAddress(images, input_d_ptr); context-setTensorAddress(outputs, output_d_ptr); context-enqueueV3(stream); cudaStreamSynchronize(stream);后处理阶段在 GPU 上解码、缩放然后拷回 CPU 做 NMS能省掉不少 PCIe 带宽。最终在 RTX 5070 上跑 FP16 的 YOLOv12实测比纯 ONNX Runtime 快了不少具体数字受图片分辨率影响很大但方向是明确的。5070 这类 50 系卡建议用 TensorRT 10.8 以上版本否则 engine 可能根本构建不出来。4.2 PP-OCRv6 的 ONNX 推理流水线OCR 类模型和检测模型不一样往往是多个模型串联。PP-OCRv6 这套流水线包含文本检测、方向分类、文本识别实际部署时我把它拆为三个 ONNX 模型再在推理层做编排。文本检测模型输出文本框坐标方向分类模型决定文本框是否需要旋转识别模型负责把裁剪后的区域转成字符串。ONNX Runtime 对这类多模型串联非常方便三个模型共享同一个环境每个模型各自创建 session并行度可以各自调整。这里最容易出问题的是预处理和后处理的形状不匹配。文本检测输出的是多边形顶点经过透视变换后才能送给识别模型如果某一环节把 batch 维理解错整个流程就会静默失败。我建议在每个模型输出处打印 shape 和数值范围串好后再删掉调试日志。pp-ocrv6 这类模型的 ONNX 导出要特别注意字符表路径和输入尺寸的对应关系否则识别结果会乱码。4.3 rp2350 这类 MCU根本跑不了 TensorRT怎么办有一个热词是 rp2350 AI 推理。rp2350 是树莓派 Pico2 的芯片属于微控制器内存和算力都极其有限连 ONNX Runtime 都很难直接跑起来更没有 NVIDIA 生态。那这类设备上做推理该走什么路线核心思路是把模型压缩到微控制器能接受的程度。一般流程是训练模型 - 导出 ONNX - 转为 TensorFlow Lite 格式 - 量化成 int8 - 用 TFLite Micro 或自行实现的 C 算子部署。很多嵌入式团队也直接用 onnx2tf 或者 X-CUBE-AI 这类工具链让工具自动生成 C 代码再烧录到芯片里去。这样做的好处是验证阶段依然可以用 ONNX Runtime 在 PC 上完成等确认特征对齐后才考虑端侧实现。别指望 rp2350 能跑检测大模型或者 Transformer现实一点它更适合跑几十 KB 大小的分类模型、唤醒词模型这类轻量任务。推理引擎不是万能钥匙选型先要认清硬件边界。4.4 sglang serve 与通用推理引擎的分工热词里还有 sglang serve这属于大模型推理服务方向。SGLang 这类框架专门为 LLM 设计核心是 continuous batching、RadixAttention 这些高级调度策略跟 TensorRT 的传统 CNN 优化路线不太一样。那 TensorRT 和 ONNX Runtime 在大模型时代还有没有位置有但分工明确了。视觉模型、非 Transformer 结构、融合模型部署主流还是 TensorRT/ONNX Runtime 这一套而 LLM 服务一般走 vLLM、SGLang 或 TensorRT-LLM 这种专用框架。如果你要部署一个 LLM API 服务别在 TensorRT 上硬搞直接用 sglang serve 启动指定模型更高效。不过底层思路是相通的预热、batch 调度、显存复用、低比特量化所有技巧在两边都能看到影子。把通用推理引擎的优化经验吃透再看 LLM 框架里的参数会容易理解得多。5. 常见故障与排查经验速查5.1 一张速查表解决 80% 的部署问题部署中大部分报错我用一张表就能快速定位。这里整理出我踩过的典型问题和排查方向直接照着查可以少走很多弯路。现象可能原因排查与做法build engine 报算子不支持ONNX 算子版本太新或太杂简化 ONNX升级 TensorRT 版本拆分后处理engine 反序列化失败TensorRT/GPU 架构不匹配确认版本矩阵按卡重新 build推理结果全 0 或乱码输入 buffer 没对齐 / 预处理错打印输入均值比对 PyTorch 预处理显存暴涨动态 shape 不合理检查 maxShapes设置显存池上限第一次推理特别慢未 warmup正式压测前 warmup 10 次以上多路并发偶发崩溃多个线程共享执行上下文每个线程独立 IExecutionContextONNX Runtime 输出和 PyTorch 差一点预处理或算子数值精度先比精度再查归一化参数Java 侧跑久了 OOMnative 对象未释放及时 close OnnxTensor/Result这张表我贴在工位上很久了后来整理成内部文档直接当部署手册用。很多问题的根因不是复杂原理而是版本不对、参数没传对、内存没管好这三件事。5.2 让推理链路可观测的几个习惯排查推理问题最怕的是链路不可观测。我会在推理解析过程中加日志比如记录输入 shape、模型输出 shape、前处理耗时、推理耗时、后处理耗时。上线初期这些日志全开稳定后再调成 DEBUG 级别避免噪音。另外推荐每次模型发布前做一次“输出一致性测试”固定一组测试图片分别用 PyTorch、ONNX Runtime CPU、ONNX Runtime GPU、TensorRT 跑一遍保存输出结果以后每次升级模型或引擎都要重跑。这个测试看似简单但在版本迭代中省了大量回归排查时间。C 侧调试时我习惯在关键节点打印cudaGetLastError()。GPU 异步执行时错误不一定在当期调用出现很多灾难性错误是异步触发如果不检查 CUDA error可能程序崩溃时根本定位不到真正出错的 kernel。5.3 结果保存的坑与正确的调试姿势热词里有“yolov11 保存推理结果”这里也提一下我的做法。保存推理结果有两类需求一类是可视化一类是结构化数据。可视化时别直接在原图上叠加太多信息我一般画框加 label 就够结构化数据则要明确坐标是归一化还是绝对像素类别 ID 和类别名的映射是否一致这些不一致会导致下游分析全部串线。视频流的推理结果保存还有一个容易被忽略的点保存帧率和写入帧率必须同步。如果你用 OpenCV 的 VideoWriterFPS 参数要和输入源一致否则保存下来的视频时长和实际时长对不上。我在做实时检测录像时还在写入端加了队列缓冲避免推理速度波动导致丢帧。调试时的正确姿势是先用一张固定图把模型输出 dump 下来对比各阶段数值确认后再跑真实数据流。很多“推理结果不对”的问题最后都归结为预处理和后处理没有和训练代码保持一致这一步先自查别急着怀疑模型坏了。说到底TensorRT 和 ONNX Runtime 只是工具真正的功力在于对模型结构、数据流和硬件特征的理解。我个人的体会是先把 ONNX Runtime 这条链路吃透保证每一步都可观测、可对比、可复现再在性能有瓶颈时引入 TensorRT把每个优化动作建立在基准测试之上。这样做即使模型迭代很快、环境变化频繁你的部署体系也不会轻易散架。