Video Training for Assistance Driving

Pinjie Xu | 2025/05/30

目录

议程

  • 背景与概述 (Background & overview)
  • 设计 (Design)
  • 优化 (Optimization)
  • 性能 (Performance)
  • 总结 (Takeaway)

背景

为何需要视频训练?

  • 传统图像训练的问题:

    • 辅助驾驶(AV)数据量巨大,消耗过多存储空间;模型之间缺乏数据复用。
    • 原始的AV记录是视频剪辑,将其处理成图像是一个额外的步骤,需要复杂的工程。
  • 视频训练如何解决问题:

    • 模型训练直接从视频中提取图像。
    • 与视频相比,图像训练消耗更多的存储空间。
      • JPEG 压缩率 12:1
      • H.264 在较高比特率下压缩率为 107:1
      • H.265 在较高比特率下压缩率为 210:1
    • 特别是对于超高清视频 (每个视频剪辑30秒):
      • 原始数据大小 = 30 * 25 * 3 * 3840 * 2160 * 8 bits = 17.4 GB
      • 图像大小 = 30 * 25 * 2MB ~ 4.4GB
      • 视频大小
        • H.264 0.88G (~80% 节省)
        • H.265 0.44G (~90% 节省)

PyNvOnDemandDecoder

一个为辅助驾驶设计的高效按需视频解码库

  • 更少的存储和带宽
    • 通过更高的视频编码压缩率实现 (节省80%-90%的存储空间)。
  • 帧提取 + 数据增强完全由模型训练隐藏
    • 为辅助驾驶场景优化。
  • 灵活的流水线: 随机访问、流式访问、解复用器-解码器分离访问。
  • 最小的GPU内存使用
    • 80MB CUDA 上下文
    • 每个 4k 解码器实例约 ~30M
    • 每个 4k 图像约 ~20M (仅存储目标帧)
  • 轻量级开销
    • 端到端训练中 2%-10% 的性能开销 (取决于CPU利用率)。

PyNvOnDemandDecoder 设计

概览

  • 解复用器 (Demuxer):
    • 解复用器 (demultiplexer的简称) 从多媒体容器文件 (如 MP4, MKV, AVI) 中提取基本流 (视频, 音频, 字幕)。
  • 解码器 (Decoder):
    • 视频解码器接收由解复用器提取的压缩视频流,并将其解压缩为可以渲染或显示的原始视频帧。
  • 数据包 (Packets):
    • 包含来自视频流的压缩数据。
    • 可选地包含用于同步和解码顺序的元数据 (PTS, DTS, 流索引, 标志)。
PyNvOnDemandDecoder 设计概览,Page 5
PyNvOnDemandDecoder 设计概览,Page 5

随机帧访问 (Random Frame Access)

  • 随机帧访问:
    • 传统的神经网络训练从视频中提取随机图像,通常忽略了视频帧的时间信息。
    • 与那些为连续帧提取而优化的现有视频解码器不同,我们的设计优先考虑随机帧提取的性能。
随机帧访问架构图,Page 6
随机帧访问架构图,Page 6

流式帧访问 (Streaming Frame Access)

  • 流式帧访问:
    • 在AV算法中,某些方法 (例如 StreamPETR) 旨在利用视频中的时间信息,因此需要逐帧处理。
    • 我们设计了一个流式解码流水线,该流水线复用解复用器并支持多个解码器。
    • 对于每个视频文件,复用解码器以逐一输出帧,当解码完成时,重新配置解码器。
流式帧访问架构图,Page 7
流式帧访问架构图,Page 7

解复用器-解码器分离帧访问 (Demuxer-Decoder Separation Frame Access)

  • 两阶段方法:
    • DataLoader 仅执行解复用;解码在训练/推理期间完成。
  • 仅解码方法:
    • 在存储系统中存储数据包而非视频。
    • 帧提取完全依赖于解码器。

随机帧访问 API

  • Python 接口
import PyNvOnDemandDecoder as nvc
# create random access decoder, only once
nv_gop_dec = nvc.CreateGopDecoder(
                           num_video_per_clip,
                           iGpu)
while True:
  # Reuse the nv_gop_dec to extract multi-frame from multi-file
  gop_decoded = nv_gop_dec.Decode(file_path_list,frame_id_list)

流式帧访问 API

  • Python 接口
import PyNvOnDemandDecoder as nvc
# create stream reader decoder, only once
nv_smp_rd = nvc.CreateSampleReader(
                           num_sets,
                           num_video_per_clip,
                           iGpu)
while True:
  # Reuse the nv_smp_rd to extract multi-frame from multi files progressively
  gop_decoded = nv_smp_rd.Decode(file_path_list,frame_id_list)

解复用器 & 解码器分离访问 API

  • Python 接口
