
接手过不少从训练到部署的项目说实话模型在测试集上刷点的兴奋感往往会被部署上线时那口憋住的气给冲散一半。尤其是当模型要跑在边缘设备或实时服务里训练框架自带的推理接口根本顶不住。这年头做推理优化基本绕不开ONNX和TensorRT这俩名字。如果你正准备把模型弄上线或者已经被推理延迟折磨得头疼这篇文章会很有用。简单说ONNX是一个模型中间格式负责让不同框架之间可以互相转换TensorRT则是一个高性能的推理引擎能把计算图优化成硬件上跑得最快的形态。两者搭配使用是当前工业界非常主流的部署链路。这篇文章会覆盖这条链路的完整流程、里面最关键的细节、我踩过的坑以及一些在正式文档里不太好找到的实操习惯。1. 不是所有模型都需要TensorRT但所有模型都该考虑ONNX动手之前先把这条技术链路所处的位置说清楚。很多同学一开始接触时会有个误解觉得ONNX和TensorRT是同一个层面的东西。实际上它们是两个完全不同的角色。ONNXOpen Neural Network Exchange本质上是个模型表达格式。类似把一个满汉全席的大厨PyTorch做的菜用一套通用的拍照标准记录下来。不管之后谁看这张照片都能照着原样复刻。这个格式的意义在于它定死了一套算子的标准接口。PyTorch里有几百个不同的层写法Transformer里自带的注意力模块、各种自定义的损失函数、条件分支的forward逻辑一股脑全塞进ONNX里就会收获一大堆报错和不兼容。一定要记住这一点不要试图一股脑直接把整个PyTorch模型丢进去转ONNX这是绝大多数新人崩溃的根源。TensorRT则是另一回事。它不是通用格式而是一个专门针对某GPU硬件优化过的执行引擎。你可以把ONNX当成一张描述清楚的工程设计图纸TensorRT是一个自带大型加工厂的施工队它拿到图纸后会根据你给的材料和机床参数把图纸重新设计、合并流水线、淘汰冗余工序最终产出一个为这组机床量身定做的成品——一个TensorRT engine文件。正因为是针对特定硬件、特定batch大小、特定精度优化出来的所以tensorrt的引擎文件没法在另一张显卡上直接通用CUDA版本、驱动版本变了也可能要重新生成。那ONNX在这个链路里到底多有必要直接拿PyTorch模型杀到TensorRT行不行行但也有不少麻烦。TensorRT对PyTorch动态图结构的解析能力有限因为PyTorch是即时执行、动态分派的框架算子级别的图结构是运行时才能完整确定的。而ONNX提供一个静态、有向无环的计算图描述TensorRT战队读取效率最高。这就好比你想让一个陌生厨师看你说“给我炒个菜”不如给他一张写了完整配料的菜谱他能直接开工。所以常规流程一定是PyTorch - ONNX - TensorRT。基于我对这么多项目的观察这组工具组合最核心的价值有三块跨框架协作。有的团队用PyTorch训练有的用TensorFlow还有的图省事直接拿Keras搭原型。最后想要在一个统一的部署框架里跑起来ONNX就是中间那根最粗的通用转轴。推理性能显著提升。TensorRT的优化不是简单“翻新”而是做算子融合把卷积、BN、激活函数拼成一个核函数、精度校准FP32转FP16或INT8、显存复用、多流调度等深度优化。我第一次把一个分割模型从PyTorch的CUDA加速版换成TensorRT的FP16版本延迟直接降了一半还多那个震撼感至今记得。部署环境轻量化。生产环境不需要装PyTorch了只需要TensorRT和必要的CUDA库镜像体积小很多启动也快减少因为框架版本不同带来的乱七八糟的依赖问题。2. 什么时候该用它什么时候不该用把话往前说一点。我真不觉得所有场景一上来就必须上TensorRT。这玩意儿是有学习成本的而且用来做交付的时候坑还不少。讲清楚什么时候用、什么时候不该用能帮你省下大量时间。2.1 项目阶段判断从训练原型到生产交付如果你还在算法验证阶段还在频繁调整模型结构、改损失函数、反复尝试各种注意力层结构这时候强行把模型导成ONNX再转TensorRT每次改动都要重新经历一遍导出、转换、验证的过程你大概率会烦到想骂人。这个阶段老老实实用PyTorch自带的推理接口就够开发迭代速度比推理性能重要得多。当你确定了一个备选模型开始检查它在服务器上的推理速度、并发能力或者准备封装成服务接口这时候就该认真考虑ONNXTensorRT的链路了。如果模型训练阶段没有做任何算子兼容性规划在转换时大概率会遇到支持性问题需要回到代码层面调节点。这就是为什么资深部署工程师会从第一天就盯着模型结构提前把动态shape、算子种类这些因素考虑进去。我见过一个同事开发时天天拍胸脯说“后面部署肯定没问题”结果三个月后要上线被一个简单的常数操作卡了两天一个不到100MB的模型愣是转换失败了二十多次原因竟然是模型里有一段很隐蔽的控制流逻辑PyTorch动态图跑得好好的ONNX转换时它不认。2.2 硬件和性能收益的边界TensorRT的优化收益是不均匀的。如果你跑的是几十层的ResNet这种标准结构算子类型有限、计算比较规整TensorRT能做的优化很强。如果模型结构很杂穿插着各种自定义算子、动态分支比如根据输入长度来决定循环次数TensorRT的优势就会被削弱很多有时候甚至因为频繁的图优化失败而跑不起来。显存方面也得掰扯。TensorRT的算子融合和显存复用机制确实能省显存但engine构建过程本身会消耗额外的显存和内存尤其用INT8量化时还需要校准用的数据集流程。小显存的卡上用TensorRT反而可能OOMOut Of Memory这种我见得太多了。还有个容易被忽视的点——batch size与TensorRT性能的关系。TensorRT对动态batch的适应性一般如果你部署的服务需要支持多种并发不同的batch输入每个batch大小都需要调整优化配置engine构建时间成倍增加。这种情况下你要考虑是牺牲一点性能固定batch大小还是花更多时间让engine支持多个profile。这些都是实际项目中绕不开的取舍。3. 完整的模型导出动手写代码前的关键准备正式开始实操前有几个准备动作做不做得好直接决定后面踩不踩坑。3.1 锁定PyTorch与CUDA版本这不是小事。我见过太多案例PyTorch是1.12的TensorRT却是8.5版本编译出来的engine文件动不动就报版本不一致的错。我把一套比较顺手的版本组合放在这里基于我长期实测经验CUDA 11.8 或 12.0以上PyTorch 2.0以上1.x也能用但2.x对导出ONNX的兼容性明显更好TensorRT 8.6以上8.5比较疯有些算子和动态shape支持不如8.6ONNX Runtime如果你做Pipeline对比测试最好装最新版提示显卡驱动版本不需要跟CUDA版本严格对应但驱动太老会导致TensorRT调用底层库失败。这类报错通常很诡异比如“cannot find cudart64_120.dll”之类。网上很少有人直接指出这是驱动问题我遇到两次才反应过来先更新驱动可能是最快的解法。3.2 为转换而准备的模型代码很多模型代码设了各种默认参数batch_firstTrue、return_dictTrue、混合了不用条件的dropout、有随机种子。这些在训练时是好习惯在导出ONNX时就是要命的。导出前需要做的核心操作是把模型切分成两部分需要转成TensorRT的部分特征提取层、大计算量的层不需要转的部分数据预处理、后处理NMSNon-Maximum Suppression、解码逻辑让预处理和前后处理留在PyTorch环境或干脆用Python处理把模型真正“计算重”的部分交给ONNX这个思路能避免大量算子兼容问题。举几个具体做法# 把模型切换为eval模式这一步必须 model.eval()eval模式不只是关掉dropout和BN层的训练行为它会将BN层的参数冻结成固定的数值这个数值会写进ONNX图里。如果忘了切eval导出出来的模型在部署后会出现各种精度不一致的诡异问题。这类问题特别恶心因为不会立刻报错行为极其隐蔽。在迁移时强烈建议在推理前再确认一次model.eval()。另一类常见问题是多余的动态分支。下面的代码即使在推理时只有一条分支会执行转换时两个分支都会被记录下来ONNX里会有很奇怪的冗余子图TensorRT优化时会拒绝这部分。如果任务里存在这种条件判断尽量把它放到模型外面去做成预处理逻辑。if mode train: return x * 2 else: return x * 3正确做法是拆出去# 推理时永远走的一条路 def infer(x): return x * 33.3 构造正确的Dummy InputONNX转换需要一个“虚拟输入”来追踪计算图结构。这里的核心原则是dummy input的shape必须覆盖你的实际推理输入且数据类型必须与模型定义完全一致。如果你是做图像分类输入是3x224x224那dummy input就用三种典型的shape[1,3,224,224]、[4,3,224,224]和[8,3,224,224]。用多个不同shape的输入各跑一次其实没什么必要后面TensorRT配dynamic shape时其实更推荐在config里同时设置min、opt、max三个档位。构造dummy input时别用全0矩阵也别用torch.randn。全0矩阵会让某些批归一化层的输出把这个值直接变成delta分布对静态图的shape推导没问题但对某些量化校准过程会有影响。建议使用真实数据集里的几张图片做dummy input既做转换又做精度对比。import torch # 用不同batch大小创建两个dummy input dummy_input_a torch.randn(1, 3, 224, 224).cuda() dummy_input_b torch.randn(8, 3, 224, 224).cuda() # 实际使用中建议加入一个数据load的环节4. 核心实操PyTorch导出ONNX的逐行拆解准备工作做完下面就是完整的导出流程。这部分我会写得贴近实际场景方便你直接照着操作。4.1 基础导出脚本与动态轴配置先写一个最基本的导出脚本import torch from models import MyModel model MyModel() checkpoint torch.load(best_model.pth, map_locationcpu) model.load_state_dict(checkpoint[state_dict]) model.eval().cuda() # 构造dummy输入 dummy torch.randn(1, 3, 224, 224).cuda() # 指定动态轴第0维batch可变 dynamic_axes { input: {0: batch_size}, output: {0: batch_size} } torch.onnx.export( model, dummy, model.onnx, input_names[input], output_names[output], dynamic_axesdynamic_axes, opset_version17, do_constant_foldingTrue, )这里面有几个参数值得细讲。opset_versionONNX算子集版本。版本越高支持的算子越多但TensorRT对高版本opset的支持反而可能滞后。我遇到过比较稳妥的选择是opset 17TensorRT 8.6支持得很完整。opset太新比如20反而可能因为TensorRT还没来得及适配导致转换失败。do_constant_foldingTrue开启常量折叠。如果模型里有只依赖于常量参数的计算比如用零初始化mask矩阵这个选项会把它们在导出时预先算好减少图中的计算节点。这会让图更精简TensorRT转换速度也更快。dynamic_axes这个配置告诉你哪些维度是动态的。很多初学者不设置这个直接固定输入shape导出一个静态batch的模型然后在服务端只能接受固定大小的batch输入这种限制在实际应用中几乎不可接受。动态batch是生产系统的刚需宁可早配合不要等到后面再来补。导出成功后会生成model.onnx还可以用netron可视化工具查看计算图结构快速检查有没有奇怪的节点或多余的subgraph。4.2 ONNX的输出验证精度对比一个都不能少导出ONNX不是文件生成出来就算结束。我几乎每次导出都会立马做一遍精度对比确保模型行为没有发生变化。验证逻辑是固定的拿同一批输入数据分别跑PyTorch模型和ONNX通过OnnxRuntime跑比较输出的差异。对分类任务比较softmax输出向量的最大差值或余弦相似度。对回归/检测任务比较输出张量的绝对误差。import onnxruntime as ort import numpy as np # 用ONNX Runtime跑一次 sess ort.InferenceSession(model.onnx, providers[CUDAExecutionProvider]) inputs dummy.cpu().numpy() onnx_output sess.run([output], {input: inputs})[0] # 用PyTorch原始模型跑一次 with torch.no_grad(): pt_output model(dummy).cpu().numpy() # 比较 diff np.abs(onnx_output - pt_output) print(fmax diff: {diff.max():.6f}, mean diff: {diff.mean():.6f})这里有个经验值给你参考如果是FP32精度下正常转换mean diff应该在1e-6量级以下如果出现1e-3量级甚至更大的偏差说明模型里某处算子解析异常或精度丢了必须找到原因后再继续。不要带着未经验证的ONNX往下走到TensorRT到时候问题出在哪一环根本说不清。另外ONNX Runtime的版本更新很快建议用pip install onnxruntime-gpu并且确认安装的是CUDA版本对应的release。实在嫌麻烦直接用CPU版本做精度验证也够用只是慢一点。4.3 导出失败的经典处理技巧拆解与避免动态控制流如果导出直接报错最常见的两类问题不支持的算子比如某些PyTorch自定义C扩展。控制流循环、分支、动态索引过于复杂静态图构造不出来。针对第一种情况常见做法是“算子替换”。比如F.interpolate在某些老版本opset下会抱怨不支持antialias参数换成nn.Upsample或直接用torchvision.ops提供的替代品。针对自定义算子可以自己写一个ONNX自定义算子或在模型外面用Python包一层。针对复杂控制流我的建议简单粗暴把控制流提到模型外部用Python写只把纯张量计算的部分留下来走ONNX。当然要注意如果控制流中使用了动态shape的计算你需要让ONNX的图支持这种动态构造这又回到dynamic_axes上。5. TensorRT转换的关键实操trtexec与Python API双线走拿到ONNX文件后有两种方式转换到TensorRT。一种是官方命令行工具trtexec一种是Python API构建。我强烈建议先用trtexec做快速验证再用API构建生产用的engine分工比较明确。5.1 trtexec快速验证与测速trtexec是TensorRT官方自带的工具用起来极其方便非常适合快速判断一个ONNX能否成功转成engine以及大概性能什么水平。# 生成FP16 engine并测速 trtexec \ --onnxmodel.onnx \ --saveEnginemodel_fp16.engine \ --fp16 \ --minShapesinput:1x3x224x224 \ --optShapesinput:4x3x224x224 \ --maxShapesinput:8x3x224x224这段命令做的事加载ONNX用FP16精度构建engine同时指定动态shape的三个档位最小/最优/最大。optShapes是模型实际运行时最常见到的大小它会作为TensorRT优化时的基准值对性能影响很大认真想好再填。生成完后trtexec还会自动做一次基准测速输出延迟和吞吐。看到那个数字你对部署的预测性能就有了基本判断。如果转换失败trtexec会直接输出哪个算子不支持。你会经常看到某个layer名字后面跟着“unsupported”或者“assertion failed”。网上有无数人说把自己的模型文件发来帮忙看但说实话认真读懂报错里提到的算子名顺着代码把算子替换掉才是根本解法。5.2 Python API构建精细化控制trtexec适合快速验证但生产环境打包进服务更多是用Python API。下面是一个基础的构建脚本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.onnx, rb) as f: parser.parse(f.read()) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) # 1GB workspace # 设置动态shape profile builder.create_optimization_profile() profile.set_shape(input, (1, 3, 224, 224), (4, 3, 224, 224), (8, 3, 224, 224)) config.add_optimization_profile(profile) # 启用FP16 config.set_flag(trt.BuilderFlag.FP16) # 构建engine并序列化保存 engine builder.build_serialized_network(network, config) with open(model_fp16.engine, wb) as f: f.write(engine)这里面有几个参数值得深入说一下workspace大小。config.set_memory_pool_limit是控制TensorRT计算时临时内存的使用的。空间给得大TensorRT做算子融合时越有底气性能越好给得小可能某些优化策略因为内存不足而放弃导致engine性能不佳。这个数值单位是字节上面代码开的是1GB。如果你的显存比较紧张可以先从512MB开始试看性能变化再决定。注意不要盲目开无限大。之前我见过有把workspace调到5GB的然后部署时服务一启动显存就报警。workspace不是越大越好够用才是标准。构建engine的过程非常耗时。一个中等规模的模型可能要几分钟。在生产环境自动化构建时要对此有预期。这也是为什么工程上通常会把engine构建和推理分开构建好一次保存成文件之后直接加载就行不用每次启动都构建。5.3 Engine加载与推理上下文管理和显存复用拿到engine文件后推理端加载是有讲究的。我见过不少初学者的做法是每次调用都创建一个context用完就扔这会造成巨大的显存分配和释放开销延迟高不说在大并发场景下还容易因为显存碎片导致莫名其妙OOM。正确姿势是程序启动时加载一次engine创建固定数量的context反复复用。import tensorrt as trt import pycuda.driver as cuda logger trt.Logger(trt.Logger.WARNING) runtime trt.Runtime(logger) with open(model_fp16.engine, rb) as f: engine runtime.deserialize_cuda_engine(f.read()) # 创建context每个context维护一组中间缓冲区 context engine.create_execution_context() # 显存分配一次不要每次调用都搞 d_input cuda.mem_alloc(4 * 3 * 224 * 224 * 4) # 根据实际batch大小计算字节数 d_output cuda.mem_alloc(4 * 1000 * 4)推理时注意设置动态shape的输入大小context.set_binding_shape(0, (4, 3, 224, 224)) cuda.memcpy_htod(d_input, host_input_batch) context.execute_v2([int(d_input), int(d_output)]) cuda.memcpy_dtoh(host_output_batch, d_output)一份engine从加载到使用这个流程我记了无数次基本不可能翻车。核心逻辑就是所有可以复用的资源全部复用性能优化没捷径就是去掉所有不必要的重复工作。6. 算子融合与精度校准性能优化的两把刷子走到这一步模型已经能跑起来了接下来关心的就是性能还能不能更好精度够不够准。6.1 TensorRT算子融合与layer的调优TensorRT最核心的优化之一就是算子融合。它的工作原理是把卷积层的计算跟后面紧跟着的激活函数、批量归一化层等合并成一个算子在GPU上用一个kernel搞定减少内存读写次数。你从onnx的静态图来看可能是一堆三十多个节点的卷积块在TensorRT里可能被融合成三四个kernel就做完了这就是性能提升的一个主要来源。不过TensorRT不是全自动的它对图结构的规范程度有要求。我之前遇到过一种情况模型里某个分支在卷积后面接着一个Scale层再接一个Clip层再接一个另外一个Scale层TensorRT没办法一次融进去就在图里保留了两个独立kernel接口性能上比融合形式慢了不少。后来我把模型里这段代码改成直接用一个nn.Conv2d的bias参数包含掉Scale图一下子规整了性能马上提上来了。实操心得在你设计网络结构时如果预期将来要部署到TensorRT尽量让卷积和激活函数后面不要堆乱七八糟的数学操作能在权重初始化时算掉的就在初始化里固定下来。如果要细看一个engine里到底做了哪些融合可以打开TensorRT的日志记录功能。logger trt.Logger(trt.Logger.VERBOSE)构建时的verbose日志会详细列出layer名称、tactic选择、显存使用、kernel运行时间预估。这些信息对调优很关键而且看起来会上瘾因为能看到自己模型的每个算子最终落成了什么形态。6.2 INT8量化的评估指标与校准数据选择FP16带来的加速通常已经够惊喜了真正要压榨到极致还得上INT8。TensorRT的INT8量化需要一组校准数据来统计原始FP32精度的激活值范围然后把它映射到INT8能表达的范围。这部分的成败几乎完全取决于校准数据集的质量。有些人随便从测试集里抽100张图片做校准数据结果上线后某类特殊样例精度崩了。校准数据的核心要求是与真实线上数据分布尽量一致。它不一定要数量大但必须覆盖常见的情况和边缘情况。我个人的经验做法从真实历史请求里抽取200-500张图片覆盖不同光照、不同遮挡、不同类别的样本。用FP32 engine跑一遍保存每层激活值分布。对比INT8 engine的输出与FP32 engine的输出计算KL散度或max误差。如果某些层偏差特别大就把该层排除在量化之外单独保持FP16。config.set_flag(trt.BuilderFlag.INT8) # 设置INT8校准器 class MyEntropyCalibrator(trt.IInt8EntropyCalibrator2): def __init__(self, calibration_data_loader): super().__init__() self.loader calibration_data_loader self.buffer None def get_batch_size(self): return 4 def get_batch(self, names): batch self.loader.next_batch() if batch is None: return None self.buffer cuda.mem_alloc(batch.nbytes) cuda.memcpy_htod(self.buffer, batch) return [int(self.buffer)] def read_calibration_cache(self): return None def write_calibration_cache(self, cache): with open(calib.cache, wb) as f: f.write(cache)构建时传入校准器config.set_flag(trt.BuilderFlag.INT8) config.int8_calibrator MyEntropyCalibrator(calib_loader)一次校准完成后会生成一个calib.cache文件。之后构建engine时可以直接从cache文件读取校准信息不需要重新跑校准数据集启动速度快很多。论文里的量化方案很多但在工程实践上校准数据的选择权比量化算法本身更关键。好几次我们校准数据质量不一INT8输出的mAP掉了2-3个点换成好的校准集后基本追平了FP32水平。数据比算法更容易成为瓶颈。7. 性能调优与技术选型的完整记录到这一步我们手里的工具已经比较全了。最后分享一次实际调优的过程把整个思路串起来。这是一次图像检测模型的优化因为不涉及具体项目细节我用这个案例来展示方法论。7.1 性能瓶颈的定位顺序那天拿到一个yolo类检测模型在V100上生产者报告的原始PyTorch推理是单张18ms客户要求P99延迟小于5ms。地方摸了一遍按顺序做这几件事第一步导ONNX。按前面方法导出后ONNX Runtime跑一遍测单张发现已经降到12ms说明PyTorch的框架开销占了不少性能。第二步转FP16 engine。用了trtexec快速验证测速结果直接到4.2ms满足5ms要求了说明算子融合和精度转换带来的纯度提升非常显著。第三步尝试INT8。做了校准后测速2.3ms性能翻倍。但评估精度后发现检测mAP掉了1.8%客户对精度损失有异议于是回退到FP16方案。这个案例想说明的点在于性能优化不是一路减下去而是要带着精度评估一起看。FP16通常是无损的或接近无损INT8则要看模型冗余度。有些模型结构简单掉精度很明显就不要硬上。7.2 不同批处理策略与吞吐的实测记录性能优化中另一次重要选择是batch策略。我们用FP16 engine分别测了固定batch为1、4、8三种模式batch大小单张延迟FPS倒数值吞吐张/秒14.2ms约23841.6ms约62581.1ms约727从表里能看得出来batch越大吞吐越高但单张延迟增加。这就是个经典的取舍题。如果业务对响应时间没硬性限制尽量凑batch再发推理吞吐提升立竿见影。但要注意TensorRT的engine是按固定batch的opt profile优化过的跨batch切换时性能会有波动。如果你需要同时处理不同batch大小的请求推荐建立两个profile一个给batch1做低延迟响应一个给batch8做高吞吐批处理避免频繁切换上下文。7.3 多流推理提升GPU利用率的实践使用TensorRT我强烈建议试试多流推理。GPU的SMStreaming Multiprocessor数量不少如果一个context在等数据时SM空转就太浪费了。用CUDA stream技术可以让多个推理任务并行执行互相填充等待区间。实现方式import cuda stream cuda.Stream() # 创建多个context ctx1 engine.create_execution_context() ctx2 engine.create_execution_context() # 在不同stream上跑不同batch ctx1.execute_async_v2([d_input1, d_output1], stream1.handle) ctx2.execute_async_v2([d_input2, d_output2], stream2.handle) cuda.Stream.synchronize_all()一个容易忽略的问题是多个context共享同一个engine但每个context的内存缓冲区不能共用需要各自分配。如果你只分配了一个d_input/d_output那第二个context会用同一块显存地址数据会互相覆盖结果铁定错乱。8. 常见问题与排查技巧实录这部分直接上实践中最典型的几个问题每条都是我或我认识的人在项目里实际遇到过并解决过的。8.1 构建engine时Out Of Memory这在动态shape配置下非常常见。TensorRT构建Engine时会为每个profile分别规划内存这意味着你设的maxShapes越大构建时预留的显存就越多。解决办法很直接降低maxShapes的大小到业务允许的最大值。调小workspace限制看看是否优化策略放弃了一些但效果还够用。如果同时构建多个engine逐次构建并保存别同时放在内存里。8.2 转换时“Unsupported Layer”的应对思路看到这个报错不要慌。报错会给出unsupported layer的名字但这个名字往往是网络结构里某一层的名字不容易直接对应到代码。排查方法用netron打开ONNX图找到报错的layer名看它上下连接的是什么算子形式。回PyTorch代码里找到对应操作。常见的unsupported根源是用了比较冷门的池化方式、自定义的激活函数、张量索引、或是在模型内部用了torch.where这种带条件语义的操作。尝试把该操作拆成多个基础算子乘法、加法、截断或挪到前后处理中。8.3 推理结果与PyTorch不一致的概率与对策精度不一致是部署中最吓人的问题。一定要有一套固定流程确认ONNX阶段输出与PyTorch一致前面提到的最大差值法。转成TensorRT后你用同一份输入跑FP32 engine和FP16 engine对比结果。如果FP32一致、FP16不一致说明存在精度敏感的层。一处常被忽视的是gelu、softmax这类包含指数运算的层在FP16下数值范围有限容易精度丢失。把这类层单独拎出来在TensorRT里设置成FP32精度运行是一个可行的混合精度方案。如果是量化模型导致的不一致检查校准数据是否太偏、或者校准数据是不是长度不足。8.4 动态batch请求失败输入输出shape不匹配这类问题的报错多半是类似于“binding shape mismatch”。原因通常是设置dynamic_axes时只设置了input没有给output设置对应的dynamic axis。ONNX导出时output的shape往往会被固化成导出数值TensorRT这边的profile就会只认那个固定值。导ONNX时务必同时给input和output都设置动态维dynamic_axes { input: {0: batch}, output: {0: batch} }8.5 TensorRT引擎文件跨环境失效engine文件一般不小动辄几百MB。有人喜欢把它从一个环境拷到另一个环境直接用结果经常会失败。我一个同事反反复复试过五六次一律失败。原因在于TensorRT引擎强耦合了GPU架构、CUDA版本、TensorRT库版本换任何一种都可能导致无法加载。工程做法是在目标环境中重新构建engine不跨环境复制。如果嫌构建时间太长可以写一个构建脚本在Docker构建阶段做一次把engine放到持久化存储里后面启动服务直接加载。9. 从工程角度总结经验写到现在基本上把这套链路从头到尾捋了一遍。以我的实际体感来说ONNXTensorRT这套组合在工业界的生态地位短期内很难被其他东西取代。虽然每家大厂都有自己的推理引擎但跨框架、跨硬件的转换ONNX几乎已经是事实标准前奏TensorRT在自家硬件上又是性能标杆。两者组合起来是一条走得通、可复制、性能好到位的路。几条个人的实在建议模型结构设计阶段就要部署思维眼睛盯着性能和硬件资源别等到上线前才回头看。每一步转换都要做输出对比测试不要让任何未经验证的东西流入下一环节。构建Engine是重活在线操作风险大放到构建管道里做别搞“服务启动时现场构建”。一切优化以实际业务指标锚定不要为了显存、帧率等单个维度牺牲整体稳定性。再分享一个我自己屡试不爽的小动作把所有转换、构建、验证的代码参数比如dummy shape、opset、dynamic axes、profile数值提取成一份独立的配置文件存起来不仅让流程可复现每次排查问题也快了非常多。技术上的坑一旦趟平剩下的就是在流程上不断打磨。希望你也能在自己工程里把这条路走顺。