
聊到推理引擎绕不开两个名字TensorRT 和 ONNX Runtime。我在做模型部署的这五年里几乎每个项目第一轮技术选型都会把这两个引擎摆在一起比一遍。它们不是“二选一”的关系更多是各管一段ONNX Runtime 负责通用和生态TensorRT 负责把 NVIDIA GPU 的算力压榨到极致。这篇文章用最近在 RTX 5070 上部署 YOLO12 的完整过程做主线把安装、模型转换、C 推理、常见坑一次讲清楚。适合刚接触推理的新同学也适合已经踩了几天坑、想系统捋一遍的兄弟。1. 先搞清楚ONNX Runtime 和 TensorRT 到底在解决什么问题1.1 模型训练完不等于能直接上线“训练”和“推理”听起来像同一条流水线的两头实际差别非常大。训练阶段要不断前向、反向、更新梯度整个计算图是动态的框架里还保留着自动求导、断点保存这些重型能力推理阶段只做一次前向图是确定的理论上不需要任何梯度信息。同样是跑 YOLO12在 PyTorch 里做前向推理一张 640x640 的图在 RTX 5070 上差不多要几十毫秒这个速度做实验可以拿去线上扛并发肯定不行。推理引擎要做的就是把它重新“编译”一遍去掉所有用不上的部分用更高效的算子组合把延迟压下来。我最早从训练切到部署时吃过一个亏以为把模型权重保存成 .pth 再 load 进来就能上线。后来在同一个 GPU 上换了推理引擎延迟差了一个量级才反应过来模型文件是一回事怎么跑是另一回事。这里还要多说一句网上搜“推理”这个词时会撞上 QBF量化布尔公式这类逻辑推理词条那属于符号推理方向跟深度学习的张量推理不是一个赛道。你搜部署类的“推理任务”不要被这些非相关结果带偏它们对选型没有任何参考价值。1.2 ONNX Runtime不只是一个格式转换器很多新手以为 ONNX 只是个“中转格式”把 PyTorch 模型转成 .onnx 之后就完事了。其实 ONNX Runtime通常缩写成 ORT是一套完整的推理执行引擎它读取 ONNX 模型在运行时把计算图切分、调度到 CPU 或 GPU 上执行。为什么大家习惯先导出 ONNX因为主流训练框架都支持导出ONNX 找准了“生态公约数”这个位置。比如 PaddleOCR V6 的模型可以导出成 ONNX在 ONNX Runtime 里跑YOLO 系列也可以有些公司内部用 TensorFlow 训练的模型也能先转成 ONNX 再统一接管。这就是 ORT 的价值它不挑训练框架跨平台CPU 和 GPU 都能跑部署速度快适合做快速验证和通用场景的落地。如果你的需求是“把模型在别的地方跑起来”ORT 几乎是最短路径。1.3 TensorRT专门给 NVIDIA GPU 做性能压榨的引擎TensorRT 是 NVIDIA 推出的 GPU 推理引擎只支持 NVIDIA 自家 GPU换来的是实打实的性能收益。它做的事情大致可以分四块图优化、精度校准、显存优化和内核调优。图优化最典型的例子是层融合把卷积、批归一化、激活函数这些连续算子合并成一个内核减少显存读写次数精度校准是指可以把模型从 FP32 降到 FP16 甚至 INT8在精度损失可控的情况下把吞吐提上去显存方面TensorRT 会做 buffer 复用不会像训练框架那样给每个算子都保留独立显存。我习惯拿做饭打比方ORT 是啥菜系都能做的综合厨师水平稳定不容易出错。TensorRT 是专门在 NVIDIA 这张灶台上颠勺的名厨出品上限高但离开这张灶台就不开工。实用建议不要纠结“到底选 TensorRT 还是 ONNX Runtime”现在的成熟部署流程往往是先用 ORT 把逻辑跑通再针对性能敏感的模型转成 TensorRT engine两条路是互补关系不是竞争关系。2. 环境准备和版本选型踩坑从这里开始2.1 版本对不上后面全是白折腾部署环境里版本匹配是头等大事。很多莫名其妙的报错最后查来查去都是版本错位。以我这次 RTX 5070 的环境为例先给一个可以直接照抄的版本组合组件我这次用的版本注意事项显卡驱动560.x 以上RTX 5070 这类新卡尽量用新驱动别用半年前的老驱动CUDA12.4和驱动、TensorRT 配套即可cuDNN9.xTensorRT 依赖这个忘了装会在运行时报错TensorRT10.7用 tar 包解压目录自己管后面好拷贝ONNX Runtime1.20.xonnxruntime-gpu不要装成 CPU 版pip 名称很接近PyTorch2.5.x主要用于导出 ONNX 和对照验证版本错位的典型表现是Python 里 import tensorrt 成功但 C 链接时找不到符号或者跑模型时直接崩。原因很简单TensorRT 编译时的 CUDA 版本和运行环境不一致。解决办法是先确定一个基准版本比如以 TensorRT 10.7 为主CUDA 12.4、cuDNN 9.x、驱动版本都冲着这个基准去匹配。实在不想折腾建议直接用官方 Docker 镜像。Docker 能省掉很多环境问题但要注意 GPU 透传参数带对否则容器里看不到显卡推理引擎装了也白装。2.2 安装 TensorRTtar 解压的方式最省心TensorRT 安装有两种主流方式。一种是 deb 包装进系统目录优点是省心缺点是升级卸载麻烦而且会往系统里塞不少共享库之后想换版本容易留残留。另一种是 tar 包整个 TensorRT 目录解压到指定路径环境变量一配就好之后拷贝到其他机器或容器里也方便。我强烈建议用 tar 方式尤其是需要在多台机器上重复部署的场景。具体步骤从 NVIDIA 官网下载 TensorRT 的 tar 包注意选择与 CUDA 版本匹配的版本号。解压到某个固定目录比如/opt/tensorrt。配置环境变量export LD_LIBRARY_PATH/opt/tensorrt/lib:$LD_LIBRARY_PATH export PATH/opt/tensorrt/bin:$PATH验证安装python -c import tensorrt as trt; print(trt.__version__) /opt/tensorrt/bin/trtexec --version有两点容易踩坑。第一如果之前用过 pip 安装过旧版 tensorrt先卸载否则 Python 里 import 的是 pip 那个老版本而你命令行里调用的 trtexec 是新的两边对不上会让人困惑。第二LD_LIBRARY_PATH里不要同时存在多个 TensorRT 版本的 lib 目录运行时会随机加载其中一个属于“薛定谔的报错”。2.3 安装 ONNX RuntimeGPU 版别搞混ONNX Runtime 的安装比 TensorRT 简单得多直接 pip 装 GPU 版就行pip install onnxruntime-gpu装完建议先检查一遍确认用的是 GPU 执行提供程序import onnxruntime as ort print(available providers:, ort.get_available_providers()) print(device:, ort.get_device())如果输出里没有CUDAExecutionProvider多半是装了 CPU 版或者 GPU 版本动态库找不到依赖的 CUDA。这里有个很容易混淆的点ONNX Runtime 里也有一个 TensorRT 执行提供程序叫TensorrtExecutionProvider。它的含义是“ORT 把子图交给 TensorRT 去执行”跟你直接用 trtexec 生成 engine 是两回事。如果你已经通过 trtexec 生成了 .engine 文件那完全用不到这个 provider只有当你不想单独维护 TensorRT 代码、又想借用 TRT 加速时才在 ORT 的 providers 列表里加上它。这个概念当年绕了我好几天写出来帮大家少走弯路。3. 一条完整的推理链路从 PyTorch 模型到 TensorRT 部署3.1 导出 ONNX动态轴、opset 和一致性检查假设你已经有一个训练好的 YOLO12 模型第一步是把它导出成 ONNX。PyTorch 导出代码很简洁import torch model torch.load(yolo12.pt, map_locationcuda)[model].float() model.eval() dummy_input torch.randn(1, 3, 640, 640).cuda() torch.onnx.export( model, dummy_input, yolo12.onnx, opset_version17, input_names[images], output_names[outputs], dynamic_axes{images: {0: batch}, outputs: {0: batch}} )导出前核心想清楚两件事输入尺寸要不要动态输出节点放在哪。动态 batch 意味着 TensorRT 在构建 engine 时要额外处理 shape 可变的情况运行时 buffer 也要按最大 shape 预留性能和显存都会有损耗。如果线上就是固定 batch建议先设成静态 batch跑通之后再讨论动态。opset 版本影响算子的兼容性我用 opset 17 或 18 比较多比较稳。还有一个必须做的检查用一个固定输入分别跑 PyTorch 模型和 ONNX Runtime对比输出张量的最大绝对差异。允许有微小偏差因为算子融合和浮点顺序会带来细微误差但如果差异过大说明导出过程中有算子被替换掉了需要排查。3.2 用 trtexec 完成转换和性能摸底trtexec 是 TensorRT 自带的命令行工具不用写代码就能完成 ONNX 到 engine 的转换顺便还能跑一遍性能测试。常用命令/opt/tensorrt/bin/trtexec \ --onnxyolo12.onnx \ --saveEngineyolo12_fp16.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:1x3x640x640 \ --maxShapesimages:8x3x640x640参数含义--fp16开启 FP16 精度绝大多数视觉模型在 FP16 下精度损失可以忽略性能和显存收益明显。--minShapes/optShapes/maxShapes指定动态输入的最小、最优、最大形状如果模型是静态输入可以省略。--saveEngine把构建好的 engine 保存到文件后续部署直接加载。第一次构建 engine 通常很慢可能几分钟到十几分钟因为要做全图优化、算子融合、内核选择。这个等待是正常的构建完一次之后生产环境直接用反序列化加载秒级就能起来。这里有个非常重要的认知engine 文件与 GPU 型号强绑定。在 RTX 5070 上构建的 engine复制到别的 NVIDIA 显卡上大概率加载失败必须重新构建。TensorRT 版本升级后旧的 engine 也可能失效所以部署时要把模型文件、engine 文件、TensorRT 版本号一起记录免得线上更新时摸不着头脑。3.3 C 推理代码的核心骨架为什么部署要用 C倒不是 Python 跑不了而是生产环境往往有延迟稳定性要求。C 没有解释器和 GIL 的干扰第一次启动就能直接推理而且更容易嵌入到视频流、嵌入式设备等场景。核心流程可以用一段代码讲清楚。加载并反序列化 engine#include NvInfer.h #include fstream #include vector using namespace nvinfer1; std::vectorchar loadEngineFile(const std::string path) { std::ifstream f(path, std::ios::binary); f.seekg(0, std::ios::end); size_t size f.tellg(); f.seekg(0); std::vectorchar data(size); f.read(data.data(), size); return data; } IRuntime* runtime createInferRuntime(gLogger); auto engineData loadEngineFile(yolo12_fp16.engine); ICudaEngine* engine runtime-deserializeCudaEngine(engineData.data(), engineData.size()); IExecutionContext* context engine-createExecutionContext();注意createInferRuntime里的gLogger需要实现一个简单的日志接口通常直接用 TensorRT 自带的TRTLogger。IExecutionContext是执行上下文每次推理复用同一个 context 就行不要频繁创建销毁。推理部分核心是 buffer 分配和一次 memcpyauto inputIdx engine-getBindingIndex(images); auto outputIdx engine-getBindingIndex(outputs); void* inputGPU; cudaMalloc(inputGPU, inputSize); float* outputCPU new float[outputSize]; void* outputGPU; cudaMalloc(outputGPU, outputSize); // 输入图像预处理后填到 inputDataCPU cudaMemcpy(inputGPU, inputDataCPU, inputSize, cudaMemcpyHostToDevice); context-setTensorAddress(images, inputGPU); context-setTensorAddress(outputs, outputGPU); context-enqueueV3(stream); cudaMemcpy(outputCPU, outputGPU, outputSize, cudaMemcpyDeviceToHost); cudaStreamSynchronize(stream);这块有两个容易踩的坑。第一个坑是“bindings 的数量和顺序”。有些旧代码用context-setBindingDimensions和enqueueV2新版本推荐setTensorAddressenqueueV3参数以名字为准不要用 index 硬编码否则排错会非常痛苦。第二个坑是后处理时要记住 letterbox 的填充比例。YOLO 系列推理前通常会把原图等比缩放并填充到 640x640这个填充信息和缩放系数要保留下来。NMS 之后的框坐标是在 letterbox 后的图上算出来的还原到原图时要做一步逆变换减去 pad再除以 scale。如果不做这一步检测框位置会整体偏移这是视觉检测部署里最常见的低级错误。3.4 ONNX Runtime GPU 推理实现与耗时对照用 ONNX Runtime 做同一个模型的 GPU 推理代码非常短import numpy as np import onnxruntime as ort sess ort.InferenceSession( yolo12.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider] ) input_name sess.get_inputs()[0].name output sess.run(None, {input_name: input_blob})[0] print(output.shape)providers列表的顺序表示优先级GPU 可用时优先走 CUDA。注意第一次调用会有 session 初始化开销统计耗时之前要先跑几轮预热否则第一次的耗时会把整体平均值拉得特别高。我把两个引擎在同一个模型、同一种预处理条件下跑了对比结果大致是这样对比项ONNX RuntimeTensorRT首次启动快秒级需要构建 engine首次耗时以分钟计单帧延迟相对高更低吞吐中等更高易用性很友好几行代码API 复杂需要处理 buffer 和显存具体数值受显卡、驱动、模型结构影响很大我这次在 RTX 5070 上跑同规格的 YOLO12TensorRT 大概比 ORT 快 3 倍左右但不同算子比例下结论可能不同。别拿别人的跑分当自己机器的标准一定要在自己环境里实测一轮。4. 实际部署中的常见问题与排查实录4.1 训练后推理时那个 conf 参数到底是什么这个问题的出镜率极高值得单独拿出来讲。conf 是 confidence 的缩写即置信度。在目标检测里模型输出会对每个框给出一串分数代表这个框属于每个类别的概率。conf 参数就是一个阈值推理时把所有低于该阈值的候选框直接过滤掉只保留高置信度的结果。举例模型对某个框输出“person: 0.72car: 0.08dog: 0.03”如果设 conf0.5那么这个框会被判定为 person如果 conf0.8这个框就被丢掉了。关键是要和 NMS 的 IoU 阈值区分开。conf 是在 NMS 之前做初筛负责“砍掉不靠谱的框”IoU 阈值是在 NMS 阶段做去重负责“合并重叠的框”。两者的作用完全不同。调参经验分享通用目标检测任务我习惯设 conf0.25、IoU0.45这个组合在大多数业务数据上表现均衡。OCR 这类文本检测任务因为文本区域容易漏检conf 可以放宽到 0.1 到 0.15再用后处理里的版面信息去过滤。如果是检测后再接跟踪任务conf 也不要设得太高否则目标短暂被遮挡再出现时会因为漏检导致轨迹断裂。4.2 显存占用暴涨与 OOM 排查部署推理时经常遇到显存不够常见原因有四个第一动态 batch 没设上限。动态 shape 模式下TensorRT 会按最大 batch 分配显存。如果最大 batch 设成 32而线上请求只有 1显存也被预留了 32 份。第二FP16 没开。同样一个模型FP32 比 FP16 的显存占用多一倍能开就尽量开。第三buffer 反复申请。有些代码每个 batch 都cudaMalloc和cudaFree显存碎片化严重用几轮以后就分配不出大块显存了。正确做法是在启动阶段一次性分配好推理过程只做 memcpy。第四机器上还有其他任务在抢显存。比如同时跑着训练任务或者别的推理服务显存被占了大半。排查顺序建议先nvidia-smi看当前占用再用 TensorRT 的日志看引擎构建时的显存预留信息最后检查自己的 buffer 分配逻辑。我遇到过几次 OOM最后都是 buffer 碎片化导致的改成预分配后问题立刻消失。4.3 推理结果保存与可视化YOLOv11 场景有人问 YOLOv11 怎么保存推理结果其实很简单关键在几个不易察觉的细节上。基本流程是拿到 NMS 后的检测框画到原图上再保存。import cv2 for (x1, y1, x2, y2, conf, cls) in detections: color (0, 255, 0) cv2.rectangle(image, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) cv2.putText( image, f{class_names[int(cls)]} {conf:.2f}, (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1, ) cv2.imwrite(result.jpg, image)第一个坑是颜色通道。OpenCV 默认是 BGR如果你在 PyTorch 里做预处理时转成了 RGB保存前要转回 BGR否则画出来图像颜色是反的。第二个坑是坐标还原。前面说过letterbox 后的坐标必须缩放回原图。很多人在 YOLOv5 时代就吃过这个亏YOLOv11 依然存在因为做法继承了下来。第三个坑是视频流保存。如果推理对象是摄像头或视频不要每帧imwrite磁盘 IO 会成为瓶颈。用cv2.VideoWriter写视频流或者把多帧结果拼成一张大图再保存能明显提升整体吞吐。4.4 从单机脚本到推理服务化当推理要接入业务系统时单机脚本就不够了需要把模型包成服务。对于目标检测这类小模型最轻量的做法是用 FastAPI 包一层 HTTP 接口import cv2 import base64 import numpy as np from fastapi import FastAPI app FastAPI() app.post(/detect) def detect(data: dict): img_bytes base64.b64decode(data[img_base64]) img cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) # 预处理 推理 后处理 return {boxes: boxes, scores: scores}如果是大模型LLM场景那又是另一套体系。热词里的sglang serve就是面向大模型推理的服务化框架底层有连续批处理、PagedAttention 这类优化和 TensorRT/ORT 这种张量级引擎不是一个层面的东西。大模型的推理服务化更关注并发调度、显存管理和 KV Cache 复用而视觉小模型部署更关注单帧延迟和吞吐。这两类任务别混在一起选型。5. 我的个人判断与选型建议5.1 什么场景用哪个一张决策表做了这么多项目我把选型逻辑总结成一张表供你直接参考场景推荐方案原因线上单卡高吞吐目标检测TensorRT性能优势明显FP16 收益大跨平台/CPU 推理ONNX Runtime通用性强不挑硬件快速验证模型可导出性ONNX Runtime上手快几分钟出结果嵌入式 MCU如 RP2350都不推荐没有 NVIDIA GPU需走 TFLite Micro 等轻量方案大模型推理服务sglang / vLLM 等面向 LLM 的调度优化和深度学习图像推理不是一套体系我的建议流程始终是先导出 ONNX用 ONNX Runtime 把整个推理链路跑通确认结果没问题后再针对性能瓶颈切换到 TensorRT。别一上来就追求极限优化先让东西跑起来再谈跑得快。5.2 部署流程上容易被忽略的工程细节有几个工程细节看起来不起眼实际影响很大。第一版本锁定。engine 文件、模型文件、TensorRT 版本、CUDA 版本、显卡型号要作为一个整体记录进部署说明里。我见过线上服务更新时只替换了 engine 文件忘了同时换驱动结果新 engine 直接加载失败。第二多实例显存隔离。如果一张卡上跑了多个推理服务每个实例都按最大显存预留很容易互相挤爆。最好在服务启动前就明确划分每张卡上的显存配额而不是等 OOM 了再处理。第三性能测试方法。统计耗时必须分预热和正式两段正式阶段取多轮平均不要用第一次调用当参考。第一次调用包含了显存分配、内核加载、上下文初始化等一次性开销会误导判断。我每次部署新模型都会把 ORT 和 TRT 两套代码都留着。ORT 用于随时做正确性对照TRT 用于线上跑性能。一旦输出结果异常马上用 ORT 做差分测试快速定位是模型问题还是转换问题。这个习惯帮我省了不少排查时间。部署做了这几年最大的体会是工具没有绝对好坏关键是匹配场景。RTX 5070 上折腾 YOLO12 的时候我先用了半天把 ORT 流程跑通又花一下午转成 TensorRT 压性能。中间报过无数错但保持“一次只改一个变量”的习惯反而没有浪费太多时间。希望这篇总结能帮你把那几天的弯路省掉。