import PyNvOnDemandDecoder as nvc
# create random access decoder, only once
nv_gop_dec = nvc.CreateGopDecoder(
                           num_video_per_clip,
                           iGpu)
# get GOP packets from demuxer
packets = nv_gop_dec.GetPackets(file_path_list,frame_id_list)
while True:
  # Reuse the packets and nv_gop_dec to extract the frames in those GOPs
  gop_decoded = nv_gop_dec.DecodePacket(packets,file_path_list,frame_id_list)

与 PyTorch 集成

  • 通过 spawn 与 PyTorch DataLoader 集成
# Enable GPU usage in multi-processing with spawn mode
torch.multiprocessing.set_start_method('spawn', force=True)

class VideoClipDatasetWithPyNVVideo(torch.utils.data.Dataset):
    def __init__(self):
        self.decoder = nvc.CreateGopDecoder(maxfiles, usedevicememory, iGpu)
    def __getitem__(self, index):
        decoded_frames = self.decoder.DecodeN12ToRGB(video_paths, frame_idx, True)
        return decoded_frames

# Custom Dataset Class
dataset = VideoClipDatasetWithPyNVVideo()
dataloader = DataLoader(dataset, batch_size, num_workers=4, pin_memory=True)

# 训练 pipeline
for batch in dataloader:
    train()

优化

数据并行与 NVDEC 流水线

  • 数据并行:
    • 一次读取包含多个视频的 CLIP,通过异步解复用和解码最大化数据并行性。
    • 实现一个异步的生产者 (解复用器) - 消费者 (解码器) 模型,并使用并发队列作为产品缓冲区。
  • NVDEC 流水线:
    • 每个 GPU 具有多个解码硬件单元。例如,A100 有 5 个。
    • 您可以为每个视频分配一个专用的 NVDEC;但是,变化的 GOP 长度可能会导致负载不平衡。
    • 设计基于解码硬件单元的五阶段流水线。多个视频的并行解码可以填充流水线气泡,提高硬件利用率。
NVDEC 流水线数据并行示意图, Page 13
NVDEC 流水线数据并行示意图, Page 13

免映射解码 (Map-free Decode)

  • 数据包数据结构优化:
    • GOP 中的帧通常分为 I 帧、B 帧和 P 帧。并非所有数据包都对解码目标帧有贡献。
    • 移除当前 GOP 中可能属于其他 GOP 的数据包。这避免了解码器上乱序时间戳的影响并减少了内存传输开销。
    • 移除不依赖于目标帧或其他帧的数据包。
  • 免映射优化:
    • GPU 视频解码依赖于内存映射 (map/unmap) 来显示帧。我们移除了对不相关帧的映射,只保留目标帧。
目标GOP及免映射解码示意图, Page 14
目标GOP及免映射解码示意图, Page 14

设备内存管理 (Device Memory Management)

  • GPU 内存池:
    • 在辅助驾驶数据集中,视频被组织成 CLIPs。
    • 同一位置的摄像头具有相同的分辨率。在 nuScenes 数据集中,每个 CLIP 包含六个具有相同分辨率的视频。
    • GPU 内存池根据相同的摄像头顺序高效地管理内存请求。
  • 上下文大小优化:
    • 在标准的 PyTorch 训练工作负载中,使用多进程 (在 Python 中使用 spawn) 来加载数据,确保 Dataloader 的开销与模型训练重叠。
    • 将 CUDA 上下文绑定到进程会导致过多的设备内存开销,每个进程 400MB
    • 通过创建轻量级的 CUDA 上下文,我们将共享内存的资源预分配最小化。这将每个进程的上下文开销减少到 70MB
设备内存使用优化对比图, Page 15
设备内存使用优化对比图, Page 15

解复用器 (Demuxer) 与解码器 (Decoder) 的重用

  • 解复用器重用:
    • 视频解复用器的创建和使用开销通常占视频解码流程的20-30%。
    • 基于流的训练模式(例如,StreamPETR)逐帧解码视频帧,允许重用同一个解复用器。
  • 解码器重用:
    • 对于辅助驾驶数据集,每个摄像头位置的视频都具有相同的分辨率、颜色和编码格式。
    • 为每个摄像头位置创建一个解码器可以在所有视频中重用。
  • 数据包重用:
    • 基于流的数据读取以步进方式解码视频帧,创造了强大的空间局部性。
    • 一次性解复用整个GOP(图像组),并记录其起始帧和长度,可以在该GOP内重用数据包数据以解码目标帧。
Page 16 - 解复用器与解码器重用优化流程图
Page 16 - 解复用器与解码器重用优化流程图

性能

