简介:面向算法部署工程师与视频理解开发者的实战项目,针对SlowFast视频理解模型在推理时计算开销大、速度不足的问题,给出了基于TensorRT的完整加速方案。内容覆盖模型转换、图优化、TensorRT推理等关键环节,涉及ONNX导出、TensorRT引擎构建与部署验证流程,同时提供YAML配置文件和说明文档,可帮助理解层融合、内核调优、精度校准等优化策略的实际应用,便于复现并迁移到自有模型。资源共24个文件,以21个Python脚本为主,按模型转换、引擎优化、推理测试等阶段组织,辅以配置、说明及工程配置文件,压缩包仅46KB,代码轻量、模块清晰,涵盖从模型导出到高性能推理的完整链路,适合有深度学习基础并希望提升推理性能的开发者参考。目前已有158人浏览学习,结合官方文档和GPU环境动手实践,可快速掌握TensorRT部署视频理解模型的完整思路与关键细节,为后续工业落地打下坚实基础。

1. 用 TensorRT 部署 SlowFast,核心是处理双时间分辨率

视频理解算法里,SlowFast 是少有的从设计上就为效率服务的模型:它同时用高帧率低通道数的快路径捕捉运动,用低帧率高通道数的慢路径捕捉语义,两者在模型尾部融合。可它一旦离开 PyTorch,直接跑在嵌入式设备或视频服务端,绝大多数人第一个遇到的就是速度问题——3090 上能跑到实时,换到 Orin NX、边缘盒子或者只给 4 个 CPU 线程的容器里,帧率直接掉到个位数。TensorRT 在这里的价值不是简单的算子替换,而是把 SlowFast 的双路径计算图重新编排,做算子融合、显存复用、FP16/INT8 量化,让双分支从「两个独立网络」变成一张可调度、可流水化的引擎图。

这篇博文不讨论概念层面的视频理解,直接围绕 TensorRT 8.6+ 部署 SlowFast 的完整路径来讲:从 PyTorch 权重导出 ONNX,到用 trtexec 和 Python API 构建引擎,再到推理时的视频采样、双流数据组织、动态 shape 绑定和性能验证。这里假设你已经有一条能跑通的 SlowFast 训练或推理管线,至少知道 Action Recognition 和 Video Understanding 的区别,知道 slowfast 这个指标里两个分支各在干什么。

做部署的工程师容易踩一个隐含的坑:SlowFast 的 ONNX 导出不是把整个模型 torch.onnx.export 就完事,而是需要考虑两个路径各自的输入时序、长度对齐和尾部融合层。我会按实际落地顺序来讲,每一段都有可以直接抄走的命令或代码。

2. 预处理链路:从视频帧到 SlowFast 双路径输入

2.1 SlowFast 的输入到底长什么样

SlowFast 的输入不是一个静态帧,而是一段视频片段(clip)。常见的配置是慢路径每 4 帧取 1 帧,快路径每 2 帧取 1 帧,帧数为 32。也就是说输入是 [N, C=3, T=32, H, W] 这样的五维张量。但实际导出 ONNX 时,用户最容易搞错的是 batch 维度的位置和 T 是否可动态。

从 PyTorch 的原始模型来看,SlowFast 的 forward 接收的是一个 list, [slow_pathway, fast_pathway] 。慢路径 shape 是 [N, 3, 4, H, W] (假设 32 帧慢路径取 4 帧),快路径是 [N, 3, 32, H, W] 。部署时必须把这两个张量拼成一个统一的输入,还是分开两个输入?我把两种做法都遇到过,但推荐第二种。

实际结构:一个 ONNX 图,两个输入节点
输入1: slow_input  [N, 3, 4, 224, 224]   (慢路径)
输入2: fast_input  [N, 3, 32, 224, 224]  (快路径)

分别导出两个输入节点是合理的,因为 TensorRT 在构建 engine 时可以对每个 profile 分别指定不同输入的最值范围,分离后还可以分别为两个路径设置不同的 quantile 参数。

2.2 ONNX 导出脚本,注意动态轴要剖开

把 SlowFast 转成 ONNX,常规做法是:

import torch
import torch.onnx
from slowfast.models import build_model

model = build_model(cfg)
checkpoint = torch.load("SlowFast.pyth", map_location="cpu")
model.load_state_dict(checkpoint["model_state"])
model.eval()

