简介:面向视频理解与算法部署工程师的 TensorRT 部署实战资源,围绕 SlowFast 视频理解模型,解决模型计算量大、推理延迟高的问题,提供从模型导出、转换优化到推理验证的完整 Python 工程。包内共 24 个文件,以 21 个 Python 脚本为主体,覆盖 export_model_to_onnx、onnx_to_trt、tensorrt_inference 等关键阶段;YAML 文件用于定义 SlowFast 模型结构和推理配置,MD 文档对部署流程做了说明,整体仅 46KB,结构紧凑便于查阅。已有 158 人浏览学习,适合具备一定深度学习基础、正在上手 TensorRT 的算法开发或部署人员。通过实际运行和修改这些脚本,读者能够直观理解图优化、层融合、精度校准等加速原理,并可将该套流程迁移至其他视频理解或动作识别模型,提升项目落地的实时推理性能。

1. 用TensorRT部署SlowFast:从能跑到能上线,中间隔着什么

先把结论放前面:视频理解算法的上线难度,和图像识别不在一个量级。图像模型在TensorRT里往往一条命令就能转完,而SlowFast这类双路径视频模型,有两个输入、两条分支、多条横向连接,中间还有三次时间维度的特征对齐。你从GitHub跑通PyTorch版demo只算万里长征第一步,真正要上业务的是那个能在GPU上稳定跑到几十毫秒一帧的推理引擎。这篇实战笔记就围绕“TensorRT部署SlowFast视频理解算法”这条线,把模型导出、engine构建、runtime推理和踩坑记录完整过一遍,适合做动作识别、内容理解、视频审核的算法工程师或部署工程师参考。

2. 理解SlowFast的双路径结构,才能预测TensorRT的瓶颈在哪

2.1 慢路径和快路径为什么让部署不止一个难题

SlowFast的设计思路并不复杂:慢路径(slow pathway)用低帧率加宽通道,负责人物、场景、物体这类空间语义;快路径(fast pathway)用高帧率加窄通道,捕捉手势、转头、跳跃这类随时间快速变化的信息;两条路径每隔几个stage就做一次横向连接,把快路径的时间分辨率注入慢路径。模型跑起来时,两个输入张量同时进入网络,一路4帧,一路32帧,计算图从一开始就是分裂的。

部署视角下,这个设计给TensorRT带来三个麻烦。第一,两条路径的特征形状始终不一致,横向连接处有插值和3D卷积,这些算子在不同opset下的语义飘移很大。第二,时间维上的卷积核大小跟图像模型完全不同,TensorRT内核库对3D卷积的覆盖面永远比2D卷积少,遇到不支持的layout就要走通用实现,速度直接掉一个档次。第三,输入输出绑定数量是图像模型的两倍,调试的时候bindings顺序、shape对齐多一重出错面。

如果只在PyTorch里跑,这些问题全是隐性的,因为各层调用的是cuDNN的通用kernel,速度慢但都能跑。到了TensorRT,优化器逐层挑kernel并且力图融合算子,任何一个算子边缘情况超出内核库覆盖范围,构建就会失败,或者用fallback kernel守住正确性但丢掉性能。理解了这条主线,就不会在部署报错时只是一味改版本,而会沿着算子和shape的脉络排查。

2.2 TensorRT版本和GPU算力怎么配:GTX 1070要不要升10.x

部署视频理解模型之前,先查一下GPU的compute capability,再决定TensorRT主版本。这个话题最近问的人很多,常见困惑就是“TensorRT 10.x是否支持GTX 1070”。GTX 1070是Pascal架构、算力6.1,实测在TensorRT 10.x上构建引擎经常遇到UNSUPPORTED算子,能建出引擎的层也常常落到通用kernel上,性能不升反降。这不是你的代码有问题,而是TensorRT 10.x把Pascal调出了主优化列表,老卡用户继续用8.4或8.6是风险最低的选择。

| GPU架构 | compute capability | 推荐TensorRT版本 | 备注 | | Pascal(GTX 1070) | 6.1 | 8.4.x / 8.6.x | 10.x已走下优化列表 | | Turing(RTX 2080 Ti) | 7.5 | 8.6.x ~ 10.x | FP16收益明显 | | Ampere(A100 / 3090) | 8.0+ | 8.6.x / 9.x / 10.x | 部署首选 | | Ada(RTX 4090) | 8.9 | 9.x / 10.x | 新架构专用 |