随机访问延迟与吞吐量

  • nuScenes Mini 数据集:
    • 6个摄像头 x 1600宽度 x 900高度 x 3通道 x 714帧 H.264视频。
  • 测试条件:
    • 延迟测试: 测量从测试集中的一个随机视频解码一帧所需的时间。
    • 吞吐量测试: 测量从多批次随机视频中每秒解码的帧数,每个视频提取一帧。
Page 17 - 随机访问性能对比图,包括帧解码延迟、完整视频解码吞吐量和存储使用情况。我们的方法在延迟上降低了53%,吞吐量提升了13.3倍,视频存储相比图像节省了79%。
Page 17 - 随机访问性能对比图,包括帧解码延迟、完整视频解码吞吐量和存储使用情况。我们的方法在延迟上降低了53%,吞吐量提升了13.3倍,视频存储相比图像节省了79%。

测试设备:
- GPU: NVIDIA A100-SXM4-80GB
- CPU: AMD EPYC 7742 64核处理器
-
仅用于技术讨论

流式访问延迟与吞吐量

  • nuScenes Mini 数据集:
    • 6个摄像头 x 1600宽度 x 900高度 x 3通道 x 714帧 H.264视频。
  • 测试条件:
    • 延迟测试: 测量从一个固定视频中解码单个帧所需的时间,遵循一致的访问模式(例如,每四帧解码一次)。
    • 吞吐量测试: 测量从多个视频中每秒解码的帧数,遵循相同的访问模式(例如,每四帧解码一次)。
Page 18 - 流式访问性能对比图,包括帧解码延迟和完整视频解码吞吐量。我们的方法在延迟上降低了43%,吞吐量提升了12.3倍。
Page 18 - 流式访问性能对比图,包括帧解码延迟和完整视频解码吞吐量。我们的方法在延迟上降低了43%,吞吐量提升了12.3倍。

测试设备:
- GPU: NVIDIA A100-SXM4-80GB
- CPU: AMD EPYC 7742 64核处理器
-
仅用于技术讨论

未来工作

  • 开源:
    • PyNvOnDemandDecoder项目即将开源。
    • 欢迎随时与我联系以获取预发布版本。
  • 性能:
    • 在大规模数据集上进行集成测试。
    • 进一步减少端到端的开销。

总结 (Takeaway)

选择视频训练与PyNvOnDemandDecoder

  • 存储效率:
    • 节省80-90%的存储开销。
  • 性能提升:
    • 延迟降低53%。
    • 吞吐量提升13.3倍。
  • 最小开销:
    • 端到端训练开销仅增加2%-10%。
  • 辅助驾驶工作负载的灵活性:
    • 支持灵活的访问模式(随机、流式、分离)。

DALI 图像管道设计用于 ADAS 训练

Roman Schäffert, DevTech | AI Open Day 2025/05/30

动机

概述
- 数据加载和预处理通常是训练中的瓶颈。

ADAS 使用案例
- 每个样本中处理多个图像(例如,多摄像头,多时间步)。
- 每个图像有多个处理步骤。

DALI 提供的方案
- 高效的数据加载和预处理。
- 在 GPU 上执行。
- 利用 GPU 的空闲时间进行训练。

目标:简化用于 ADAS 使用案例的 DALI 管道设计
- 提供一种能够轻松表示复杂训练数据的可能性。
- 构建一个灵活的 DALI 管道框架,该框架:
- 专门为 ADAS 设计。
- 灵活且易于适应新的使用案例。
- 可轻松集成到现有实现中(作为 PyTorch DataLoader 的直接替代品)。
- 计算效率高。

灵活的管道设计

概述

  • 管道构成:
    • 数据输入实现(部分可定制)。
    • 自定义数据读取:基类 DataProvider
    • 可用的输入可调用对象/可迭代对象之一。
    • 一系列处理步骤(继承自 PipelineStepBase)。
    • 单个步骤可以被包装在访问修饰符中。
  • DALI 管道由构建块 PipelineDefinition 创建。
  • 支持作为 PyTorch DataLoader 的直接替代品:
    • 包装器:DALIStructuredOutputIterator
    • 可以输出嵌套的字典。
    • 可以使用自定义函数对 DALI 输出进行后处理(例如,转换为不支持的类型)。
    • 注意:后处理发生在主线程中,如果可能,请优先在 DALI 管道内部进行处理。
  • SampleDataGroup:训练样本的层级表示。
Page 25 - DALI ADAS 管道的灵活设计概览
Page 25 - DALI ADAS 管道的灵活设计概览

数据结构

  • SampleDataGroup
    • 访问数据时类似嵌套字典。
    • 预定义的结构和数据类型(两者都可以显式更改)。
    • 结构和类型必须:
      • 在输入中定义(在数据提供者中)。
      • 在需要的处理步骤中进行更改,例如:
        • 转换数据类型,例如,图像从 float 转换为 int 以进行归一化。
        • 添加/移除字段。
    • 包含额外的辅助功能(例如,用于字符串处理)。
    • 支持灵活的处理步骤。