dynamic_axes = {
    "slow_input": {0: "batch", 3: "height", 4: "width"},
    "fast_input": {0: "batch", 3: "height", 4: "width"},
    "output": {0: "batch"}
}

torch.onnx.export(
    model,
    ({"slow_input": torch.randn(1, 3, 4, 224, 224),
      "fast_input": torch.randn(1, 3, 32, 224, 224)}),
    "slowfast.onnx",
    input_names=["slow_input", "fast_input"],
    output_names=["output"],
    dynamic_axes=dynamic_axes,
    opset_version=17,
    do_constant_folding=True
)

这里有几个参数说明:

  • opset_version=17 :TensorRT 8.6 对 ONNX 17 支持较好,尤其是 Resize 和 ScatterND 这类算子。如果你的工具链版本较老,用 14 也可能,但 ScatterND 在某些版本上导出后 TensorRT 不支持,会退到 DLA 或直接报错。
  • dynamic_axes 中我只对 batch、H、W 设了动态,T 维固定。实际部署中 T 固定为模型训练时的长度最稳定。如果你非要让 T 动态,TensorRT 里 profile 的 minShape 、 optShape 、 maxShape 都要覆盖 T 范围,这时 SUS(Strided Uppper Bound)的计算会变复杂,不值得。
  • model.eval() 必须调用,否则导出的图里带 dropout 和 BN 的 training 路径,TensorRT 会莫名多出一些不可融合的节点。

2.3 视频采样:均匀采帧与排到 batch 里

视频理解部署最容易被低估的是采样对延迟的影响。模型推理可能只要 10ms,而视频解码加 resize 可能吃掉 50ms。这里的关键在于不要用 PyTorch 的 DataLoader,直接用高效的方式组织帧序列。

import cv2
import numpy as np

def sample_frames(video_path, num_frames=32, height=224, width=224):
    cap = cv2.VideoCapture(video_path)
    frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    
    # 均匀采样,保证覆盖整个视频
    indices = np.linspace(0, frame_count - 1, num_frames, dtype=int)
    frames = []
    for idx in indices:
        cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
        ret, frame = cap.read()
        if not ret:
            break
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        frame = cv2.resize(frame, (width, height))
        frames.append(frame)
    cap.release()
    
    # 填补不足的帧:用最后一帧复制
    while len(frames) < num_frames:
        frames.append(frames[-1])
    
    return np.stack(frames, axis=0)  # shape [T, H, W, C]

def preprocess(frames, mean=[0.45, 0.45, 0.45], std=[0.225, 0.225, 0.225]):
    # 转为 NCHW,归一化
    frames = frames.transpose(3, 0, 1, 2).astype(np.float32)  # [C, T, H, W]
    frames /= 255.0
    frames = (frames - np.array(mean).reshape(3, 1, 1, 1)) / np.array(std).reshape(3, 1, 1, 1)
    return frames

这段代码包含了整个视频预处理的关键设计:

  • 使用 cv2.CAP_PROP_POS_FRAMES 随机跳到指定帧,而不是逐帧读取再丢弃。这在大视频上能省去大量解码时间。
  • 帧数不足时用最后一帧补位,避免 TensorRT 输入出现 NaN 或未初始化内存。
  • 归一化时把 mean 和 std reshape 成 [C, 1, 1, 1] ,在 NCHW 布局下 Numpy 广播,效率高,无循环。

2.4 从预处理输出到 TensorRT 的输入张量

在 PyTorch 原版 SlowFast 里, slow_pathway 必须使用训练时的 _construct 方式抽取。部署时如果不想重新实现采样,就直接从上面的 frames 数组里抽取:

def build_pathway_input(frames, slow_stride=8, fast_stride=2):
    # frames: [C, T, H, W] 其中 T = 32
    slow_frames = frames[:, ::slow_stride, :, :]   # 每 8 帧取一帧,得到 4 帧
    fast_frames = frames[:, ::fast_stride, :, :]  # 每 2 帧取一帧,得到 16 帧
    return slow_frames, fast_frames

实际上官方 SlowFast 的 slow pathway 步长是 8(针对 64 帧输入),而 fast pathway 步长是 2。这里 32 帧输入下慢路径取 4 帧、快路径取 16 帧。这个比例和模型内部的 lateral connection 维度是对应的,如果训练时用的是 64 帧,那你部署时要么改成 64 帧输入,要么把模型的 cfg.DATA.NUM_FRAMES 一并调整。我们后面在 ONNX 导出时已经固定了 T 为 32,所以这里直接按 32 帧来。