建议是:训练和部署如果共用同一张卡,按卡选TensorRT;如果构建机和运行机不是同一张卡,最好在同一架构上构建,跨架构复制engine是不可靠的。有人想图省事把在3090上构建的engine拷到T4上跑,TensorRT会拒绝加载或者表现异常。这类问题的定位成本很高,因为报错信息往往只说cubin不匹配,不会告诉你该换哪一版TensorRT。

2.3 TensorRT给SlowFast提速的三个主要来源

第一个来源是层融合。PyTorch的计算图里,3D卷积、偏置、ReLU三个op分三次读写显存,TensorRT构建时会把它们融成一个kernel。以SlowFast的慢路径为例,每层stage能减少两次全局显存往返,整网十几层卷积下来,访存开销削减非常可观。这个收益不需要你做任何额外配置,构建engine时自动完成,但前提是ONNX导出的图足够干净,别带一大堆恒等节点干扰融合判断。

第二个来源是FP16精度。绝大多数消费级和专业级GPU上,FP16算力是FP32的两倍,显存占用还减半。配置合理的视频推理服务会跑到不小的batch,显存下压之后并行度能拉得更开。代价是精度,尤其是SlowFast的横向连接对时间维细节敏感,这个我放在避坑章单独讲。FP16不是开关一开就万事大吉,它需要你用真实数据做精度验收。

第三个来源是kernel autotuning。TensorRT在构建engine时会对每一层做benchmark,从kernel库中挑最快的那一个。这个机制能否发挥,前提是shape确定。工程上我强烈建议固定T/H/W三个维度,只保留batch维度动态,否则kernel搜索空间成倍扩张,构建时间可能从分钟级变成小时级。你可以在优化阶段解除限制看看效果,但线上正式engine一定用固定shape构建。

2.4 数据预处理:CPU喂帧还是GPU喂帧

很多人在这一步把“视频理解部署”做成了“视频理解部署的灾难”。SlowFast需要从视频片段里分别抽取慢路径和快路径的帧,再做缩放、归一化、排列组合,最终拼成两个四维张量。如果在Python里用OpenCV逐帧读、再逐帧resize,等待你的是GPU利用率上不去、吞吐量上不来的尴尬局面。实测中,CPU端预处理的开销经常比TensorRT推理本身还高。

我一般的做法是:解码交给PyAV,用硬件解码器把视频帧解出来;预处理从OpenCV换成GPU上的张量变换,最后一步在同一个CUDA stream里和推理串联。如果项目简单,也可以用torchvision的read_video配合Transform,虽然代码好写,但显存占用多出一截。数据喂养这块,建议在写部署代码时就列为正式环节,否则后期优化无从下手,只能靠玄学调参。

3. 把SlowFast的pt权重转成TensorRT engine:导出、构建、推理一条龙

3.1 环境版本怎么搭

先用我组合过多次的一个配置:Ubuntu 20.04、CUDA 11.8、cuDNN 8.9、TensorRT 8.6.1、PyTorch 2.0.1。这个组合出问题最少,尤其是手头有一张Pascal或Turing卡的时候,基本一路顺畅。TensorRT 10.x可以装在单独的机器上做实验,但不要贸然用于生产构建,它对新架构的优化激进,对老卡和旧opset的兼容就没那么好。

安装TensorRT时常见的坑是pip包和系统库版本错位。正确做法是:先下载NVIDIA官网的TensorRT tar包,解压到 /usr/local/tensorrt,随后把lib路径加进LD_LIBRARY_PATH,再用pip安装同版本的python wheel。装完先验证版本一致:

/usr/local/tensorrt/bin/trtexec --version

执行这条命令会打印出TensorRT版本和构建信息。如果打印出来的版本跟你pip包里的版本不一致,后面解析engine时会遇到找不到符号或加载失败,这些都是可以提前排掉的雷。另外,显卡驱动要支持CUDA 11.8,装好后用 nvidia-smi 看驱动版本,驱动太老TensorRT 8.6起不来。很多网上流传的“按教程装好却跑不起来”的案例,最后都查到了驱动和CUDA版本不匹配上。

3.2 导出ONNX:pt文件转TensorRT时最影响成败的一步

现在行业里的常见做法是把pt权重转成onnx,再由TensorRT解析onnx,直接用torch_tensorrt属于少数场景。SlowFast模型有两个输入,导出的时候要把两个输入都命名、都声明动态轴。下面这段是我项目里常用的导出脚本:

