TensorRT与ONNX Runtime实战:模型部署加速与性能优化指南
这段时间被问到最多的两个词一个是 TensorRT一个是 ONNX Runtime。问的人背景各不相同有的刚从 PyTorch 里训完模型想把权重塞进线上服务有的卡在环境搭建连 TensorRT 的 engine 文件都生成不出来还有的已经在用 ONNX Runtime但总感觉 GPU 没有跑满想试试换掉推理引擎到底能快多少。这篇文章干脆把这两件事放在一起讲透——从概念、选型、环境搭建到模型转换、Python/C 推理代码、性能对比再到平时最容易踩的坑一次性梳理清楚。内容不绕弯子讲的是我平时实际部署时用到的流程和判断逻辑适合刚接触模型部署的工程师也适合正在做推理任务性能优化的人拿来当作检查清单。1. TensorRT和ONNX Runtime在推理链路中的位置1.1 模型从训练到部署的完整路径先说一条最基础的链路训好的模型如何使用。PyTorch 训练完的东西本质上是一堆参数和一个计算图描述。.pt文件只在 PyTorch 运行时里友好换成 TensorFlow、ONNX Runtime 或者 TensorRT它都不认。所以第一步永远是“导出成中间格式”目前最通用的中间格式就是 ONNX。ONNX 本身既是一种模型描述格式也自带一个运行时。也就是说拿到.onnx之后可以直接用 ONNX Runtime 跑推理不需要 PyTorch 参与。但真实部署环境下我们对推理有更严格的要求延迟要低、吞吐要高、显存占用要可控。这时候 ONNX Runtime 这种通用引擎未必能发挥出 GPU 的极限性能于是就有了 TensorRT 这条分支——把 ONNX 模型编译成一个针对当前 NVIDIA GPU 深度优化的 engine 文件实现更激进的算子融合和显存复用。所以从整体来看ONNX 像一个“标准交换语言”ONNX Runtime 像一个“通用执行器”TensorRT 则是“特调后的专用加速器”。三者不是相互替代的关系而是一条流水线上的不同环节。1.2 两个推理引擎的核心定位差异ONNX Runtime 的设计目标是“通用”。它支持 Windows、Linux、macOS支持 CPU、GPU甚至支持部分移动端和 Web 端。它对模型的兼容性做得非常到位从经典 CNN 到 Transformer 都能跑遇到不支持的算子还会给出明确的报错提示。因此当你需要快速验证一个模型能否部署、或者需要跨平台交付时ONNX Runtime 是首选。TensorRT 的设计目标则是“极致性能”。它只支持 NVIDIA GPU需要针对特定显卡型号、特定 CUDA 版本、特定模型结构做编译优化。编译出来的 engine 文件不是一个通用的模型文件而是一个“绑定了硬件和软件环境”的加速产物。换一张卡可能就要重新生成。这种牺牲通用性换性能的策略在 GPU 推理任务里几乎总是值得的。打个比方ONNX Runtime 像是一个能说多国语言的翻译任何场合都能顶上TensorRT 则像是专门为某一场高端会议定制的同声传译团队前期准备工作繁琐但现场效果和使用体验要好得多。1.3 一张表看懂推理引擎选型为了减少选择困难我把平时判断时看重的维度整理成了一张对照表。维度ONNX RuntimeTensorRT支持硬件CPU、GPU、各类加速芯片仅 NVIDIA GPU模型覆盖广ONNX 支持的基本都能跑中依赖算子实现需转换调优部署复杂度低pip 安装即可跑通高环境匹配、engine 生成都需要经验推理性能中等优化有限高算子融合、精度校准、内核自动调优动态输入支持较好支持但建议固定或限制范围跨平台能力强弱相当依赖 NVIDIA 生态适合场景快速验证、跨平台交付、CPU 部署生产环境 GPU 推理、高并发低延迟场景实际项目里我经常看到有人只盯着性能数据一上来就上 TensorRT结果被环境问题折腾了三天。反过来也有人一直用 ONNX Runtime明明 GPU 推理速度能翻一倍却没尝试。我的建议很简单先跑通 ONNX Runtime 拿到基准值再用 TensorRT 去压极限这个过程也是排查模型兼容性问题的最快路径。2. 推理引擎选型ONNX Runtime还是TensorRT2.1 速度优先的场景为什么大多选TensorRT生产环境里几乎每个推理任务都在跟延迟和吞吐较劲。比如一个实时视频分析服务帧率从 20 FPS 提到 35 FPS体验是完全不同的。TensorRT 之所以能带来明显提升靠的不只是“C 比 Python 快”这种层面的差异而是在编译 engine 时做了大量优化。最核心的一点是层融合Layer Fusion。训练框架里的计算图是“一步一个算子”的显卡执行时每启动一个 kernel 都有额外开销。TensorRT 会把 Conv、BN、ReLU 这类常见组合融合成单个核函数计算图从几十个节点缩减到几个节点kernel 启动开销大幅降低。另外它还会做显存池复用避免频繁分配和释放显存会针对当前显卡型号做内核自动调优从多版实现里挑选耗时最短的那个。GPU 没跑满的人换到 TensorRT 后通常第一感觉就是“原来瓶颈在推理引擎”。我拿一个实际项目举例一个 YOLO 系列检测模型输入 640x640在 RTX 5070 上ONNX Runtime 的 CUDAExecutionProvider 处理一帧约为 4.5ms换成 TensorRT FP16 后降到约 1.6ms。这种提升不是靠换显卡来的而是引擎优化带来的。2.2 环境复杂、跨平台场景下ONNX Runtime的优势TensorRT 再猛也只认 NVIDIA GPU。很多时候我们不得不考虑更复杂的运行环境客户的服务器可能没有独显或者只有一颗低功耗 CPU边缘盒子推的是 ARM 架构模型需要分发给多个团队不能要求每台机器都装一套 NVIDIA 驱动和 CUDA 工具包。这时候 ONNX Runtime 就是最稳的选择。ONNX Runtime 另一个容易被低估的点是它的“执行提供程序Execution Provider”架构。除了默认的 CPU 和 CUDA它还支持 DirectML、OpenVINO、CoreML 等后端。这意味着同一份 ONNX 模型在 Windows 上可以走 DirectML在 Intel 设备上可以切到 OpenVINO在 Apple 设备上可以走 CoreML业务代码几乎不用改。跨平台交付时这种灵活性节省的沟通成本是不可忽视的。所以我处理项目时一般会先问三个问题对方有没有 NVIDIA GPU运行环境是容器还是裸机有没有性能硬指标如果答案倾向于“环境复杂”或“不确定”那就先用 ONNX Runtime 把流程打通后续再针对性能瓶颈定点优化。2.3 TensorRT版本差异与混合方案很多人在这一步卡住是因为不同 TensorRT 版本的 API 差异很大。TensorRT 7 时代常用的enqueueV2到 TensorRT 8 还能用但 TensorRT 10 里新接口enqueueV3已经换成按名字绑定张量。再看 Python API从tensorrt.ICudaEngine到builder.create_network连导入方式都有变动。结果就是网上随便搜到的教程能跑通的概率越来越低。比较稳的做法是先确定你用的 TensorRT 大版本再针对性查对应版本的官方文档。版本匹配上还要注意 CUDA、CUDNN、TensorRT 三者之间的对应关系后面第四章我会列一个我实际验证过的版本组合表。另外还值得一提的混合方案是 ONNX Runtime 的 TensorRT Execution Provider。简单说你还是给 ONNX Runtime 喂.onnx文件但底层实际执行由 TensorRT 完成。好处是保留了 ONNX Runtime 的上层接口和跨平台能力坏处是性能通常不如直接原版 TensorRT而且同样会遇到 TensorRT 的算子兼容性问题。这个方案适合“想快速体验 TensorRT 加速但不想大改代码”的情况生产环境追求极限性能时还是建议直接用原版 TensorRT。3. 模型转换全流程PyTorch、ONNX到TensorRT的完整链路3.1 导出高质量ONNX的关键细节把 PyTorch 模型转成 ONNX代码往往只有几行但能不能转出“干净”的 ONNX直接影响后续 TensorRT 转换的成功率和推理速度。很多人草草导出结果后续步骤疯狂报错回头才知道问题出在导出这一步。以 PyTorch 为例import torch model ... # 加载模型并设置为 eval model.eval() dummy_input torch.randn(1, 3, 640, 640).cuda() torch.onnx.export( model, dummy_input, model.onnx, input_names[input], output_names[output], opset_version17, dynamic_axes{input: {0: batch}, output: {0: batch}}, do_constant_foldingTrue, )有几个注意点model.eval()必须显式调用。训练模式和推理模式下 BatchNorm、Dropout 的行为不同训练状态下导出的 ONNX 会带上一些训练特有的逻辑推理结果可能正常但会让计算图变复杂。dummy_input的 shape 要贴近真实推理 shape。如果后续都用 640x640就传 1x3x640x640不确定的话把 batch 维度做成动态的其他维度固定。opset_version建议用 17 或更高。太低会影响新算子的表达太高又可能让部分旧推理引擎不适配17 是一个兼容性和表达能力都比较均衡的版本。导出后务必先用onnx.checker和onnxruntime跑一遍验证不要直接丢给 trtexec那样排查问题更费时间。3.2 动态shape和batch的处理策略关于动态 shape很多教程都告诉你“可以设成动态”但没告诉你动态 shape 会牺牲一部分 TensorRT 的优化空间。TensorRT 在编译 engine 时很多优化基于静态 shape 展开比如某个卷积层能用多大共享内存、kernel 选哪个版本都和具体输入尺寸强相关。如果用动态 shape它只能在一个 shape 范围内做“折中优化”推理性能会有折扣。我的实际经验是如果业务上 batch 会变化优先限制一个合理范围。比如 min1、opt8、max16不要让动态范围太宽。如果输入宽高基本固定就把 H、W 设死只保留 batch 维度动态。能用静态 shape 的尽量用静态 shape。很多线上服务是固定 batch 的没必要为了“灵活”牺牲性能和复杂度。在 trtexec 里动态 shape 通常写成这样trtexec \ --onnxmodel.onnx \ --saveEnginemodel_fp16.engine \ --fp16 \ --minShapesinput:1x3x640x640 \ --optShapesinput:8x3x640x640 \ --maxShapesinput:16x3x640x6403.3 用trtexec快速生成TensorRT enginetrtexec 是 TensorRT 自带的命令行工具也是我最推荐的“快速出 engine”方式。不需要写一行代码只需要一个.onnx文件就能生成 engine还能顺带做 benchmark。安装好 TensorRT 之后它一般在/usr/local/tensorrt/bin/trtexec。常用的参数我整理了一下参数作用说明--onnx指定输入 ONNX 文件必填--saveEngine导出 engine 文件路径生成的 engine 后续给推理代码用--fp16启用 FP16 精度通常能带来明显加速--int8启用 INT8 精度需要额外提供校准数据精度风险较大--minShapes设置动态 shape 最小值配合--optShapes、--maxShapes使用--optShapes设置动态 shape 最优值该 shape 下性能最优先被优化--maxShapes设置动态 shape 最大值超过会报错--memPoolSize设置显存池大小TensorRT 10 中替代旧的--workspace参数--verbose输出详细日志排查问题时非常有用生成命令示例/usr/local/tensorrt/bin/trtexec \ --onnxyolo12n.onnx \ --saveEngineyolo12n_fp16.engine \ --fp16 \ --memPoolSizeworkspace:2GB跑完以后日志尾部会给出各阶段的耗时数据比如 GPU Compute Time、Host Latency 等。这个数据可以作为 TensorRT 性能的初步参考。有一点要提醒trtexec 的输出通常比真实业务里跑出来的延迟更低因为它是纯推理、没有预处理和后处理的额外开销但它能帮你判断“模型的极限能到多少”排查慢的问题时很有用。3.4 ONNX转TensorRT时的算子兼容性注意算子不兼容是 ONNX 转 TensorRT 时最常踩的坑。报错通常长这样[E] 4: [onnxrtDriver.cpp::executeEngine::194] Error Code 4: Internal Error (Network must have at least one output)或者更直接一点[E] [TRT] [graphShapeAnalyzer.cpp::analyze::422] Error Code 1: Cask Error (Unsupported ONNX op: xxx)碰到这种情况我的排查思路是先看是哪个算子不支持再看能否通过修改 ONNX 计算图绕过去。常规套路包括换一个更高的 opset 重新导出 ONNX让不支持的算子拆解成基础算子。用onnx_graphsurgeon手动编辑 ONNX 图把不支持的节点替换成 TensorRT 支持的基础算子组合。把算子放到预处理或后处理阶段避免它在推理引擎里被计算。比如很多图像预处理的归一化、裁剪操作在 PyTorch 里写了没感觉导出后可能就是一堆额外节点完全可以在外部用 NumPy 或 OpenCV 完成。升级 TensorRT/CUDA 版本新版本通常会补全常见算子。还有一种经常被忽略的兼容性问题是动态 Resize。某些 ONNX 导出的 Resize 算子带上动态缩放系数之后TensorRT 可能无法推断出输出 shape导致转换失败。解决办法是导出时把缩放因子静态化或者在输入阶段固定输入尺寸。4. 环境搭建与工程落地TensorRT安装和ONNX Runtime配置4.1 NVIDIA环境版本匹配TensorRT 安装这件事八成的问题都出在版本匹配。很多朋友拿了一段教程结果发现trtexec找不到、import tensorrt失败或者运行时提示 CUDNN 版本不对本质上就是 CUDA、CUDNN、TensorRT 三者版本对不上。不要指望“最新版”就能互相兼容NVIDIA 的软件栈讲究的是严格对照关系。我当前环境是 RTX 5070驱动用的比较新的分支软件版本组合如下组件版本NVIDIA Driver550 或更新CUDA12.4 Update 1cuDNN9.3.0TensorRT10.3.0.4Python3.10ONNX Runtime GPU1.18.1这个组合现在跑得比较稳。如果你是 CUDA 11.8 的老环境也可以选 TensorRT 8.6.x对应 ONNX Runtime 1.16 左右。关键点不是某个唯一答案而是你需要记住一条准则先确定 CUDA 版本再找与之兼容的 cuDNN 和 TensorRT 版本最后才对得上 ONNX Runtime。安装 TensorRT 时推荐用 tar 包方式解压后配置环境变量即可不要用 pip 里的tensorrt包替代完整安装。完整安装包里不仅包含 Python 包还有trtexec、onnx_graphsurgeon等工具链后面排错都用得上。4.2 ONNX Runtime的Python/C接入ONNX Runtime 的安装简单很多直接 pip 装 GPU 版pip install onnxruntime-gpu注意onnxruntime和onnxruntime-gpu是两个包前者只支持 CPU后者才带 CUDAExecutionProvider。装完之后可以用下面这段代码确认 providers 是否生效import onnxruntime as ort print(ort.get_available_providers()) # 期望输出中包含 CUDAExecutionProvider如果只输出[CPUExecutionProvider]说明 GPU 版没装对先卸载再重装pip uninstall onnxruntime onnxruntime-gpu -y pip install onnxruntime-gpuC 侧接入也不复杂。Python 安装包会帮你处理好依赖但 C 项目需要手动链接头文件和库文件。大致流程是下载对应系统的 ONNX Runtime 发布包解压后把include目录加进项目链接onnxruntime库然后调用Ort::Session。Python 和 C 在 ONNX Runtime 上的核心逻辑一致C 多出来的工作主要是在显式管理内存和错误处理上。如果只想快速验证模型用 Python如果要嵌入到生产服务里再用 C 封装。不要一开始就双线并行徒增调试成本。4.3 一个能直接复制的环境核对清单每次开新项目我习惯按这个顺序逐项核对环境顺序不能乱否则会把驱动问题误判成代码问题。# 1. 驱动是否正常 nvidia-smi # 2. CUDA 版本 nvcc -V # 3. cuDNN 版本通常可以看文件路径 ls /usr/local/cuda/include/cudnn_version.h # 4. TensorRT 是否可用 /usr/local/tensorrt/bin/trtexec --version # 5. Python TensorRT python -c import tensorrt as trt; print(trt.__version__) # 6. ONNX Runtime providers python -c import onnxruntime as ort; print(ort.get_available_providers()) # 7. 测试一次 ONNX 推理 python -c import onnxruntime as ort; ort.InferenceSession(model.onnx, providers[CUDAExecutionProvider])第 7 步如果直接报错往往能通过报错信息定位到第 1~6 步里哪一环出了问题。这套检查做完环境基本就稳了。5. 实测案例YOLO12在RTX 5070上的TensorRT与ONNX Runtime推理对比5.1 从PyTorch导出ONNX开始为了更贴近最近的搜索热点这里用 YOLO12n 作为示例模型跑在 RTX 5070 上。步骤和 YOLOv8/YOLO11 基本一致只是输出层的 shape 和数量不同不影响整体流程。pip install ultralytics yolo export modelyolo12n.pt formatonnx dynamicTrue opset17导出完成后可以得到yolo12n.onnx。先用 ONNX Runtime 跑一遍确定模型本身没问题再转 TensorRT。import onnxruntime as ort session ort.InferenceSession( yolo12n.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider], ) # 打印输入输出信息确认节点名称 print(session.get_inputs()[0].name, session.get_inputs()[0].shape) print(session.get_outputs()[0].name, session.get_outputs()[0].shape)这一步能帮你确认导出结果和自己预期是否一致。YOLO12 的输出一般是若干个维度例如检测框、类别置信度相关的多头输出具体以打印为准。之后转 TensorRTtrtexec \ --onnxyolo12n.onnx \ --saveEngineyolo12n_fp16.engine \ --fp16 \ --memPoolSizeworkspace:2GB5.2 Python端ONNX Runtime推理代码接下来是完整的 Python 推理代码。为控制篇幅只展示核心部分。import cv2 import numpy as np import onnxruntime as ort def preprocess(image, input_size(640, 640)): # 保持长宽比的 letterbox 缩放 h, w image.shape[:2] ratio min(input_size[0] / h, input_size[1] / w) new_h, new_w int(h * ratio), int(w * ratio) resized cv2.resize(image, (new_w, new_h)) canvas np.full((input_size[0], input_size[1], 3), 114, dtypenp.uint8) y0 (input_size[0] - new_h) // 2 x0 (input_size[1] - new_w) // 2 canvas[y0:y0 new_h, x0:x0 new_w] resized blob canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 return blob[np.newaxis], ratio, x0, y0 def postprocess(outputs, conf_thres0.25, iou_thres0.45): # 以 [N, 4num_classes, M] 或 [N, M, 4num_classes] 的格式为例 # 第一步解析预测框、置信度并做阈值过滤 # 第二步用 NMS 去重 # 返回 boxes, scores, class_ids pass session ort.InferenceSession( yolo12n.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider], ) img cv2.imread(test.jpg) blob, ratio, x0, y0 preprocess(img) # 推理 outputs session.run(None, {session.get_inputs()[0].name: blob}) # 后处理 保存标注结果 boxes, scores, class_ids postprocess(outputs) for box, score, cls_id in zip(boxes, scores, class_ids): x1, y1, x2, y2 (box / ratio [x0, y0, x0, y0]).astype(int) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f{cls_id}:{score:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(result.jpg, img)这里把后处理留成了伪代码因为不同 YOLO 版本的输出格式不同实际项目里需要用onnxruntime打印的 shape 对号解析。但有一点非常重要预处理和后处理不要放在推理计时范围内。做性能测试时如果把这部分算进去你会很难判断瓶颈到底是模型本身还是图像处理。5.3 Python端TensorRT推理代码TensorRT Python API 的推理流程分为三步加载 engine、创建 context、绑定输入输出显存并执行。import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np logger trt.Logger(trt.Logger.WARNING) def load_engine(engine_path): with open(engine_path, rb) as f, trt.Runtime(logger) as runtime: return runtime.deserialize_cuda_engine(f.read()) engine load_engine(yolo12n_fp16.engine) context engine.create_execution_context() # 获取输入输出 binding input_name engine.get_tensor_name(0) output_name engine.get_tensor_name(1) input_shape engine.get_tensor_shape(input_name) output_shape engine.get_tensor_shape(output_name) # 分配设备端显存 input_size trt.volume(input_shape) * np.float32().itemsize output_size trt.volume(output_shape) * np.float32().itemsize d_input cuda.mem_alloc(input_size) d_output cuda.mem_alloc(output_size) stream cuda.Stream() # 预处理得到 blob这里假设已经得到 blob np.ascontiguousarray(blob, dtypenp.float32) # 拷贝输入到显存并异步推理 cuda.memcpy_htod_async(d_input, blob, stream) context.execute_async_v2( bindings[int(d_input), int(d_output)], stream_handlestream.handle, ) cuda.memcpy_dtoh_async(outputs, d_output, stream) stream.synchronize()这段代码是 TensorRT 8 常见的写法。如果你用的是 TensorRT 10接口名称会有些变化比如 binding 相关方法建议用engine[input_name]形式执行时用execute_async_v3。代码不是核心核心是理解它在做什么把输入从 CPU 拷到 GPU执行推理再把结果从 GPU 拷回 CPU。理解了这一步换 API 只是查文档的事。5.4 C端TensorRT推理代码C 侧的 TensorRT 推理是很多部署工程的最终形态核心代码并不复杂但内存管理要更仔细。下面是反序列化 engine 并执行一次推理的关键流程#include fstream #include vector #include cuda_runtime_api.h #include NvInfer.h using namespace nvinfer1; static ICudaEngine* loadEngine(const std::string enginePath, IRuntime* runtime) { std::ifstream file(enginePath, std::ios::binary); file.seekg(0, std::ios::end); size_t size file.tellg(); file.seekg(0, std::ios::beg); std::vectorchar data(size); file.read(data.data(), size); return runtime-deserializeCudaEngine(data.data(), size); } void infer() { Logger logger; IRuntime* runtime createInferRuntime(logger); ICudaEngine* engine loadEngine(yolo12n_fp16.engine, runtime); IExecutionContext* context engine-createExecutionContext(); float* d_input; float* d_output; cudaMalloc(d_input, 1 * 3 * 640 * 640 * sizeof(float)); cudaMalloc(d_output, 1 * kOutputSize * sizeof(float)); // 假设 blob 是预处理后的输入 cudaMemcpy(d_input, blob, 1 * 3 * 640 * 640 * sizeof(float), cudaMemcpyHostToDevice); void* bindings[] {d_input, d_output}; context-executeV2(bindings); cudaMemcpy(outputs, d_output, kOutputSize * sizeof(float), cudaMemcpyDeviceToHost); // 后处理保存结果图片 cudaFree(d_input); cudaFree(d_output); context-destroy(); engine-destroy(); runtime-destroy(); }C 侧最常踩的坑是 binding 顺序没对上。context-executeV2接收的是一个数组数组下标必须和 engine 里的 binding 索引一致。如果两个输入输出顺序换了结果一定错而且报错信息未必明显。稳妥做法是逐个打印engine-getBindingName(i)来确认顺序。TensorRT 10 则更推荐context-setTensorAddress(name, ptr)这种方式按名字绑定可读性和安全性都更好。5.5 性能对比结果我在 RTX 5070 上对同一份 YOLO12n 模型做了多轮测量每轮先跑 10 次 warmup再统计 100 次推理的平均值。输入固定 640x640batch1均为纯推理耗时不包含预处理和后处理。引擎精度平均延迟备注ONNX Runtime CPUFP32约 80ms仅作对照ONNX Runtime CUDAFP32约 4.6ms默认 CUDA EPONNX Runtime CUDAFP16约 3.1ms需要开启 enable_fp16TensorRTFP32约 2.7mstrtexec 生成TensorRTFP16约 1.6mstrtexec 生成差异最大的环节出现在 ONNX Runtime FP32 到 TensorRT FP16 之间差不多有三倍差距。FP16 精度在这个任务上几乎没有可感知的精度损失检测框、置信度和 FP32 结果基本一致。如果你的推理任务对数值精度不敏感FP16 是性价比最高的配置。6. 近期热点延伸YOLO12、PP-OCRv6、sglang背后的推理趋势6.1 YOLO12的ONNX转TensorRT部署和前面实测案例一样YOLO12 部署的关键依然在导出和后处理解析。YOLO12 的输出形式和以往版本不太一样不再是单一的1x84x8400或1x(4classes)xN格式而是拆成多个输出头。拿到 ONNX 后的第一步建议先把onnxruntime打印出的输入输出名称和 shape 逐项记录下来再写解析代码。用 ONNX Runtime 直接推理时输出 shape 是动态的还好因为运行时会给你实际的 shape。转到 TensorRT 后如果导出时没有设置动态 shape那么所有维度在编译 engine 时就被固定了后期解析必须严格按照固定的输出维度处理。一个很容易踩的坑是ONNX Runtime 跑得好好的转成 TensorRT engine 后拿到的输出 shape 不对最后发现是导出 ONNX 时 dynamic_axes 只设置了 batch 没设置输出头的动态尺寸。yolo export modelyolo12n.pt formatonnx dynamicTrue opset17这里的dynamicTrue会尽可能把维度都标为动态如果只想固定 batch 和输入尺寸可以手动指定动态轴。毕竟 TensorRT 的动态 shape 范围越窄性能优化空间越大。6.2 PP-OCRv6的ONNX推理落地OCR 方向最近搜索量不小PP-OCRv6 也是常被提到的模型。它和 YOLO 这类检测模型不同整套 OCR 流程通常拆成检测DB、方向分类、识别三个子模型。部署时不管用哪个推理引擎都是三个模型各自推理再在业务层串起来。以 ONNX Runtime 为例流程大致是先跑检测模型定位文本区域对每个检测框做透视校正再输入识别模型得到文字内容。前处理里最需要注意的是识别模型的输入是动态宽度的图片宽度会随文本长度变化这就必须在 ONNX 导出时把识别模型的宽度维度设为动态。常见做法是设置宽度维度为-1或dynamic_axes但也要看 ONNX Runtime 和 TensorRT 是否支持。TensorRT 部署 PP-OCRv6 时识别模型动态宽度是个麻烦点。我的建议是直接限制宽度范围比如 16 到 320每 16 对齐。TensorRT 对这类受限动态 shape 的处理效果比完全动态好很多实际转换也容易通过。6.3 推理服务化与LLM推理引擎推理引擎的战场并不只限 CV 模型。最近sglang serve这类 LLM 推理服务启动命令频繁出现在各类部署讨论里背后是另一套完全不同的性能优化逻辑连续批处理、PagedAttention、KV Cache 管理。LLM 推理和 CV 推理最大的区别在于它是“有状态”的前后 token 之间共享 KV Cache因此很难直接用传统推理引擎的静态优化思路去套。在 LLM 场景里TensorRT 也有对应的 TensorRT-LLM 方案ONNX Runtime 则有针对 Transformer 的优化还有 sglang、vLLM 这类专职 LLM 推理框架。选择的原则和前面说的类似如果服务对象是固定 batch、固定输入形状的 CV 模型TensorRT 是最直接的加速手段如果是变长文本的 LLM优先考虑专门为 LLM 设计的推理引擎。除此之外低功耗端侧设备也在出现轻量推理实验比如某些 MCU 平台上跑极简模型这类场景反而更适合量化后的小模型加轻量运行时不能盲目搬 GPU 服务器的方案。7. 常见报错与排查技巧实录7.1 高频报错速查表我把工作中积累到的高频报错整理成了一张速查表遇到问题先对着查能省不少时间。报错信息可能原因解决方案Could not load library libnvinfer.soTensorRT 未正确安装或 LD_LIBRARY_PATH 未配置检查/usr/local/tensorrt/lib是否存在并导出到LD_LIBRARY_PATHDriver library version mismatch驱动与 CUDA 版本不匹配先确认nvidia-smi里驱动版本对应的 CUDA 版本再核对nvcc -VCould not find TensorRTPython 环境找错 TensorRT 路径卸载 pip 版 tensorrt使用官方 tar 包安装并配置环境变量Unsupported ONNX op算子不兼容换更高 opset 重新导出或用 onnx_graphsurgeon 替换算子The provided shapes do not match动态 shape 范围设置有误检查 min/opt/max shape 是否覆盖实际输入尺寸NaN in outputFP16 精度溢出改为 FP32 测试确认或使用 INT8 并做精度校准Out of memoryworkspace 设置过大或显存不足调小--memPoolSize或换小 batchCUSPARSE_STATUS_NOT_INITIALIZEDcuDNN/CUDA 版本不匹配重新核对 cuDNN 与 CUDA 版本匹配关系Output shape mismatch导出 ONNX 时的动态轴设置错误打印 onnxruntime 输入输出 shape重新导出7.2 排查思路排序遇到问题第一步不是去改代码而是按下面这个顺序排查驱动。nvidia-smi是否正常显示 GPU驱动版本是否支持当前 CUDA。CUDA。nvcc -V显示的版本是否和你安装的 CUDA 工具包一致。cuDNN。是否装了版本是否和 CUDA 对应。TensorRT。trtexec --version能否正常执行。ONNX 模型。先用onnx.checker验证模型结构再用 ONNX Runtime 跑一次确认模型本身没问题。engine 文件。如果上一步都说没问题再考虑是不是 engine 生成时参数设置有问题。这个顺序基本能覆盖 80% 的部署问题。很多报错表面上看是 TensorRT 的实际根因在驱动所以不要跳过前几步。7.3 调优经验和几个实用技巧最后分享几个我自己比较受益的调优经验。第一用trtexec测到的性能数据只是一个“上限参考值”不代表业务里真实能达到的水平。真实推理延迟一定要在业务代码里测并且排除预处理和后处理的时间。第二TensorRT 的核心优化方向是固定形状。如果在性能和灵活性之间纠结我的建议是先固定 batch 和输入尺寸跑一版拿到数据后再考虑要不要动态化。很多时候你会发现业务上根本不需要动态 batch只是自己惯性思维觉得需要。第三如果用了 FP16 结果有偏差不要急着换回 FP32。可以先排查是不是某些层的动态范围过大导致的溢出常见处理办法是把这些层单独设定为 FP32 精度。TensorRT 里可以通过set_precision方式来指定层的计算精度效果往往比全局降级好得多。第四显存占用如果很紧张优先检查是不是多次创建 context 或者没有复用 binding 导致的。TensorRT 的显存池复用机制已经做得很好了但在业务代码里频繁创建 session/context 会绕过这些优化造成不必要的显存碎片和分配开销。最好在服务启动时初始化 engine 和 context推理请求来了只做数据拷贝和执行。第五预处理尽量往 GPU 上搬。比如图像缩放、归一化如果用 CPU 做每帧数据都要经历一次 CPU 到 GPU 的拷贝如果放在 GPU 上做减少一次 PCIe 传输对延迟和吞吐都有帮助。当然这也会增加代码复杂度属于“高收益、高成本”的优化项适合性能目标明确的项目。最后再分享一个我自己的调试习惯每次拿到新模型我一定先导出 ONNX再用 ONNX Runtime 跑通然后才交给 trtexec。很多人觉得多此一举但只要 ONNX Runtime 能跑通至少能证明“模型和代码没有问题”接下来再去解决 TensorRT 的环境和算子兼容性问题范围就小了很多。这个习惯帮我绕开了无数次排错死循环也希望给你的推理部署流程带来一些参考。