3. 用 trtexec 和 TensorRT API 构建 SlowFast 引擎

3.1 从 ONNX 到 engine,trtexec 只是第一步

拿到 slowfast.onnx 之后,一个最直接的命令是:

/usr/src/tensorrt/bin/trtexec \
  --onnx=/path/to/slowfast.onnx \
  --saveEngine=/path/to/slowfast.engine \
  --minShapes=slow_input:1x3x4x224x224,fast_input:1x3x32x224x224 \
  --optShapes=slow_input:4x3x4x224x224,fast_input:4x3x32x224x224 \
  --maxShapes=slow_input:8x3x4x224x224,fast_input:8x3x32x224x224 \
  --fp16 \
  --workspace=2048 \
  --verbose

参数说明:

  • --fp16 开启 FP16 校准。SlowFast 的两个分支全是卷积、ReLU、lateral 1x1 连接和少数矩阵乘法,FP16 基本不掉点,UCF101 上 top-1 误差通常小于 0.3%。
  • --minShapes/optShapes/maxShapes 分别对应动态 shape 的下限、优化上限和上限。batch=4 作为 optShape 是一个折中:batch 太大会让 cuDNN 选择偏向大 batch 的算法,小 batch 时反而变慢。
  • --workspace 最大可用显存,单位 MB。旧版本是 workspace,TensorRT 8.6 之后改为 memPoolSize 。如果你的 TensorRT 版本用 8.5+,命令要调整。

但 trtexec 生成 engine 后,实际部署时直接读文件就行,不需要在运行时再 parse ONNX。下面的 Python API 只用于在目标机器上生成 engine。

3.2 用 Python API 构建 engine,处理动态 profile

trtexec 适合验证转换是否成功,但实际工程里更常直接在推理服务里用 TensorRT Python API 构建:

import tensorrt as trt

TRT_LOGGER = trt.Logger(trt.Logger.WARNING)

def build_engine(onnx_path, engine_path):
    builder = trt.Builder(TRT_LOGGER)
    network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
    parser = trt.OnnxParser(network, TRT_LOGGER)
    
    with open(onnx_path, "rb") as f:
        parser.parse(f.read())
    
    config = builder.create_builder_config()
    config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 2 << 20)
    config.set_flag(trt.BuilderFlag.FP16)
    
    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 = builder.build_serialized_network(network, config)
    with open(engine_path, "wb") as f:
        f.write(engine)
    
    return engine_path

这段代码和 trtexec 的功能完全等价,但可以嵌入到你的算法部署平台里,比如在嵌入式设备上首次启动时自动构建。注意 set_memory_pool_limit 的单位是字节, 2 << 20 实际上是 2MB,不是 2GB,这里我写了 2GB 的写法是 2 << 30 。在实际代码请用 2 << 30 ,否则 workspace 过小会导致算子融合失败。

3.3 常见的 ONNX 解析失败,和 SlowFast 的特殊性

在实际使用 TensorRT 部署 SlowFast 时,ONNX 解析阶段远比模型推断阶段更容易卡住。这里最多的问题集中在这几个:

  • Resize 算子的 coordinate_transformation_mode 设为 half_pixel ,TensorRT 版本如果低于 8.0 会直接报错。SlowFast 官方实现里的空间池化不涉及 Resize,但如果你的模型中用了 Fast-RCNN 的 ROIAlign,且 ONNX 导出时选择的是 roi_align (PyTorch 版本实现),TensorRT 会走自己的 IROIAlignLayer ,数值与 PyTorch 略有差异,最后推理时标定出来的精度会有 1% 左右的偏差。
  • ScatterND 算是 TensorRT 8.6 里支持得比较好的算子,但批量动态 shape 下,如果索引是 int64 类型,TensorRT 的 ScatterND 性能很差。建议在导出 ONNX 前把索引转换为 int32。
  • Gather 在 axis=0 上如果索引维度超过一个 batch,容易出现 profile 不匹配。建议部署时确保 T 是常量,避免动态 shape 在全连接层附近造成维度过大的推导延迟。

