简介:一套面向算法部署工程师与视频理解开发者的完整实战源码,聚焦如何用TensorRT对SlowFast模型做推理加速与落地部署。资源解决了视频理解模型参数量大、推理延迟高、难以实时处理的实际问题,覆盖从ONNX导出、模型转换、图优化到目标硬件适配的整个流程。包体结构精简,共24个文件,含21个Python脚本、1个YAML配置、1个Markdown说明及.gitignore,压缩包仅46KB,便于快速阅读与复用;其中脚本包括模型转换、TensorRT推理等核心模块,配置文件可用于指定SlowFast网络结构。目前已有158人学习下载,适合具备一定深度学习基础、希望掌握TensorRT部署技能的开发者。通过实际操作可理解层融合、精度校准、内核自动调优等优化手段,并直接获得可用于工业级视频理解项目的代码框架,具有较高的工程参考价值。
1. 算法部署的最后一公里:TensorRT 跑 SlowFast 视频理解,快的不只是推理
SlowFast 论文指标好看,但把它从 PyTorch 权重变成线上能用的推理服务,这段算法部署的路比想象中难走。4×16、ResNet-50 这种标准配置,一个 clip 要同时过 slow 和 fast 两条路径,FP32 下在消费级显卡上跑一次前向动辄几百毫秒,放到视频理解、动作识别这种流式场景根本顶不住。这个项目解决的就是这最后一公里:把训练好的 SlowFast 导出 ONNX,再交给 TensorRT 做图优化、层融合和 kernel 自动调优,最后用统一的推理脚本和 yaml 配置把整条链路串起来。包里 export_model_to_onnx.py、onnx_to_trt.py、tensorrt_inference.py 三件套加 configs 目录,适合手上有 SlowFast 权重、想在 NVIDIA GPU 上做实时视频理解的算法工程师和部署工程师。读完这套东西,你对 TensorRT 的动态 shape、精度取舍和常见坑会有一次完整的实感。
2. 导出 ONNX:SlowFast 双路径结构下的模型转换与参数陷阱
2.1 部署前先看清 SlowFast 的两条输入路径
SlowFast 不是一个单输入单输出的普通 CNN。它有两个并行分支:slow pathway 处理低帧率、高通道数的帧序列,负责语义和外观;fast pathway 处理高帧率、低通道数的帧序列,负责运动细节。两条分支中间通过 lateral connection 做特征融合,最后统一进分类头。这套设计在精度上很有效,但到了部署环节,直接变成两个硬约束:模型 forward 的入参是两个五维张量,shape 是 (N, C, T, H, W),不是一张图;导出 ONNX 时两个输入必须都占位,漏掉任何一条路径,导出的计算图都不完整。
项目里 configs/SLOWFAST_4x16_R50_inference.yaml 把采样配置写得很清楚。4×16 的常见含义是:slow path 在时间轴上以步长 16 取 4 帧关键主帧,fast path 以 4 倍帧率密度取 32 帧,两条路径覆盖同一段视频 clip 的时间跨度。这两个数字直接决定导出时的输入张量维度,也决定后面 TensorRT 的 optimization profile 怎么填,属于全链路第一个要对齐的参数。R50 表示主干是 ResNet-50,slow path 的 stem 输出 64 通道,fast path 通道数按比例压缩(β 通常取 1/8),stem 输出 8 通道。注意,输入到模型里的原始张量还是 3 通道 RGB,stem 里的 Conv3d 负责把 3 变成 64 或 8。有人导出时图省事,直接拿 stem 输出通道当输入通道填,导出的图里多一层无意义的变换,后面推理时索引全是乱的。
这也是项目里 transform_model.py 和 export_model_to_onnx.py 分开两个脚本的原因。transform_model.py 干的是权重层面的对齐:把 PySlowFast 官方预训练权重或者自训 checkpoint 的 state dict key,映射到本地 slowfast/models 下定义的模型结构,顺带把 BN 固定在 eval 统计量上。这一步不做,load_state_dict 会报一堆 missing/unexpected key;就算硬导出,加载的权重没对齐,推理结果也是垃圾。我见过有人跳过这步直接导 ONNX,最后花两天排查一个「模型为什么所有输入都输出同一个类别」的问题,其实就是权重 key 没对齐。
2.2 export_model_to_onnx.py 的完整导出流程
导出脚本的核心代码不长,但每个参数都影响后面 TensorRT 能不能顺利解析。下面这段就是 export_model_to_onnx.py 的主流程,按 PyTorch 的标准 torch.onnx.export 写法展开:
import torch from slowfast.models import build_model from slowfast.utils import load_config, load_checkpoint # 1. 从 yaml 构建模型结构,加载 transform_model.py 对齐过的权重 cfg = load_config("configs/SLOWFAST_4x16_R50_inference.yaml") model = build_model(cfg) state_dict = torch.load("slowfast_4x16_r50.pth", map_location="cpu") model.load_state_dict(state_dict, strict=False) model.eval() # 2. 两条路径各占一个 dummy 输入,布局为 (N, C, T, H, W) slow_dummy = torch.randn(1, 3, 4, 224, 224) # slow path: 4 帧 fast_dummy = torch.randn(1, 3, 32, 224, 224) # fast path: 32 帧 # 3. 导出 ONNX,只把 batch 维度设成动态 torch.onnx.export( model, (slow_dummy, fast_dummy), "slowfast.onnx", opset_version=13, input_names=["slow_input", "fast_input"], output_names=["logits"], dynamic_axes={ "slow_input": {0: "batch"}, "fast_input": {0: "batch"}, "logits": {0: "batch"}, }, do_constant_folding=True, )逻辑说明:第一步先按 yaml 配置构建模型,再加载权重。strict=False 是因为 transform_model.py 已经处理过大部分 key 对齐,剩余的 missing/unexpected 通常是 Dropout 这类推理时无用的层;如果你不放心,可以先 strict=True 跑一遍,看看到底差哪些 key,再决定要不要补。第二步的 dummy 张量,T 维度必须和 yaml 里的采样帧数严格一致——4×16 配置下 slow 是 4、fast 是 32;你把 fast 填成 16,ONNX 图能导出来,但后面 TensorRT profile 和预处理代码全都要跟着改,属于部署里最怕的「静默不一致」。第三步 dynamic_axes 只动 batch,H/W/T 全部固定。
参数说明:opset_version 试过 13 和 11 都行,TensorRT 8.x 的 OnnxParser 对 opset 11~13 支持最稳,14 以上某些算子解析会有意外。要是转 engine 时报 unsupported operator,先把 opset 降到 11 再导一次,多半能过。do_constant_folding=True 是默认建议开着的,它会把权重里的常量计算先折叠掉,减小 ONNX 体积,也能减少 TensorRT 解析时的节点数。
2.3 导出后第一件事:用 Netron 和 onnxruntime 验收
导出完别急着转 TensorRT,先做一次验收。我一般固定两步:第一步用 Netron 打开 slowfast.onnx,确认输入节点名字是 slow_input 和 fast_input,各自的 shape 分别是 (batch, 3, 4, 224, 224) 和 (batch, 3, 32, 224, 224)。顺便看第一个 Conv3d 的权重 shape——slow path 的第一个卷积核是 64 组,fast path 是 8 组。如果两个输入连到的卷积组数反了,说明 export 时传参顺序和模型 forward 签名不一致。
第二步用 onnxruntime 跑一次 CPU 推理,和 PyTorch 原始模型对比输出。这一条能拦住绝大多数导出阶段的问题,做法是:同一份随机输入,先跑 PyTorch 得到 logits,再跑 ONNX Runtime 得到 logits,两者最大绝对差压在 1e-4 以内才算过。超过这个量级就要回头查权重对齐和 BN 状态。
import onnxruntime as ort import numpy as np ort_session = ort.InferenceSession("slowfast.onnx", providers=["CPUExecutionProvider"]) slow_in = np.random.rand(1, 3, 4, 224, 224).astype(np.float32) fast_in = np.random.rand(1, 3, 32, 224, 224).astype(np.float32) ort_out = ort_session.run( ["logits"], {"slow_input": slow_in, "fast_input": fast_in}, )[0] # 和 PyTorch 的 model(slow_t, fast_t) 输出做 allclose 校验,max abs diff < 1e-4这一步的价值在于:ONNX 本身就是个中间产物,如果在这里就坏了,后面 TensorRT 的报错会让人误以为是 TensorRT 的问题,实际锅在导出。先验收 ONNX,等于给整条链路设了一个干净的基线。
3. ONNX 转 TensorRT 引擎:onnx_to_trt.py 的构建选项与精度取舍
3.1 TensorRT 靠什么把 SlowFast 变快
先建立预期:TensorRT 不是通用的模型加速魔改,它做的是把计算图画成一张针对具体 GPU 的调度表。核心优化有三块。第一是层融合,把 Conv+BN+ReLU 这类串行算子合并成一个 kernel,减少 kernel launch 和显存读写。对 SlowFast 这种 Conv3d 密集、BN 多、残差连接一堆的网络,融合收益尤其明显,单单 Conv+BN+ReLU 融合就能把 kernel 数量砍掉三分之一。第二是 kernel 自动调优,同一个算子按 GPU 的 SM 数量、寄存器预算、共享内存大小去试多个实现,跑得快的那版留下。第三是显存复用,给中间张量做生命周期分析,让不同时存在的 tensor 共用同一块显存,整体 footprint 降下来。
项目里 onnx_to_trt.py 就是触发这三块优化的入口。它读入上一章的 slowfast.onnx,用 OnnxParser 把计算图解析成 network,再交给 builder 构建 engine。下面这段按 TensorRT 8/10 的 Python API 写,是构建脚本的核心:
import tensorrt as trt logger = trt.Logger(trt.Logger.INFO) builder = trt.Builder(logger) # 显式 batch 模式,ONNX 里的动态维度要在这里显式声明 network = builder.create_network( 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) ) parser = trt.OnnxParser(network, logger) with open("slowfast.onnx", "rb") as f: if not parser.parse(f.read()): for i in range(parser.num_errors): print(parser.get_error(i)) raise RuntimeError("ONNX parse failed") config = builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 2 << 30) if args.fp16: config.set_flag(trt.BuilderFlag.FP16) # 动态 batch 三档 shape:min / opt / max profile = builder.create_optimization_profile() profile.set_shape( "slow_input", (1, 3, 4, 224, 224), (4, 3, 4, 224, 224), (8, 3, 4, 224, 224), ) profile.set_shape( "fast_input", (1, 3, 32, 224, 224), (4, 3, 32, 224, 224), (8, 3, 32, 224, 224), ) config.add_optimization_profile(profile) engine_bytes = builder.build_serialized_network(network, config) with open("slowfast_fp16.trt", "wb") as f: f.write(engine_bytes)逻辑说明:EXPLICIT_BATCH 是必须的,ONNX 里有 dynamic_axes 的输入,不加这个 flag,TensorRT 会把 batch 维度也当成网络结构的一部分,构建直接失败。profile.set_shape 的三档参数,min 是最小允许 shape,opt 是 kernel 优选时实际参考的 shape,max 是上限;推理时只要 shape 落在区间内都能跑,但性能按 opt 那档优化。如果你的线上 batch 固定是 1,直接三档全写 (1, ...),TensorRT 可以做得更激进。
参数说明:set_memory_pool_limit 里的 workspace 是构建时允许 TensorRT 试错的上限,不是推理时固定占用的显存。给得越大,kernel 优选越激进,越可能挖出更快的实现,代价是构建时间变长。SlowFast R50 这个体量,我一般给 2GB 起步;显存小的卡降到 1GB 也能跑,只是性能会打折。另外注意 API 差异:TensorRT 8 以上用 build_serialized_network,7.x 是 build_engine 加 engine.serialize(),换版本时这两行要跟着改。
3.2 FP32、FP16 还是 INT8:精度取舍别拍脑袋
TensorRT 的精度档位是部署时最纠结的地方。先说结论:SlowFast 4×16 R50 在 FP16 下基本不掉点,Top-1 掉 0.5 以内很常见,在 Turing、Ampere 这些架构的卡上没有理由不用 FP16;INT8 需要额外做校准,收益对视频理解这种连续帧场景没那么突出,而且校准集选不好会在特定片段上出现明显误判。这个项目里没有给 INT8 校准脚本,我建议第一版直接 FP16 上线,把精度对比和校准作为二期优化。
| 精度档位 | 加速收益 | 精度影响 | 适用场景 |
|---|---|---|---|
| FP32 | 基准 | 无 | 冷启动验证、精度对照 |
| FP16 | 1.5~3× | Top-1 掉 0.5 以内 | 默认选择,Turing 以上强烈推荐 |
| INT8 | 3~5× | 依赖校准集质量 | 数据分布稳定、有足够代表视频时 |
判断精度能不能接受,别靠感觉,直接用 trtexec 做同输入对比。构建和测试一条命令完成:
trtexec --onnx=slowfast.onnx --fp16 \ --shapes=slow_input:1x3x4x224x224,fast_input:1x3x32x224x224 \ --saveEngine=slowfast_fp16.trt跑完看输出的 mean GPU time 和 end-to-end latency,再在推理脚本里用同一段视频分别跑 FP32 和 FP16 engine,对比 logits。如果你的卡是 GTX 1070 这种 Pascal 架构,情况会特殊一点:Pascal 的半精度存储减半,但计算单元的 FP16 吞吐并没有 Turing 之后那种双倍收益,实测经常只比 FP32 快 20% 左右。为了这点收益去接受精度损耗,不划算,这种老卡老老实实跑 FP32 就好。
3.3 引擎序列化:构建时间不应该是每次启动的代价
build_serialized_network 这一步出了名的慢。SlowFast R50 在 FP16 加动态 profile 下,一次完整构建从几分钟到十几分钟都见过,取决于显卡和 workspace 上限。生产环境的标准做法是构建一次、落盘、以后直接反序列化。
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING)) with open("slowfast_fp16.trt", "rb") as f: engine = runtime.deserialize_cuda_engine(f.read())这里有个硬约束必须知道:TensorRT 引擎和构建它的 TensorRT 库版本、GPU 型号强绑定。同一个 .trt 文件换一张卡,或者升级 TensorRT 版本,反序列化大概率直接抛错。项目 readme 里一般会写清楚「在哪张卡、哪个 TensorRT 版本上构建的」,你拿到资源包在自己的机器上跑,第一件事永远是在目标机器上重新构建引擎,而不是试图加载别人附带的 engine。
4. tensorrt_inference.py 推理链路:从视频帧到动作类别的完整流水线
4.1 视频解码与 clip 采样:预处理要和训练完全一致
TensorRT 只管加速网络前向,不管视频怎么变成张量。SlowFast 的预处理比普通图像分类多两步:时间维采样和双路径分组。常见做法是用 OpenCV 或 PyAV 把视频解码成帧序列,按均匀间隔抽出一段 clip,再按 yaml 里的采样配置拆成 slow 和 fast 两份。归一化参数直接读配置文件里的 DATA.MEAN 和 DATA.STD——PySlowFast 默认用的是 RGB 三通道 (0.45, 0.45, 0.45) 和 (0.225, 0.225, 0.225),别自作主张换成 ImageNet 那套 (0.485, 0.456, 0.406),换错位置推理精度会掉一截。
import cv2 import numpy as np def preprocess_clip(frames, cfg): # frames: 解码出来的 RGB 帧列表,按时间顺序排列 resized = [cv2.resize(f, (224, 224)) for f in frames] clip = np.stack(resized, axis=0).astype(np.float32) # (T, H, W, C) clip = clip.transpose(3, 0, 1, 2) # (C, T, H, W) mean = np.array(cfg.DATA.MEAN, dtype=np.float32).reshape(3, 1, 1, 1) std = np.array(cfg.DATA.STD, dtype=np.float32).reshape(3, 1, 1, 1) clip = (clip / 255.0 - mean) / std # 采样索引交给 utils 里的采样器,按 yaml 配置生成,不在这里硬编码 sampler = SlowFastSampler( slow_frames=cfg.SLOWFAST.SLOW_FRAMES, fast_frames=cfg.SLOWFAST.FAST_FRAMES, alpha=cfg.SLOWFAST.ALPHA, ) slow_idx, fast_idx = sampler.get_indices(len(frames)) # 补 batch 维,返回两个可以直接喂给 TRT 的 (1, C, T, H, W) return clip[:, slow_idx][None], clip[:, fast_idx][None]逻辑说明:resize 这块要跟训练时的数据增强对齐,SlowFast 训练一般用短边缩放再中心裁剪到 224×224,推理时保持同样的处理,别直接用拉伸。采样器放在 slowfast/utils 里封装好,索引生成规则以 yaml 为准——项目里这个设计是对的,因为采样配置变了,只需要改配置,不需要动推理脚本。
参数说明:clip / 255.0 之后减均值除方差,顺序不能反,先除后减和先减后除是两个结果。mean 和 std 的 shape 要 broadcast 到 (C, T, H, W),reshape 成 (3,1,1,1) 是省事的写法。整段预处理在 CPU 上做,一帧 224×224 的 RGB 开销不大,但如果你要跑多路视频流,建议用多线程把解码和预处理放到独立线程池,别和 GPU 推理串在一起。
4.2 Engine 反序列化、ExecutionContext 与 binding 绑定
引擎加载进来之后,核心是 ExecutionContext。一个 engine 可以创建多个 context,对应多路并发推理;每个 context 维护自己的输入输出 binding 状态。动态 shape 下,推理前必须先用实际 shape 调 set_input_shape,然后重新查输出 binding 的 shape 并分配显存——输出维度是跟着输入走的,这一步漏了,后面显存越界是必然的。
import numpy as np import tensorrt as trt import pycuda.driver as cuda class SlowFastTRTEngine: def __init__(self, engine_path): runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING)) with open(engine_path, "rb") as f: self.engine = runtime.deserialize_cuda_engine(f.read()) self.context = self.engine.create_execution_context() def infer(self, slow_input, fast_input, output_shape): # 动态输入声明实际 shape,必须在分配绑定内存之前 self.context.set_input_shape("slow_input", slow_input.shape) self.context.set_input_shape("fast_input", fast_input.shape) d_slow = cuda.mem_alloc(slow_input.nbytes) d_fast = cuda.mem_alloc(fast_input.nbytes) cuda.memcpy_htod(d_slow, slow_input) cuda.memcpy_htod(d_fast, fast_input) d_out = cuda.mem_alloc(np.prod(output_shape) * np.float32().itemsize) h_out = np.empty(output_shape, dtype=np.float32) bindings = [None] * self.engine.num_bindings bindings[self.engine.get_binding_index("slow_input")] = d_slow bindings[self.engine.get_binding_index("fast_input")] = d_fast bindings[self.engine.get_binding_index("logits")] = d_out self.context.execute_v2(bindings=bindings) cuda.memcpy_dtoh(h_out, d_out) return h_out逻辑说明:bindings 列表的长度必须等于 engine.num_bindings,每个位置对应一个 binding 索引。输入输出的索引用 get_binding_index 按名字查,顺序跟网络定义顺序无关,所以别自己数位置,一定用名字查。execute_v2 是异步执行,显存拷贝是同步的,这里为了演示用了同步 memcpy;要压吞吐就改成 cuda.memcpy_htod_async 加 stream,多个 context 并行跑。
参数说明:output_shape 通常就是 (batch, num_classes),num_classes 从 yaml 的 MODEL.NUM_CLASSES 读。如果你用的是动态 batch,output_shape 第一维必须和输入 batch 一致,建议每次推理后从 context.get_binding_shape 重新取一次输出 shape,而不是缓存死值。
4.3 logits 后处理:softmax、TopK 与标签映射
网络输出的 logits 是原始分数,要变成可读的动作类别,得做 softmax 取概率、TopK 排序、再映射到标签名。这里有个小坑:softmax 不要对 logits 直接 exp,先减去当前行的最大值,防止指数溢出变成 inf。数值稳定性这步在 FP32 下影响不大,但 FP16 下溢出是真实存在的,养成习惯直接写好。
def softmax(x, axis=1): x = x - np.max(x, axis=axis, keepdims=True) # 先减最大值,防溢出 e = np.exp(x) return e / np.sum(e, axis=axis, keepdims=True) def postprocess(logits, labels, topk=5): probs = softmax(logits, axis=1) # (N, C) order = np.argsort(probs, axis=1)[:, ::-1][:, :topk] # 从大到小 results = [] for i in range(order.shape[0]): results.append( [(labels[k], round(float(probs[i, k]), 4)) for k in order[i]] ) return results逻辑说明:labels 是类别名列表,从项目提供的标签文件读,顺序和训练时的 class index 一一对应。SlowFast 在 Kinetics-400 上预训练的话是 400 类,自训数据集就按自己的 class list 来。这步后处理在 CPU 上做,开销可以忽略,关键是别把 softmax 写进 TensorRT 图里——引擎输出 logits 就好,softmax 留在外面,这样标签和阈值调整不用重新构建引擎。
5. 部署避坑:TensorRT 版本、旧显卡与动态 shape 的五个踩坑现场
5.1 构建与加载阶段的坑
坑一:引擎文件跨机器加载,反序列化直接崩
现象:在 A 机器上构建好的 slowfast_fp16.trt,拷贝到 B 机器(同型号 GPU),运行时 deserialize_cuda_engine 直接抛异常,或者加载成功但推理结果全错。
原因:TensorRT 引擎和 GPU 架构、TensorRT 库版本、CUDA 版本强绑定。哪怕两张卡型号一样,TensorRT 小版本不同,序列化格式都可能不兼容。
解决:把 engine 文件当构建缓存看待,不带出厂。每台部署机器上,用 onnx_to_trt.py 现场重新构建一次,或者启动时检查 engine 的构建信息,不匹配就自动重建。项目里 onnx_to_trt.py 就是干这个的,把它接到部署脚本的初始化阶段,比手动管理 .trt 文件稳妥得多。
坑二:TensorRT 10.x 在 GTX 1070 上构建失败或者性能没提升
现象:老显卡上跑 TensorRT 新版本,要么构建报 unsupported,要么 FP16 速度没起来。
原因:Pascal 架构(GTX 10 系列,compute capability 6.x)在 TensorRT 新版本里的支持逐年收紧,驱动和 CUDA 版本要求变高;同时 Pascal 的半精度计算吞吐本来就不占优势,FP16 对它的收益很有限。
解决:上项目前先查官方 support matrix 确认你这张卡在当前 TensorRT 版本里是不是完整支持。实测比看帖靠谱——同一份 ONNX,分别用 FP32 和 FP16 构建,trtexec 跑一遍看 mean GPU time。GTX 1070 上如果 FP16 提速不到 30%,直接留 FP32,省得后续为精度波动折腾。
5.2 推理与精度阶段的坑
坑三:FP16 engine 精度暴跌,Top-1 掉了好几个点
现象:同一段视频,FP32 engine 分类正确,FP16 engine 输出概率分布完全变形,甚至出现 NaN。
原因:某些层在 FP16 下数值动态范围不够,SlowFast 里通常是最后的分类头或者某些大数值响应层先溢出。TensorRT 的 FP16 是全图统一开关,不会自动帮你挑层。
解决:用层级别精度控制,把出问题的层强制回 FP32。常见做法是构建时遍历 network 的层,对指定名字的层设置 precision 为 trt.float32,并打开 config.set_flag(trt.BuilderFlag.OBEY_PRECISION_CONSTRAINTS)。判断哪层出问题,可以二分法:先 lock 后半段全层 FP32,精度恢复说明问题在后段,再缩小范围。
坑四:推理结果输出 shape 对,但数值全错,或者永远输出同一个类别
现象:engine 构建顺利,推理不报错,但 softmax 之后某个类别概率接近 1,换什么视频都一样。
原因:预处理和训练不一致,最常见三个:RGB/BGR 通道序反了、mean/std 用错了、采样索引和 yaml 配置不一致。另一个隐蔽原因是 export 时输入名字顺序和模型 forward 签名不一致,导致 slow/fast 两条路径权重互换。
解决:不要凭肉眼看结果,做一次端到端数值对齐:同一段视频,分别跑 PyTorch 原模型(FP32)和 TensorRT engine(也先用 FP32),对比 logits 的最大绝对差,压在 1e-3 以内算通过。这一步能同时揪出预处理、导出顺序、引擎解析三类问题,是部署验收里性价比最高的一步。
坑五:动态 batch 调到 max 档时突然 OOM
现象:batch=4 跑得好好的,batch=8 直接 out of memory,或者报 shape 不匹配。
原因:optimization profile 的 max 档设了 8,但输出 buffer 是按构建时的 shape 缓存的,没有在 set_input_shape 之后重新查输出 shape;或者显存分配时只按 opt 档的大小估算,实际 max 档超出了。
解决:每次 set_input_shape 之后,强制重新走一遍 context.get_binding_shape 获取输出维度,再分配 host/device 内存。显存不够就把 max 档改成线上真实的峰值 batch 而不是一个很大的数字——profile 区间越小,kernel 优选越激进,显存占用也越可控。
6. 用 yaml 固化推理参数并量化性能:上线前最后一道工序
6.1 配置文件里值得逐项核对的参数
项目里 configs/SLOWFAST_4x16_R50_inference.yaml 不光是给构建脚本看的,它是整个部署链路的参数源。把变量写进 yaml 而不是散落在代码里,最大的好处是换数据集、换卡、换精度档位时,只改一个文件,不用动代码。我每次拿到这类项目,第一件事就是把 yaml 里这几个字段逐个核对:
| 配置项 | 含义 | 部署时要注意的 |
|---|---|---|
| DATA.MEAN / DATA.STD | 归一化均值方差 | 推理脚本里写死的值必须和这里一致 |
| SLOWFAST.SLOW_FRAMES / FAST_FRAMES | 两条路径的采样帧数 | 和导出 ONNX 的 dummy T 维度必须一致 |
| SLOWFAST.ALPHA | slow/fast 帧率比 | 改了它,采样索引和推理速度都会变 |
| MODEL.NUM_CLASSES | 类别数 | 决定输出 binding 的 shape 和后处理标签数 |
| TRT.PRECISION / TRT.WORKSPACE | 精度档位与 workspace 上限 | 构建脚本直接读取,改配置即改构建行为 |
这里最容易翻车的是第一行:PySlowFast 的 mean 是 (0.45, 0.45, 0.45),std 是 (0.225, 0.225, 0.225),和 ImageNet 分类常用的 (0.485, 0.456, 0.406) 不一样。很多从图像分类转过来的同事,惯性沿用 ImageNet 那套,结果 SlowFast 精度莫名其妙掉一截。所以预处理代码里别硬编码 mean/std,直接读 yaml,让 yaml 成为唯一事实来源。
6.2 性能验证的正确口径
部署完不能只说「变快了」,要给一个能复现的量化口径。我习惯用 trtexec 做引擎基准,再在推理脚本里算端到端延迟。衡量标准只有两个数字:单次前向延迟(ms)和每秒处理 clip 数(throughput)。
trtexec --loadEngine=slowfast_fp16.trt \ --shapes=slow_input:1x3x4x224x224,fast_input:1x3x32x224x224 \ --warmUp=500 --iterations=100trtexec 输出的 mean GPU time 是纯 GPU 前向时间,不包含 CPU 解码和预处理,这是引擎层面的基准。端到端延迟要自己在推理脚本里测,注意两点:第一,先跑 50 次 warmup 再计时,GPU 频率和显存分配在冷启动时都是不稳定的;第二,测延迟时把视频解码排除在外,或者单独统计解码耗时,不然 CPU 瓶颈会掩盖 GPU 的真实水平。看 GPU 利用率就开个 nvidia-smi dmon 挂着,观察推理期间利用率有没有长时间在 90% 以上——利用率上不去,问题基本在 CPU 预处理或者数据搬运,不在 TensorRT。
从那以后,我每次部署 TensorRT 项目都强制走一遍这套流程:先在目标机器上重建 engine,再用 FP32 输出对齐验收预处理和导出链路,最后用 trtexec 出基准、用 yaml 固化参数。这套项目资源我整理在下载包里了,readme 把构建顺序、卡型和配置都写清楚了,你要是正好在折腾 SlowFast 上线,直接拿这套骨架去改,能少踩一半我前面说的那些坑。希望帮到你。
本文还有配套的精品资源,点击获取