import torch
# model 是你加载了预训练权重的 SlowFast 实例
# 示例中 T=4 是慢路径帧数,T_fast=32 是快路径帧数
model.eval().cuda()

slow_input  = torch.randn(1, 3, 4, 224, 224).cuda()
fast_input  = torch.randn(1, 3, 32, 224, 224).cuda()

torch.onnx.export(
    model,
    (slow_input, fast_input),
    "slowfast.onnx",
    input_names=["slow_input", "fast_input"],
    output_names=["class_logits"],
    dynamic_axes={
        "slow_input": {0: "batch"},
        "fast_input": {0: "batch"},
        "class_logits": {0: "batch"}
    },
    opset_version=13,
    do_constant_folding=True,
    verbose=False
)

这段脚本里有三个参数值得单独说明。dynamic_axes只声明batch维度为动态,不把时间维和空间维设动态,原因前面说过,维度全动态后TensorRT的kernel搜索会失去方向,构建时间不可控。opset_version固定13,这是ONNX Resize算子语义较稳定的版本,opset 13升到16的过程中Resize的语义有过调整,容易在TensorRT解析阶段报错。do_constant_folding开启后会把BN的scale/shift折叠进卷积权重,导出图更干净,导出耗时略有增加但值得。

导出结束后建议先用onnxsim做一次图简化:

python -m onnxsim slowfast.onnx slowfast_sim.onnx

onnxsim会做常量折叠和图简化,多数情况下能删掉一批TensorRT不关心的identity节点。如果这个命令报错,不代表模型有问题,可以直接跳过简化,输出仍然可用。简化完成后用netron打开看一眼Resize节点的属性,重点确认coordinate_transformation_mode是不是asymmetric,这决定TensorRT用哪个kernel实现时间维插值。这一步做完,导出阶段才算真正闭环。

注意:onnxsim输出的文件名不要覆盖原文件,保留一份未简化版本。有些情况下简化后的图在TensorRT里能构建,推理结果却异常,保留原版便于回溯对比。

3.3 用trtexec构建engine:FP16、shape范围与内存池

拿到onnx之后,最快捷的方式是用trtexec命令行构建engine。我用的构建命令:

/usr/local/tensorrt/bin/trtexec \
    --onnx=slowfast_sim.onnx \
    --saveEngine=slowfast_fp16.engine \
    --fp16 \
    --minShapes=slow_input:1x3x4x224x224,fast_input:1x3x32x224x224 \
    --optShapes=slow_input:4x3x4x224x224,fast_input:4x3x32x224x224 \
    --maxShapes=slow_input:8x3x4x224x224,fast_input:8x3x32x224x224 \
    --memPoolSize=workspace:4096

命令里每个参数对应一类需求。--fp16表示允许TensorRT将层降到FP16精度,实际哪些层降精度由构建器决定,如果后续发现精度掉点可以用per-layer精度控制把关键层拉回FP32。--minShapes/--optShapes/--maxShapes这组参数只在动态batch场景需要,表示batch从1到8的覆盖范围,三组shapes缺一不可,TensorRT构建时会把optShapes附近的kernel选为默认。--memPoolSize=workspace:4096设置构建和运行时的工作内存上限,单位是MB,显存充裕的机器可以放宽到8192,engine构建时tactic搜索会更充分。

注意TensorRT 8.x里可以用 --workspace=4096 这种老写法,9.x之后统一改成了 --memPoolSize=workspace:4096 。两种写法在不同的版本里混用会直接报参数解析错误,所以构建脚本里建议加上版本判断。构建日志建议重定向到文件保存,TensorRT会在其中打印每一层选用的kernel名称,这在后续做per-layer精度控制时是重要的索引表。

3.4 引擎验证与Python推理

engine构建完,先用trtexec自带的性能模式跑一轮测速:

/usr/local/tensorrt/bin/trtexec \
    --loadEngine=slowfast_fp16.engine \
    --shapes=slow_input:1x3x4x224x224,fast_input:1x3x32x224x224 \
    --iterations=50 \
    --avgRuns=10

这条命令会在当前GPU上跑50次推理,取10次均值打印出延迟和吞吐。要提醒的是,trtexec的纯引擎测速不包含预处理和后处理,前端接入真实业务后延迟会高于这个数字,这个差距就是CPU喂帧的时间,所以前面说预处理要专门优化。如果发现引擎测速和PyTorch FP32的延迟差距不到一半,先别急着调batch,回去检查是不是有层走了fallback kernel。