如果出现形状推导失败,先执行 trtexec --onnx=slowfast.onnx --shapes=slow_input:1x3x4x224x224,fast_input:1x3x32x224x224 --fp16 --verbose 且不加动态 shape,看报错在第几层,之后根据报错去修改 ONNX 图。

这里有一个容易踩的坑:如果你导出 ONNX 时,模型 forward 用的是 list 输入(PyTorch 标准),onnx 图里的两个输入名字叫 input1 和 input2 ,而你在 TensorRT 里设置 profile 时写的是 slow_input 和 fast_input ,这会导致 set_shape 报错说 input name not found。解决方法是输入名称必须与 ONNX 图中的 input_names 完全一致,导出时就得定好。

4. 运行时推理:显存绑定、并发队列和动态 shape

4.1 用 PyCUDA 绑定输入输出

engine 构建完成之后,推理核心就是把视频数据从 CPU 放到 GPU 显存,执行,然后拿回结果。为了保证概率输出不丢,我们要理解 TensorRT 的 execute 机制:TensorRT 的 execution context 是状态化的,必须用 set_input_shape 指定当前 batch 的具体形状,再绑定 buffer。

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

class SlowFastTRT:
    def __init__(self, engine_path):
        with open(engine_path, "rb") as f:
            engine_data = f.read()
        runtime = trt.Runtime(TRT_LOGGER)
        self.engine = runtime.deserialize_cuda_engine(engine_data)
        self.context = self.engine.create_execution_context()
        
        # 为输入输出分配显存
        self.inputs, self.outputs, self.bindings = [], [], []
        for binding in self.engine:
            binding_shape = self.engine.get_tensor_shape(binding)  # 动态 shape 时为 -1
            size = trt.volume(binding_shape) * self.engine.max_batch_size if binding_shape[0] == -1 else trt.volume(binding_shape)
            dtype = trt.nptype(self.engine.get_tensor_dtype(binding))
            
            host_mem = cuda.pagelocked_empty(size, dtype)
            device_mem = cuda.mem_alloc(host_mem.nbytes)
            self.bindings.append(int(device_mem))
            
            if self.engine.get_tensor_mode(binding) == trt.TensorIOMode.INPUT:
                self.inputs.append((host_mem, device_mem))
            else:
                self.outputs.append((host_mem, device_mem))
    
    def infer(self, slow_input, fast_input):
        # 设定 profile 动态形状
        self.context.set_input_shape("slow_input", slow_input.shape)
        self.context.set_input_shape("fast_input", fast_input.shape)
        
        # 将 numpy 数据拷贝到锁页内存
        np.copyto(self.inputs[0][0], slow_input.ravel())
        np.copyto(self.inputs[1][0], fast_input.ravel())
        
        # 从锁页内存到 GPU 显存
        cuda.memcpy_htod_async(self.inputs[0][1], self.inputs[0][0], stream)
        cuda.memcpy_htod_async(self.inputs[1][1], self.inputs[1][0], stream)
        
        # 执行推理
        self.context.execute_async_v2(bindings=self.bindings, stream_handle=stream.handle)
        
        # 从 GPU 拷贝回 CPU
        cuda.memcpy_dtoh_async(self.outputs[0][0], self.outputs[0][1], stream)
        cuda.Stream.synchronize(stream)
        
        return self.outputs[0][0]

这段代码的要点:

  • pagelocked_empty 分配的是锁页内存, memcpy_htod_async 才能走 DMA 通道,避免 CPU 内存被交换到磁盘导致拷贝延迟飙到几十毫秒。
  • binding 在运行时是一整块显存区域,动态 shape 时第一次 set_input_shape 之后 TensorRT 会根据当前 shape 计算内部 buffer 大小。如果你在第一次推理后,调用了 shape 改变的大小超过 maxShape ,会报 out of memory 或直接 illegal memory access 。
  • execute_async_v2 必须传入绑定的设备端指针列表,不能用 PyTorch tensor 的 data_ptr ,因为 PyTorch 的显存是它自己分配的,TensorRT 不能直接复用,除非你走了 torch.cuda.memory 的零拷贝通道。

4.2 多 batch 和时序重叠

用上面这个类时,如果同时要跑多个视频流,我们往往不会为每个流单独建一个 context,而是把多个 clip 拼进一个 batch。拼 batch 的原则是把长度一致的视频帧堆在一起。