Page 26 - SampleDataGroup (SDG) 的数据结构示意图,展示了其层级结构。
Page 26 - SampleDataGroup (SDG) 的数据结构示意图,展示了其层级结构。

处理步骤设计

  • 处理步骤设计
    • 步骤在构建时进行配置。
      • 配置要执行的处理。
      • 定义该步骤应用于哪些数据。
  • 示例: AffineTransformer
    • 将变换定义为简单变换的序列。
    • 根据名称将变换应用于数据字段。
      • 图像:例如,名称为 "image"
      • 投影矩阵:例如,名称为 "proj_mat"
      • 点集:例如,名称为 "bboxes"
    • 将相同的变换应用于所有字段,保持对应关系,例如:
      • 图像和边界框及投影矩阵之间。
      • 不同图像之间(例如,原始图像和分割掩码)。
  • 如果我们想为每个摄像头应用不同的变换该怎么办?
Page 27 - 处理步骤设计示例,展示了变换如何应用于数据字段。
Page 27 - 处理步骤设计示例,展示了变换如何应用于数据字段。
处理步骤设计 (续)
  • 将操作应用于部分数据:访问修饰符包装器,例如:
    • 将步骤应用于子树
      • 此处显示:["cams", "0"]
Page 28 - 应用处理步骤到指定的子树(例如第一个摄像头)。
Page 28 - 应用处理步骤到指定的子树(例如第一个摄像头)。
- **将操作应用于具有给定名称的条目的所有子节点(在树中的任何位置)**
    - 此处显示:`"cams"`
Page 29 - 应用处理步骤到所有名为 "cams" 的子节点。
Page 29 - 应用处理步骤到所有名为 "cams" 的子节点。

- **还有更多可用的修饰符**

Page 30 - 示意图与上一页相同,强调有更多修饰符可用。
Page 30 - 示意图与上一页相同,强调有更多修饰符可用。
  • 效率
    • 无需为每个样本执行搜索。
    • 使用 DALI 的图构建机制。
    • 适配(搜索)在图构建期间发生。
    • 在运行时,只执行实际的数据处理。
    • ✓ 引入的灵活性在运行时没有成本(仅在初始化期间有开销)。
Page 31
Page 31

流水线灵活性

Page 32
Page 32
  • 如果我们更改输入数据格式,流水线需要哪些更改?
    • 左侧图示展示了输入格式的变化,从单个样本数据组(SampleDataGroup)变为包含时间步(time_steps)的序列数据。
    • 所需的流水线更改:
      • 调整 DataProvider
      • 确保在每个时间步中应用不同的仿射变换(如果需要)。
      • 调整数据格式转换。
    • 右侧图示指出了在 DALI 流水线中需要修改的具体组件,包括 SampleInputModuleVisibleBBoxSelectorBEVBBoxTransformer3DStreamPETROutputCombiner 以及用户提供的后处理函数。

运行时性能

设置

  • 硬件设置
    • 以下为用于技术讨论的硬件配置详情。
Page 34
Page 34
  • 示例用例:StreamPETR [1]
  • 配置
    • 大部分遵循 stream_petr_r50_flash_704_bs2_seq_24e 配置。
    • 显著差异:
      • 使用 8 个 PyTorch DataLoader 工作进程,而原文为 4 个。
      • 训练 1000 次迭代进行评估(其中 50 次作为预热)。
      • 使用自定义高斯热图生成。
  • 数据:NuScenes mini(所有迭代作为一个 epoch)。
  • 对 DALI 的适配:用 DALI 流水线替换 PyTorch DataLoader

[1] S. Wang et. al., “Exploring Object-Centric Temporal Modeling for Efficient Multi-View 3D Object Detection”, arXiv:2303.11926 [http://cs.CV]
代码: https://github.com/exiawsh/StreamPETR

结果

  • 结果
    • 运行时:预热阶段后每批次的平均运行时。
Page 36
Page 36
  • 结果分析
    • 2-GPU 设置
      • 两种方法的运行时相当(使用 DALI 的加速比为 1.05 倍)。
    • 8-GPU 设置
      • DALI 带来了 1.25 倍的加速。
    • 在两种情况下,DALI 的 CPU 使用率都显著更低。
  • 讨论
    • DALI 在多 GPU 训练中显示出优势,因为在这些情况下 CPU 的负载本来会相当大。
    • 尽管 CPU 未被完全利用,但存在瓶颈导致执行速度变慢。

谢谢!

如果您想尝试/使用 DALI 流水线框架,请告诉我们!