Python侧的最小推理代码长这样:

import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
import numpy as np

# 加载engine
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
with open("slowfast_fp16.engine", "rb") as f:
    engine = runtime.deserialize_cuda_engine(f.read())
ctx = engine.create_execution_context()
ctx.set_input_shape("slow_input", (1, 3, 4, 224, 224))
ctx.set_input_shape("fast_input", (1, 3, 32, 224, 224))

# 分配显存:slow 4帧、fast 32帧、输出400类
d_slow = cuda.mem_alloc(1 * 3 * 4 * 224 * 224 * 4)
d_fast = cuda.mem_alloc(1 * 3 * 32 * 224 * 224 * 4)
d_out  = cuda.mem_alloc(1 * 400 * 4)

# 一次性拷贝输入并执行
cuda.memcpy_htod(d_slow, slow_np.ravel())
cuda.memcpy_htod(d_fast, fast_np.ravel())
ctx.execute_v2(bindings=[int(d_slow), int(d_fast), int(d_out)])

# 拷贝输出回内存
out_np = np.empty(1 * 400, dtype=np.float32)
cuda.memcpy_dtoh(out_np, d_out)

代码里有几个关键点。ctx.set_input_shape必须在第一次execute之前调用,动态shape引擎如果没有显式设置输入shape就直接执行,会按构建时默认的optShapes跑,和实际输入不一致。bindings列表的顺序必须和engine里的binding顺序一致,通常先遍历engine对象把name和index记下来,再按index传地址,直接手写顺序容易在输入输出多的模型上出错。pycuda的mem_alloc返回的是device pointer,传给execute_v2时需要转成int,tensorrt和pycuda版本更新中这个行为有变化,报类型错误时就检查这里。

到这里,pt到engine再到runtime推理的链路就闭环了。下面这些坑,是在真机上踩过一遍之后整理出来的,值得在项目里留个神。

4. 避坑:TensorRT部署SlowFast最常踩的五个坑

这一章的五条记录,是按照现场排障的顺序整理的。每一条都保留现象和解决办法,遇到报错先对号入座,比从头开始重新读日志省时间。

4.1 在GTX 1070上构建TensorRT 10.x的engine,报错UNSUPPORTED

现象:使用TensorRT 10.x加载导出的onnx或构建engine时,日志出现 [E] [TRT] unsupported op... ,而且报错的算子集中在3D卷积或Resize,换成TensorRT 8.6后同样的onnx又顺利构建。

原因:TensorRT 10.x把Pascal架构(compute capability 6.1)移出了主优化列表,部分算子的kernel实现没有为6.1编译,解析器会直接判定为不支持,而不是走通用实现。网上讨论“TensorRT 10.x是否支持GTX 1070”的帖子很多,实际结论就是:能装上不代表能好好用。

解决:在GTX 1070或同代老卡上部署,固定用TensorRT 8.4或8.6版本,不要追新。如果业务要求必须用新版本,换一台Turing或Ampere架构的机器做engine构建和运行,不要在Pascal卡上浪费排错时间。另外,跨架构拷贝engine这件事也别做,在3090上构建好的engine拷贝到T4上会报cubin不匹配,正确做法是目标机器上重新构建。

4.2 ONNX导出时interpolate节点语义变化,模型变成黑匣子

现象:同一个SlowFast权重,在A机器上导出onnx后转engine顺利,在B机器上导出后构建时报Resize相关错误,或者engine能建出来但推理结果跟PyTorch对不上。

原因:torch版本和opset版本组合不同,导致interpolate被导出为不同形态的Resize算子。opset 13之后Resize新增了多个控制属性,TensorRT对部分属性组合没有实现,构建阶段报错,或运行阶段走进通用路径。

解决:统一导出环境中的torch版本,opset固定13,并在导出后检查onnx图里Resize节点的coordinate_transformation_mode属性。如果发现不是asymmetric,在导出前把模型里的插值方式改成nearest,或者在onnx里手动修正节点属性。改模型结构是最后手段,因为重新训练或微调的成本不可控。

4.3 开启FP16后精度掉点,视频模型比图像模型更敏感

现象:FP16 engine推理出来的top-5准确率比PyTorch的FP32低3到5个百分点,部分类别的预测概率混在一起,个别动作类别直接错乱。