slow_batch = []
fast_batch = []
for clip in clips:  # 每个 clip 已预处理
    s, f = build_pathway_input(clip)
    slow_batch.append(s)
    fast_batch.append(f)
slow_batch = np.stack(slow_batch, axis=0)
fast_batch = np.stack(fast_batch, axis=0)

这样一次 infer 就能处理 4~8 个视频片段。在 Orin 这种嵌入式平台上,多 batch 带来的吞吐量提升比单纯调小模型更明显,因为内存带宽和计算单元能同时被两个输入路径占满。

但要注意,batch 变大后, slow_input 和 fast_input 的 memcpy_htod_async 时间会显著增加。一个优化办法是使用双 stream——CPU 负责下一批数据的预处理和拷贝,GPU 同时执行当前 batch 的推理。常见代码结构:

stream1 = cuda.Stream()
stream2 = cuda.Stream()
# 生产者线程:当前 batch 丢给 stream2 做 H2D
# 消费者线程:stream1 做 D2H 和 postprocess

这样交错之后,整个流水线的端到端延迟不会下降太多,但吞吐量能提升 1.5 到 2 倍,尤其是视频源有多个时。

4.3 输出 shape 与概率后处理

SlowFast 的输出是每个动作类别的 logits,假如训练在 Kinetics-400 上,输出 shape 是 [N, 400] 。拿到后处理时通常要 softmax。有的用户直接把 softmax 堆在 ONNX 里,有的则留在 Python 里做。在嵌入式设备上,softmax 放 GPU 上做能省一次 D2H 拷贝。但放 GPU 上做意味着 TensorRT 的输出就不是最终概率,还需要一个额外的 numpy 操作或 PyTorch 操作。

实际上 TensorRT 8.6 之后自带了一个 ISoftMaxLayer ,可以直接接到分类头后面。但如果你用 trtexec 转出的 engine,要么在 ONNX 导出时加 softmax 在顶层,要么等拿回输出再在 CPU 上做。推荐后者,原因很简单:如果你想在部署后为了调试或对比不同后处理策略(比如温度缩放),数据在 CPU 上处理更灵活。

logits = self.outputs[0][0].reshape(-1, 400).copy()
probs = np.exp(logits - logits.max(axis=1, keepdims=True))
probs /= probs.sum(axis=1, keepdims=True)

这里减掉 logits.max 是为了数值稳定性,FP16 下的 logits 数值范围可能达到 20 左右,直接 exp 可能溢出成 inf。

5. 性能调优:时序精度对齐、延迟瓶颈排查和 INT8 量化

5.1 数值一致性验证,别用余弦相似度糊弄

TensorRT 引擎建好后,首要验证的不是性能,而是精度。典型做法是把同一份输入同时跑 PyTorch 模型和 TensorRT engine,对比输出 logits 差异。余弦相似度在这里是不够的,正确做法是检查 top-1 类别的重合度,以及 logits 的绝对误差。

def validate(engine, pt_model, sample_input):
    # sample_input 是预处理好的 [N, C, T, H, W]
    trt_logits = engine.infer(sample_input)
    with torch.no_grad():
        pt_logits = pt_model(sample_input)
    
    diff = np.abs(trt_logits - pt_logits.numpy()).mean()
    # 更严格的做法:看每个类别 logits 标准差
    top1_trt = trt_logits.argmax(axis=1)
    top1_pt = pt_logits.argmax(axis=1).numpy()
    print(f"Mean abs diff: {diff:.4f}, top1 match: {(top1_trt == top1_pt).mean():.2%}")

在 FP16 下,SlowFast 的 logits 平均绝对误差一般在 0.1~0.5 之间,top-1 重合度应该在 99% 以上。如果误差明显偏大,优先检查上面提到的 ROIAlign 或 Resize 问题。一个更快速的验证法是单独跑慢路径,因为这部分的通道数多,FP16 误差在深层网络里会被放大。

5.2 延迟瓶颈的定位,是 D2H 还是 kernel 不融合

在 trtexec 的 profile 输出里,有一行是 GPU Compute Time 和 H2D/D2H 的时间。很多时候你会看到:

  • GPU Compute 只有 8ms,但 H2D+D2H 加起来有 15ms。
  • 这时瓶颈不在 TensorRT 引擎,而在显存拷贝。

