部署深度学习模型这件事,前几年大家聊得最多的是精度怎么提、网络怎么改,但模型真的到了上线环节,延迟压不下去、显存爆掉、帧率上不来,这些问题比训练阶段更让人头大。我这两年经手的几个推理项目,最终都落在了TensorRT上,尤其是在Ubuntu环境下的完整部署链路。TensorRT不是简单一个库,它更像是一套针对NVIDIA GPU的“性能榨干方案”,从模型解析、层融合、精度校准到运行时优化,整条链路都有讲究。
这篇就围绕“Ubuntu + TensorRT推理部署”这条主线,把从环境准备、模型转换到推理引擎构建、工程化落地的完整过程拆开揉碎。内容主要基于我自己踩过的坑和实践过的方案,适合已经能跑通PyTorch或TensorFlow训练、但还没系统搞过推理优化的开发者参考。你会看到为什么TensorRT能提速这么多、ONNX转TensorRT时哪些算子容易翻车、FP16和INT8到底怎么选、以及上线后遇到各种诡异问题该怎么排查。
1. 为什么推理部署要选TensorRT
1.1 训练框架和推理引擎的“代沟”
绝大多数人训练模型用的是PyTorch或TensorFlow,这两个框架的核心诉求是“灵活”,要支持自动求导、动态图、各种算子组合,这对研究阶段非常友好。但灵活是有代价的,框架运行时有很多额外的调度开销,算子在GPU上的执行方式也未必是最高效的。简单说,训练框架“什么都能干”,但“干什么都不够极致”。
推理部署不一样,模型结构固定了,权重固定了,输入输出的shape范围也基本确定。这时候再用训练框架那套运行时去跑,等于开着一辆房车去跑赛道,能跑但不是最优解。TensorRT做的事情就是针对固定模型做深度优化——它拿到了你的模型结构和权重之后,会做层融合、精度校准、内核自动调优、显存复用等一系列操作,生成一个高度特化的推理引擎。
我打个比方:训练框架是通用编译器,什么代码都能编译;TensorRT更像是针对特定处理器的手写汇编,只服务你那一个模型,但每一个指令都是为性能服务的。
1.2 TensorRT到底优化了什么
很多人以为TensorRT就是“换个推理后端”,跑一下就完事了。其实它的加速效果来自几个层面的叠加:
第一是层融合。GPU执行算子时有固定的启动开销,算子越小、越碎,这个开销占比越高。TensorRT会把相邻的卷积、BN、ReLU融合成一个内核,把能合并的矩阵乘法合并,显著减少kernel启动次数。实测下来,光这一项就能带来30%到50%的加速。
第二是精度校准。TensorRT支持FP16和INT8两种低精度推理。FP16直接用FP16计算,INT8则需要一个校准过程,通过校准数据集统计激活值的分布,确定每个张量的缩放因子。合适的精度策略能带来数倍吞吐提升,代价是精度轻微损失。
第三是显存优化。TensorRT会分析整个网络的张量生命周期,复用显存池,避免反复申请和释放。这项优化在显存受限的场景下尤其重要,有时候能把峰值显存压掉一半以上。
第四是内核自动调优。同一层计算在不同GPU上有不同的最优实现,TensorRT启动构建时会对候选内核做benchmark,选择实际速度最快的那个。这也是为什么TensorRT的engine文件通常和GPU型号、驱动版本强绑定的原因。
1.3 什么场景必须上TensorRT
我个人的判断标准是:如果你的服务需要支撑接近实时的响应、需要最大化GPU吞吐、或者显存刚好卡在边界,TensorRT基本是绕不开的选择。比如视频流分析、工业质检、广告推荐模型在线推理、L2级别的辅助驾驶感知,这些场景延迟每多几毫秒都会影响业务指标。
反过来,如果模型对精度极其敏感、算子特别冷门没有TensorRT支持,或者模型每次请求的动态性极强、输入shape完全不可预测,那可能就要重新评估。腾讯的TNN、阿里的MNN当然也是选择,但如果你本身用的是NVIDIA GPU,部署环境是Ubuntu,TensorRT的优化深度和生态成熟度在闭源商用方案里确实是最能打的。
2. 环境准备与版本选型
2.1 CUDA、cuDNN、TensorRT版本匹配是第一道坎
TensorRT的部署环境搭建,第一原则就是版本匹配。TensorRT不是独立运行的,它依赖CUDA runtime和cuDNN,三个版本互相之间有兼容范围。你装了TensorRT 8.6,可能就需要CUDA 11.8以上,cuDNN 8.9以上;换到TensorRT 10.x,可能又要求CUDA 12.x系列。文档里写得很清楚,但很多人不看,直接装了个最新的CUDA,结果TensorRT跑不起来,报错信息还特别隐晦。
这里我强烈建议,先确定你的GPU型号和驱动版本,然后反推CUDA版本,再根据CUDA版本选TensorRT和cuDNN。不要先装个最新的TensorRT再回头折腾CUDA,那是自找麻烦。用nvidia-smi能直接看到驱动版本和它支持的CUDA最高版本,TensorRT本身的版本要求看官方文档的Support Matrix最准。
表里列一下当前比较主流的搭配,注意这是基于我实际部署过的组合,不同阶段版本会有更新,但思路是一致的:
| TensorRT版本 | 推荐CUDA版本 | 推荐cuDNN版本 | 说明 |
|---|---|---|---|
| TensorRT 8.4 | CUDA 11.6 | cuDNN 8.4 | 老项目常用,资料多 |
| TensorRT 8.6 | CUDA 11.8 | cuDNN 8.9 | 大批量部署验证过的版本 |
| TensorRT 10.0+ | CUDA 12.0+ | cuDNN 8.9+ | 新特性多,但生态迁移要检查 |
2.2 deb安装还是tar解压
TensorRT的安装方式主要两种:通过NVIDIA官方apt源用deb包安装,或者下载tar压缩包解压配置。我两种都试过,总结下来的经验是:
deb包安装最大的好处是依赖关系自动处理,但前提是你的CUDA和cuDNN也是用deb装的,版本还得在TensorRT要求的范围内。如果依赖没对上,apt会尝试自动装一套CUDA,反而把环境搞乱。
tar包解压更灵活,适合你已经有了一套手动的CUDA环境,或者需要同时保留多个TensorRT版本做对比测试。解压之后需要手动配置环境变量,主要是LD_LIBRARY_PATH和PATH。这个方式对CI/CD流水线更友好,因为你可以在构建镜像时精确控制版本。
个人建议:如果是直接在服务器上做物理部署,用deb省事;如果是Docker容器,所有依赖都在镜像里固定,用tar包反而更干净。
2.3 环境验证:先跑通官方样例
环境装好之后,很多人上来就把自己的模型丢进去转换,然后报错一头雾水。我建议先跑TensorRT自带的样例,比如sample_onnx_mnist,这个样例流程非常完整:ONNX转engine、推理、输出结果。它能跑通,说明你的CUDA、cuDNN、TensorRT三者协作没问题,之后调试自己的模型时就能排除环境因素的干扰。
另外建议在这个阶段就把trtexec这个命令行工具用熟。trtexec是TensorRT自带的可执行文件,可以直接对ONNX模型构建engine、跑推理、测性能,不需要写一行代码。它支持指定精度、指定batch size、指定工作空间大小,还可以自动生成性能报告。我实际排查问题的时候,遇到模型转换失败或者性能不达预期,第一件事就是先用trtexec跑一遍,把问题定位在模型层面还是代码层面。
3. 模型转换全流程:从PyTorch到TensorRT
3.1 导ONNX时最常见的几个坑
PyTorch转ONNX用的是torch.onnx.export(),接口看起来简单,但里面坑不少。最典型的几个:
第一是动态轴设置。如果你的服务需要支持不同分辨率的输入,dynamic_axes必须要配好。很多人在导出时忘了设置,默认固定了输入尺寸,结果生成ONNX后,TensorRT里只要输入尺寸一变就报错。设置的时候要明确哪些维度是动态的,比如N(batch)、H和W。
第二是算子兼容性。PyTorch里有些算子导出到ONNX会走不同的分支,比如某些上采样方式、注意力机制里的reshape和permute组合,导出后可能变成一个效率极低的子图,甚至在TensorRT解析阶段直接报错。我遇到过几次Transformer结构里torch.einsum导出失败的情况,处理办法是把这些特殊算子拆成多个基础算子组合。
第三是opset版本。ONNX的算子集版本一直在演进,PyTorch的支持程度也不同。经验上,导出时用opset 13及以上比较稳妥,但也不要无脑追新,因为TensorRT对不同opset版本的支持有覆盖差异。一个适合选型的办法是:先用opset 13导出,如果遇到不支持的算子,再看看能否通过升级opset解决,而不是一上来就用opset 17。升级前先查一下目标TensorRT版本支持的ONNX算子集范围。
import torch def export_onnx(model, dummy_input, onnx_path): torch.onnx.export( model, dummy_input, onnx_path, export_params=True, opset_version=13, do_constant_folding=True, input_names=["input"], output_names=["output"], dynamic_axes={ "input": {0: "batch", 2: "height", 3: "width"}, "output": {0: "batch"} } )这段代码里dynamic_axes把batch、height、width都标成动态,是部署多尺寸输入的基本写法。do_constant_folding建议打开,它会把一些常量计算在导出时就提前算好,减小模型体积也减少推理时的计算量。
3.2 ONNX Simplifier:把图洗干净再喂给TensorRT
即便你的ONNX能成功导出来,也不建议直接拿去转TensorRT。PyTorch导出的计算图往往有很多冗余结构,比如没用的Identity节点、多余的Transpose、可以提前融合的常量折返,这些会干扰TensorRT的优化器。
我在这个环节会先跑一遍onnx-simplifier,这个工具会做常量折叠、冗余节点消除、算子融合等工作,把计算图“洗干净”。实际经验是,有些模型raw ONNX直接转TensorRT会报“unsupported operator”错误,但经过simplify之后反而能正常转换,原因就是某些复杂的算子组合被化简成了TensorRT支持的常规结构。
安装和使用非常简单:
pip install onnx-simplifier python -m onnxsim input.onnx output_sim.onnx跑完后建议用netron可视化对比一下simplify前后的计算图,你会直观看到很多节点被消掉了。这一步对后续构建效率和推理性能都有帮助。
3.3 构建TensorRT引擎时该关注哪些配置
ONNX准备干净之后,就开始构建TensorRT引擎。代码层面,核心几个配置项:
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, 2 << 30) # 2GB workspace config.set_flag(trt.BuilderFlag.FP16) # 启用FP16 config.set_flag(trt.BuilderFlag.OBEY_PRECISION_CONSTRAINTS) # 通过network定义input shape profile = builder.create_optimization_profile() input_tensor = network.get_input(0) profile.set_shape(input_tensor.name, (1, 3, 360, 640), (4, 3, 1080, 1920), (8, 3, 1080, 1920)) config.add_optimization_profile(profile) engine = builder.build_serialized_network(network, config)这里有个很重要的概念要理解:optimization_profile定义的是输入shape的min、opt、max三档。min是最小输入,max是最大输入,opt是TensorRT做内核选择时的基准shape。很多人在这一步会犯错,直接把min和max设置成一样,那等于放弃了动态shape的灵活性,或者optshape选的不好,导致实际推理时多数shape都不是最优内核,性能反而下降。
从经验来说,如果你要支持多种分辨率,optshape应该设成最常出现的那一档,然后用min和max包住所有可能性。需要注意,profile的几个值定义了一个范围,但TensorRT不会保证范围里的每个shape都是性能最优,这是动态shape方案的固有取舍。
3.4 FP16和INT8:精度与性能的权衡
TensorRT里性能提升最明显的是低精度推理,但这一步也是很多人犹豫的地方。FP16的加速在大多数GPU上是立竿见影的,特别是像Ampere架构之后的GPU,FP16的计算吞吐往往比FP32翻倍。但INT8需要做校准,操作复杂度高一些。
FP16要注意的问题是精度溢出。当网络里有特别大或特别小的数值时,FP16的动态范围可能不够,表现为推理结果出现NaN或明显的精度异常。常见解决办法是给敏感层单独设置精度,不让它用FP16计算。
INT8则更麻烦一点,已经不是简单设置一个flag的问题了,而是要用一个校准数据集去统计activation的分布,然后确定量化参数。校准数据集必须和真实业务数据在分布上保持一致,不然量化误差会集中在某些区间里爆发。实习生经常犯的错误就是随便抽了几百张训练集图片做校准,然后线上效果崩了。
这块我的建议是:项目初期先跑FP16,拿到稳定收益后再考虑INT8。FP16几乎是免费的性能提升,INT8则需要你在精度和速度之间做精细平衡。INT8的收益主要在显存带宽受限的场景,如果你的GPU利用率已经很高、瓶颈在网络计算,INT8带来的提升未必有想象中大。
4. 推理代码工程化:别让后处理拖垮整体延迟
4.1 引擎加载与序列化的正确姿势
TensorRT引擎构建完成后,有两种使用方式:直接在内存里build后使用,或者把engine序列化成文件,下次启动直接反序列化加载。生产环境几乎必然选择后者,因为build过程非常耗时,大型模型可能需要几分钟到十几分钟,不可能每次启动都重新构建。
引擎文件需要妥善管理。它跟GPU型号、TensorRT版本、CUDA版本是强绑定的,换任何一个环境都要重新build。这也是很多人换机器部署时最崩溃的地方——明明代码没变,engine加载就是报错或者性能下降。
加载engine的核心代码如下:
def load_engine(engine_path): with open(engine_path, "rb") as f: engine_data = f.read() runtime = trt.Runtime(logger) return runtime.deserialize_cuda_engine(engine_data)deserialize_cuda_engine本质上是在反序列化一个已经优化好的计算图及其内核选择结果。序列化文件本身不跨环境兼容,这是TensorRT的已知限制,不能靠“复制粘贴大法”跨GPU迁移。
4.2 context与显存绑定
有了engine之后,还要创建execution_context。这里必须理解context的用法——它的核心作用是为推理提供独立的执行状态。TensorRT 8以后,显存分配可以通过set_tensor_address绑定到我们自己管理的内存,而不是让框架内部自动分配。这意味着你可以预先申请好输入输出缓冲区,推理时反复复用,减少显存抖动。
context = engine.create_execution_context() context.set_input_shape("input", (batch, 3, h, w)) # 预先分配显存 d_input = cuda.mem_alloc(input_size) d_output = cuda.mem_alloc(output_size) context.set_tensor_address("input", int(d_input)) context.set_tensor_address("output", int(d_output))对于batch推理,如果你使用固定batch大小,这个步骤相对简单。但如果你要动态batch,就需要额外小心input shape的设置时机,必须在每次推理前根据当下实际batch重新调用set_input_shape。
4.3 多线程与多流的推理并发
推理服务通常需要同时处理多个请求。最简单的并发方式是开多个线程,每个线程持有自己的context和CUDA stream,互不干扰。但要注意,context本身不是线程安全的,每个线程必须有自己独立的context实例。
在GPU资源允许的情况下,每个线程一个context是最稳妥的方案。如果需要更高的并发密度,可以考虑单context多stream的方式,让多个推理任务在同一个context上并发,由CUDA stream调度。但这种方式对模型的执行顺序、显存复用都有更严格的要求,不适合新手直接上手。
我用过的稳定模式是线程池加context池:预先创建N个context,接请求后从池里取一个空闲context执行推理。池的大小取决于GPU显存和推理延迟,需要线上压测来确定。当你用动态shape时,context池还需要多注意一点:同一个context在不同shape间切换会产生额外开销,所以按常用shape分组管理context是更好的工程做法。
4.4 前处理和后处理同样是延迟的一部分
很多人做过一个错误判断:只看模型推理时间,以为那么快就够了。但完整链路里,图像解码、缩放、归一化、通道转换、后处理NMS这些操作如果都在CPU上做,前处理就可能吃掉10到20毫秒,后处理再吃几毫秒,模型推理的几十毫秒优势就被稀释了。
前处理优化的方向是尽量落到GPU上。用torchvision.transforms的GPU版本或者自己写CUDA核函数,把resize和normalize放进GPU流水线。OpenCV的图像解码目前主要还是CPU,但解码后的前置处理可以交给GPU。后处理里的NMS也有GPU实现,或者用TensorRT自带的EfficientNMS插件,这些都是官方支持的特性。
判断性能瓶颈时,不要只看模型的单次推理耗时。把一次完整的请求从进入到返回切段计时,通常你会发现“意外”的时间消耗都花在CPU和GPU之间的数据拷贝上。每秒钟处理多少帧才是部署性能的唯一标准。
5. 动态shape方案规划
5.1 动态shape的三种层级
不少场景要求模型支持不同尺寸的输入。TensorRT里的动态shape分为几种情况:动态batch、动态宽高、完全动态。这三种的复杂度和性能代价是递增的。
动态batch最简单,只要在optimization profile里把batch维度设成动态即可。动态宽高稍微复杂,特别是当你的模型里包含需要固定shape的算子时,比如某些Transformer的位置编码表。完全动态不推荐,因为TensorRT的优化依赖shape的形状信息,越动态,优化空间越小,还可能触发一些kernel重新选择的开销。
如果你能控制业务场景,我建议优先固定输入尺寸,把动态shape作为备选方案。同一模型,固定shape的engine在性能上通常比动态shape的engine快10%左右,调试也更省心。
5.2 显存上限与batch size的关系
动态batch的方案里,max batch size直接决定了显存申请的峰值。TensorRT在构建engine时,会根据最大shape预分配足够的显存池,你max定得越大,显存占用就越高。这个机制很关键——很多人在max batch设成32后,发现显存占用比固定batch 8的模型高出一大截,原因就在这里。
所以设置max batch要按真实需求来,不要给“反正不用白不用”的心理留太多余量。如果线上大多数请求都是小batch,少数高峰请求是大batch,可以考虑用两个engine:一个固定小batch应对常态流量,一个大batch应对高峰。缺点是工程复杂度增加,但显存利用率和性能会更理想。
5.3 一个常见场景:视频流批处理
我做过一个视频流分析项目,需要同时处理多路RTSP流,每一路流的分辨率还不一样。最初的方案是每路流一个独立engine,显存很快就顶不住了。后来改成动态shape方案,所有流共享一个engine,batch维度是总路数,宽高维度用最大的那个分辨率,小分辨率的流通过padding补齐。
这个方案的代价是GPU计算量略有浪费,因为小分辨率也会按大分辨率的计算量跑。但无论如何,单靠这种方式,显存比多engine方案降了接近一半,整体吞吐反而因为batch利用率提高而上升了。如果你的场景类似,就绕不开动态shape,而一旦用了动态shape,就必须接受内核选择在非最优shape下略有折损的现实。
6. 精度对齐与性能调优
6.1 必做的回归测试:输出数值对齐
换了TensorRT之后,第一步永远不是看速度快了多少,而是确认输出结果和原来的模型基本一致。我通常会做两件事:一是用一批固定输入分别跑原模型和TensorRT engine,保存输出,对比绝对误差和相对误差;二是在业务层面定义好“可接受精度差异”的阈值,比如检测任务的mAP下降不超过1个点,或者分类任务的top-1准确率浮动在0.5%以内。
当开启FP16或INT8后,输出差异一定会出现。关键是差异不能过大。一般的经验规则是:FP16正常差异在1e-2级别,INT8在1e-1级别,具体取决于模型和数据分布。如果你的模型开启FP16后出现了大面积NaN,那就是数值溢出了,需要单独处理敏感层。
6.2 性能瓶颈的分析方法
性能不达标时,不要凭感觉猜,用工具量化。
先用nvidia-smi看GPU利用率。如果你发现GPU利用率只有20%,但推理延迟还是很高,那大概率是数据传输或者CPU端处理拖了后腿,而不是模型算得慢。接着可以用nsys或ncu做更细粒度的分析,查看kernel执行时间、显存拷贝时间、CPU启动等待时间等等。
还有一个容易被忽略的指标是PCIe传输。如果你的输入数据每次都在CPU与GPU之间大规模拷贝,这个时间在总延迟里的占比会很明显。尤其是在视频流场景下,每帧图像都要从CPU内存拷贝到GPU显存,累积起来是很大的开销。
6.3 实际项目中性能调优的几个有效操作
调优手段按性价比排序,我个人的经验大致是:
第一,确保输入输出缓冲区的复用。不要在推理循环里反复申请释放显存,这是最基础的优化。
第二,把多个小请求拼成batch。GPU在batch较大时效率提升显著,但要控制batch增大带来的延迟增长,避免一条请求被其他请求拖住。
第三,启用CUDA Graph。对于固定shape的模型,CUDA Graph可以把整个推理过程录制下来,再次执行时大幅降低kernel启动的开销。实测在某些模型上,这个优化能让端到端延迟再降10%到20%。
第四,使用异步推理接口。如果业务允许“提交请求后先干别的事,稍后取结果”,那就用异步方式把CPU等待GPU的时间藏起来。
7. 上线部署与运维经验
7.1 Triton还是自建服务
腾讯云、阿里云都有托管的推理服务,但它们底层的机制都值得了解。自建方案主要分两种:自己写一个推理服务封装TensorRT,或者用NVIDIA Triton Inference Server。
自建服务的优势是完全可控、依赖少、调试直观,适合业务逻辑简单的场景。但一旦需要多模型管理、动态batch、模型热加载、请求并发调度这些能力,自建的代码量会迅速膨胀,这时候Triton就更有吸引力。
Triton内置了大量工程能力:动态batch、并发模型加载、模型版本管理、支持多种后端(包括TensorRT)。它还有一套成熟的metrics接口,可以直接对接监控系统。缺点是多了一层学习和运维成本,而且Triton本身也是要求你理解TensorRT细节的。
两种方案我都用过,我的建议是:如果只是单机部署、模型固定、请求量可控,自建一个轻量HTTP服务完全够用;如果要做多模型服务、弹性扩缩容、频繁模型更新,直接上Triton。
7.2 模型更新策略
上线后一定会遇到模型迭代的情况。TensorRT的engine文件跟模型权重强绑定,更新权重必须重新build engine。如果每次更新都在线上临时build,抢占了GPU资源不说,万一build失败还会影响线上服务。
我的做法是做一个独立的构建任务:模型权重更新后,先在构建机上build engine,然后推到模型仓库,再通过Triton的热加载或者自建服务的优雅重启完成切换。构建机不需要和线上机器一模一样,但GPU型号必须一致,TensorRT和CUDA版本也要完全一致,否则构建出的engine在线上跑不起来。
7.3 监控指标与告警
部署上线只是开始,最重要的是建立监控。除了常规的CPU、内存、GPU利用率外,还要监控这几项:推理延迟的P99值、每秒推理次数、engine加载失败率、显存分配失败次数。
延迟监控别只看平均值,平均值会被少量快速请求拉低,P99才是用户体验的真实反映。如果你发现P99明显高于P50,说明有一部分请求被排队或调度拖慢了,大概率是并发处理能力到了瓶颈。
显存分配失败是另一个要盯紧的指标。TensorRT构建engine时会锁定一块显存池,如果和其他进程共用GPU,显存池可能被挤压,导致推理时报显存不足。上线之前就要确定好GPU的分配方式——是独占一张卡还是和其他任务共享,共享的话显存隔离要靠MPS或MIG这类机制。
8. 常见问题与排查技巧
8.1 构建引擎时报“Unsupported Operator”
这是转TensorRT时最常见的报错。处理思路是:先定位图里哪个算子不支持,再考虑替换或拆解。
排查步骤我建议这样走:先用trtexec跑原始ONNX,把报错信息里的节点名记下来,然后用netron找到这个节点对应的网络层。如果是某个不常见的激活函数、特殊的池化方式、或者某次upsample的align方式不同,一般都能通过修改模型实现等价替换。还有一种情况是算子实际被支持,但输入数据的维度或类型不匹配,比如某个层要求4D输入但你的tensor是3D的,这就要回溯前面的reshape逻辑。
替换算子的原则是:找到功能等价但TensorRT原生支持的结构。比如F.grid_sample在旧版TensorRT不支持,可以换成用F.affine_grid加F.interpolate组合实现。有时候变换后精度会有微小变化,需要结合回归测试确认。
8.2 推理结果出现NaN或Inf
FP16开启后NaN问题,多数是因为激活值或者中间结果超过了FP16的最大表示范围(65504)。定位方法是用ONNX Runtime在FP32模式下跑一遍,确认原始模型无误,然后逐个层排查。TensorRT里可以对特定层指定精度,把这个层单独切成FP32计算。
处理办法是把网络里容易产生大数值的区域找出来。常见的隐患是attention里softmax之前的logits,矩阵乘法数值稍大就可能溢出。实测过几次后,我的经验是:检测、分割这类模型的FP16精度风险点通常在分割头的输出和loss相关层,而不在backbone;Transformer结构则要重点检查attention block。
8.3 动态shape推理报“Invalid Shape”
这个报错一般发生在set_input_shape时,输入的shape不在optimization profile设定的范围内。检查点有三处:profile的min和max是否包含了所有可能性、当前推理shape是否落在这个区间内、opt档是否设置合理。如果模型有多个输入,每个输入的profile都要单独确认。
8.4 engine加载慢或构建时间过长
构建时间过长可能的原因:网络结构太深、shape范围太大、开启了AutoTT、或者没设置合理的工作空间大小。config.set_memory_pool_limit设得越大,TensorRT做内核选择时的搜索空间就越大,构建时间就越长,但性能未必线性提升。
实际工作中,在构建时间和推理性能之间要找到平衡。比如先用小一点的workspace快速构建,测试性能是否达标,再尝试增大workspace看有没有额外收益。不要一上来就无脑设最大。
8.5 推理延迟浮动大
延迟浮动通常和GPU上是否有其他任务抢占有关,也可能是显存碎片化导致分配变慢。先查其他进程的GPU占用,再查推理服务自身的显存申请频次。如果你在推理循环里频繁调用cuda.mem_alloc,哪怕量不大,波动也会很明显。这里就是缓冲区复用发挥价值的地方。
9. 工具链与工作流沉淀
9.1 用好trtexec这个“瑞士军刀”
再次强调trtexec的价值:模型转换、精度验证、性能测试、参数扫描,它都能做。一个典型的性能测试命令:
trtexec --onnx=model_sim.onnx \ --saveEngine=model.engine \ --fp16 \ --minShapes=input:1x3x360x640 \ --optShapes=input:4x3x1080x1920 \ --maxShapes=input:8x3x1080x1920 \ --workspace=2048这个命令会直接告诉你构建耗时、平均推理耗时、显存占用等关键指标。在调优阶段,我会用不同optShapes和workspace组合跑几遍,用数据说话而不是靠感觉。
9.2 搭建一个可复用的转换流水线
项目做多了之后,我沉淀了一套固定的转换流程,每次新模型接入只需要跑一套脚本:
- PyTorch导出ONNX,设置好动态轴和opset。
- 用
onnx-simplifier清洗计算图。 - 在
netron里人工检查关键节点,确认没有意外的结构。 - 用
trtexec快速构建并测试,确认转换成功和性能基线。 - 在代码里接入TensorRT runtime,做精度回归对比。
- 跑一轮完整业务链路测试,确认端到端指标达标。
这套流程最大的意义是减少“出问题才去排查”的被动状态。每个环节都有明确的输出物和检查点,出问题时能快速判断是哪一步出了问题。
9.3 Docker镜像固化环境
环境一致性是部署稳定性的大前提。我会把CUDA、cuDNN、TensorRT以及所有Python依赖全部打进Docker镜像,确保开发、测试、生产三套环境完全一致。镜像tag里标明TensorRT版本和GPU架构信息,这样回滚也方便。
Docker方式还便于在构建机上批量build engine,然后通过共享存储同步到推理节点。我自己常用的做法是构建机和推理机分开,构建机负责跑模型转换和engine生成,推理机只做加载和服务,职责清晰、故障隔离。
10. 最后再聊几句实操体会
整个TensorRT部署链路走下来,最深的体会是:这个工具的性能上限确实高,但每一分性能都需要对应的认知水平去换。版本匹配、精度配置、shape规划、并发调度,任何一个环节偷懒,都会在上线后以诡异的方式反噬。
给刚接触TensorRT的同学一个具体建议:不要直接上手自己最复杂的业务模型,先拿一个公开的小模型(比如ResNet-18或者YOLOv8s)完整走一遍流程——目标不是跑通,而是把每个环节的参数都亲手改一遍、感受一遍差异。比如把workspace从1GB改成2GB,看看构建时间和推理延迟分别怎么变;把opt shape改掉,再用偏离它的shape跑一版,感觉一下性能抖动。有了这些基础手感,再去碰业务模型,遇到问题时你大概能猜到问题出在哪个环节,而不是一头雾水。这种“先小后大”的做法,是我认为学习TensorRT最快也最稳妥的路径。