原因:SlowFast的横向连接会把fast路径的时间特征逐层叠加到slow路径,FP16的尾数精度在多次加法中累积误差,时间维上比空间维更容易放大。图像分类模型FP16掉点通常可以忽略,但视频理解模型对时间维差分敏感,这个误差会被横向连接放大。

解决:不追求全局FP16,改用per-layer精度策略。导出onnx时给关键层命名,构建时用TensorRT的层精度接口把head和横向连接部分的层设为FP32。实际项目中可以先全局FP16跑一遍trtexec,用日志确认哪些层被降精度,再把敏感层挑出来。做法上我一般先固定batch=1验证精度对齐,再逐步加batch,排除batch维对数值范围的影响。

4.4 动态shape的引擎测出来延迟高一倍,固定shape却很快

现象:engine在minShapes到maxShapes范围内都能跑,但延迟波动极大,batch=1时都比固定shape慢了近一倍。把时间维和空间维也设为动态后,构建时间从10分钟涨到2小时。

原因:TensorRT的kernel autotuning会为每一组shape挑选专用kernel,动态维度越多,候选kernel越泛化,最后选到的不是最优实现。固定shape时TensorRT可以在构建期确定所有张量维度和对齐方式,kernel选择空间小而精准。

解决:把T/H/W三个维度固定,只保留batch维度动态。如果业务要覆盖多分辨率输入,为每种分辨率单独构建一个engine,运行时按输入尺寸加载对应engine,比用一个万能engine靠谱得多。engines多了以后记得做懒加载,按需加载到显存,避免初始化时把显存占满。

4.5 预处理拖后腿,GPU利用率上不去

现象:推理延迟单独测只有30ms,接入完整服务后变成120ms,GPU利用率不到40%,看着nvidia-smi一头雾水。

原因:视频解码、抽帧、缩放、归一化全在CPU上串行执行,CPU端耗时超过GPU推理。SlowFast两个路径的帧数差异大,预处理要分别组织两套图像序列,比图像分类的预处理重得多。很多人把优化重点放在engine上,忽略了这个环节。

解决:解码换成PyAV并用硬件解码,预处理做成异步流水线,最后把帧数据直接放到GPU显存再进TensorRT。排查这类问题的一般顺序是:先测TensorRT引擎本身的延迟,再测接上解码和预处理的整链路延迟,两者相减就是数据管线的开销。用这个差值定位瓶颈,比直接看GPU利用率直观得多。视频理解的GPU利用率本身就比图像模型低,因为输入张量大,数据传输和预处理占用的时间比例高。

5. 进阶:三个让视频理解推理进一步变快的小技巧

前面把部署链路走通了,接下来谈三个我反复用到的小技巧,正好衔接真实业务里常遇到的吞吐问题。

第一个技巧是多batch处理。单路视频的延迟再低,qps都有天花板。把不同视频片段拼成batch一次推理,才是拉吞吐的正路。TensorRT动态batch在这里非常有用,但拼接时要保证每段视频的时间轴对齐。SlowFast的输入是固定帧数,所以拼接难度反而比图像模型低。显存允许的前提下,batch=4往往是延迟和吞吐的甜点,继续加大后显存占用上升却换不来同比例的吞吐提升,需要拿真实数据测一下曲线。

第二个技巧是在关键层上做混合精度。完全FP16和完全FP32之外,第三条路是per-layer精度控制。我建议先用FP16构建,再把横向连接和分类头这两个部件单独切回FP32,保留推理速度的同时把精度拉回可接受区间。trtexec构建日志里的层名列表是找敏感层的索引图,比逐层试错省时间得多。做这一步时一定要用带标签的验证集跑一遍对比,不能只看一两帧的结果下结论。

第三个技巧是用CUDA stream并行解码与推理。如果预处理已经搬上GPU,可以让一个stream跑TensorRT推理,另一个stream同时做下一批帧的预处理,两者之间用event同步,能抵消预处理和推理串行的等待时间。这个模式下GPU利用率能稳定跑在70%以上,之前的CPU方案跑不到这个数。如果对延迟波动敏感,还可以把解码放到独立线程,用有界队列控制喂帧节奏,避免突发流量把CPU打满。

视频理解算法上线和图像模型最大的区别是:它不止有网络,还有一套围绕时间轴的输入流水线。TensorRT能帮你把网络压到极限,但喂得进去才行。这套流程我跑过三轮之后,最深的体会是不要一上来就堆新版本,先把版本、shape、预处理三件事钉死,再谈优化。希望帮到你。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