缓解手段是:

  • 把多个 clip 的预处理结果在 CPU 端拼成一个连续大数组,一次 memcpy 拷贝进 GPU,而不是逐个拷贝。
  • 对输入用锁页内存配合 cudaMemcpyAsync ,并事先在一个 stream 上预热,避免首次调用时 CUDA context 初始化耗时。
  • 如果计算时间确实长,再看 profile 输出里 top 几层的耗时。SlowFast 的快路径前几层都是 3D 卷积,TensorRT 通常会把 3D 卷积拆成 Im2Col + GEMM 或者使用 CUDNN 的 conv3d ;此时如果是 CUDNN 实现,要考虑选择不同的 heuristic 或者用 --tacticSources 限制算法来源。
# 只允许 cuDNN 的卷积和 cuBLAS 的 GEMM,禁用 cuBLASLt
trtexec --onnx=slowfast.onnx --fp16 --tacticSources=-cublasLt,+cublas,+cudnn \
        --saveEngine=slowfast_tactic.engine

在某些 Ampere 架构和 Orin 上, cuBLASLt 默认选出的矩阵乘算法在 batch=1 时非常拉胯,禁用后延迟能降低 20%。在嵌入式的 Orin 上反而建议保留默认。

5.3 从 FP16 到 INT8,但 SlowFast 很挑数据

INT8 量化永远是一个诱人的选项,毕竟在嵌入式设备上 INT8 的卷积吞吐量比 FP16 高出一截。但 SlowFast 的时序建模依赖快路径的高频特征,边界在特征图上非常稀疏,纳入了很多数值幅度很小但语义重要的信号。如果你用 500 张静态图片做校准,量化后的掉点可能高达 3~5%。这不是 TensorRT 的错,而是校准集采样分布和实际视频帧分布不匹配。

如果决定走 INT8,至少用 1000 段视频片段,且每段视频随机抽取 4~10 帧制作校准集,而不是用一堆关键帧。推荐的校准代码:

def int8_calibrator(calib_dataloader):
    # 使用 TensorRT 的 IInt8CalibratorEntropyCalibrator2
    class SlowFastCalibrator(trt.IInt8CalibratorEntropyCalibrator2):
        def __init__(self, dataloader):
            super().__init__()
            self.dataloader = dataloader
            self.buffer_size = 8 * 3 * 32 * 224 * 224 * 4
            self.device_inputs = [
                cuda.mem_alloc(self.buffer_size),
                cuda.mem_alloc(self.buffer_size)
            ]
        
        def get_batch_size(self):
            return 8
        
        def get_batch(self, names):
            try:
                slow, fast = next(self.dataloader)
                cuda.memcpy_htod(self.device_inputs[0], slow.ravel())
                cuda.memcpy_htod(self.device_inputs[1], fast.ravel())
                return self.device_inputs
            except StopIteration:
                return None
    
    return SlowFastCalibrator(calib_dataloader)

get_batch 每次返回两个显存地址给 TensorRT 填充网络输入,跑一遍前向得到激活分布直方图。SlowFast 的模型比较大,校准耗时通常在 10~20 分钟,但这一步一旦刷新成功,在 Orin 上的吞吐量能再提升 1.3 倍左右。

5.4 固定 T 带来的实际影响和长视频切段部署

最后要讲一个很容易被忽略的部署技巧。SlowFast 训练时用的是 32 帧片段,但实际视频可能长达 10 分钟,部署时的做法是把视频按固定秒数切段,每段单独做片段预测,然后对各个片段的 softmax 概率取平均。

常见的做法是滑窗重叠,比如每 2 秒抽 32 帧,窗口重叠 50%,这样时序上的动作不会因为切段位置被截断。现有的优化方案里,直接在 TensorRT 引擎中调整 T 是不推荐的,因为模型内部的时序池化和 lateral connection 结构在时间维度上做了一个低秩变换,T 的变化会和卷积核的感受野产生奇怪的互动,反而是把视频切短更可控。

如果服务端资源允许,可以用 NVIDIA DALI 直接做视频解码和预处理,与 TensorRT 的 stream 组合起来,但那是另一个话题。到这里,一个可用的 TensorRT + SlowFast 视频理解部署闭环已经完整走通了,剩下的就是针对你的具体设备做参数微调和性能压测。

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

Logo

邀请您加入社区

更多推荐