Video Training for Assistance Driving
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
- JPEG 压缩率
- 特别是对于超高清视频 (每个视频剪辑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%节省)
- H.264
- 原始数据大小 =
PyNvOnDemandDecoder
一个为辅助驾驶设计的高效按需视频解码库
- 更少的存储和带宽
- 通过更高的视频编码压缩率实现 (节省
80%-90%的存储空间)。
- 通过更高的视频编码压缩率实现 (节省
- 帧提取 + 数据增强完全由模型训练隐藏
- 为辅助驾驶场景优化。
- 灵活的流水线: 随机访问、流式访问、解复用器-解码器分离访问。
- 最小的GPU内存使用
80MBCUDA 上下文- 每个 4k 解码器实例约
~30M - 每个 4k 图像约
~20M(仅存储目标帧)
- 轻量级开销
- 端到端训练中
2%-10%的性能开销 (取决于CPU利用率)。
- 端到端训练中
PyNvOnDemandDecoder 设计
概览
- 解复用器 (Demuxer):
- 解复用器 (demultiplexer的简称) 从多媒体容器文件 (如 MP4, MKV, AVI) 中提取基本流 (视频, 音频, 字幕)。
- 解码器 (Decoder):
- 视频解码器接收由解复用器提取的压缩视频流,并将其解压缩为可以渲染或显示的原始视频帧。
- 数据包 (Packets):
- 包含来自视频流的压缩数据。
- 可选地包含用于同步和解码顺序的元数据 (PTS, DTS, 流索引, 标志)。
随机帧访问 (Random Frame Access)
- 随机帧访问:
- 传统的神经网络训练从视频中提取随机图像,通常忽略了视频帧的时间信息。
- 与那些为连续帧提取而优化的现有视频解码器不同,我们的设计优先考虑随机帧提取的性能。
流式帧访问 (Streaming Frame Access)
- 流式帧访问:
- 在AV算法中,某些方法 (例如 StreamPETR) 旨在利用视频中的时间信息,因此需要逐帧处理。
- 我们设计了一个流式解码流水线,该流水线复用解复用器并支持多个解码器。
- 对于每个视频文件,复用解码器以逐一输出帧,当解码完成时,重新配置解码器。
解复用器-解码器分离帧访问 (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 长度可能会导致负载不平衡。
- 设计基于解码硬件单元的五阶段流水线。多个视频的并行解码可以填充流水线气泡,提高硬件利用率。
免映射解码 (Map-free Decode)
- 数据包数据结构优化:
- GOP 中的帧通常分为 I 帧、B 帧和 P 帧。并非所有数据包都对解码目标帧有贡献。
- 移除当前 GOP 中可能属于其他 GOP 的数据包。这避免了解码器上乱序时间戳的影响并减少了内存传输开销。
- 移除不依赖于目标帧或其他帧的数据包。
- 免映射优化:
- GPU 视频解码依赖于内存映射 (map/unmap) 来显示帧。我们移除了对不相关帧的映射,只保留目标帧。
设备内存管理 (Device Memory Management)
- GPU 内存池:
- 在辅助驾驶数据集中,视频被组织成 CLIPs。
- 同一位置的摄像头具有相同的分辨率。在 nuScenes 数据集中,每个 CLIP 包含六个具有相同分辨率的视频。
- GPU 内存池根据相同的摄像头顺序高效地管理内存请求。
- 上下文大小优化:
- 在标准的 PyTorch 训练工作负载中,使用多进程 (在 Python 中使用 spawn) 来加载数据,确保 Dataloader 的开销与模型训练重叠。
- 将 CUDA 上下文绑定到进程会导致过多的设备内存开销,每个进程
400MB。 - 通过创建轻量级的 CUDA 上下文,我们将共享内存的资源预分配最小化。这将每个进程的上下文开销减少到
70MB。
解复用器 (Demuxer) 与解码器 (Decoder) 的重用
- 解复用器重用:
- 视频解复用器的创建和使用开销通常占视频解码流程的20-30%。
- 基于流的训练模式(例如,StreamPETR)逐帧解码视频帧,允许重用同一个解复用器。
- 解码器重用:
- 对于辅助驾驶数据集,每个摄像头位置的视频都具有相同的分辨率、颜色和编码格式。
- 为每个摄像头位置创建一个解码器可以在所有视频中重用。
- 数据包重用:
- 基于流的数据读取以步进方式解码视频帧,创造了强大的空间局部性。
- 一次性解复用整个GOP(图像组),并记录其起始帧和长度,可以在该GOP内重用数据包数据以解码目标帧。
性能
随机访问延迟与吞吐量
- nuScenes Mini 数据集:
- 6个摄像头 x 1600宽度 x 900高度 x 3通道 x 714帧 H.264视频。
- 测试条件:
- 延迟测试: 测量从测试集中的一个随机视频解码一帧所需的时间。
- 吞吐量测试: 测量从多批次随机视频中每秒解码的帧数,每个视频提取一帧。
测试设备:
- GPU: NVIDIA A100-SXM4-80GB
- CPU: AMD EPYC 7742 64核处理器
- 仅用于技术讨论
流式访问延迟与吞吐量
- nuScenes Mini 数据集:
- 6个摄像头 x 1600宽度 x 900高度 x 3通道 x 714帧 H.264视频。
- 测试条件:
- 延迟测试: 测量从一个固定视频中解码单个帧所需的时间,遵循一致的访问模式(例如,每四帧解码一次)。
- 吞吐量测试: 测量从多个视频中每秒解码的帧数,遵循相同的访问模式(例如,每四帧解码一次)。
测试设备:
- 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:训练样本的层级表示。
数据结构
SampleDataGroup- 访问数据时类似嵌套字典。
- 预定义的结构和数据类型(两者都可以显式更改)。
- 结构和类型必须:
- 在输入中定义(在数据提供者中)。
- 在需要的处理步骤中进行更改,例如:
- 转换数据类型,例如,图像从 float 转换为 int 以进行归一化。
- 添加/移除字段。
- 包含额外的辅助功能(例如,用于字符串处理)。
- 支持灵活的处理步骤。
处理步骤设计
- 处理步骤设计
- 步骤在构建时进行配置。
- 配置要执行的处理。
- 定义该步骤应用于哪些数据。
- 步骤在构建时进行配置。
- 示例:
AffineTransformer- 将变换定义为简单变换的序列。
- 根据名称将变换应用于数据字段。
- 图像:例如,名称为
"image"。 - 投影矩阵:例如,名称为
"proj_mat"。 - 点集:例如,名称为
"bboxes"。
- 图像:例如,名称为
- 将相同的变换应用于所有字段,保持对应关系,例如:
- 图像和边界框及投影矩阵之间。
- 不同图像之间(例如,原始图像和分割掩码)。
- 如果我们想为每个摄像头应用不同的变换该怎么办?
处理步骤设计 (续)
- 将操作应用于部分数据:访问修饰符包装器,例如:
- 将步骤应用于子树
- 此处显示:
["cams", "0"]
- 此处显示:
- 将步骤应用于子树
- **将操作应用于具有给定名称的条目的所有子节点(在树中的任何位置)**
- 此处显示:`"cams"`
- **还有更多可用的修饰符**
- 效率
- 无需为每个样本执行搜索。
- 使用 DALI 的图构建机制。
- 适配(搜索)在图构建期间发生。
- 在运行时,只执行实际的数据处理。
- ✓ 引入的灵活性在运行时没有成本(仅在初始化期间有开销)。
流水线灵活性
- 如果我们更改输入数据格式,流水线需要哪些更改?
- 左侧图示展示了输入格式的变化,从单个样本数据组(SampleDataGroup)变为包含时间步(time_steps)的序列数据。
- 所需的流水线更改:
- 调整
DataProvider。 - 确保在每个时间步中应用不同的仿射变换(如果需要)。
- 调整数据格式转换。
- 调整
- 右侧图示指出了在 DALI 流水线中需要修改的具体组件,包括
SampleInputModule、VisibleBBoxSelector、BEVBBoxTransformer3D、StreamPETROutputCombiner以及用户提供的后处理函数。
运行时性能
设置
- 硬件设置
- 以下为用于技术讨论的硬件配置详情。
- 示例用例: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
结果
- 结果
- 运行时:预热阶段后每批次的平均运行时。
- 结果分析
- 2-GPU 设置
- 两种方法的运行时相当(使用 DALI 的加速比为 1.05 倍)。
- 8-GPU 设置
- DALI 带来了 1.25 倍的加速。
- 在两种情况下,DALI 的 CPU 使用率都显著更低。
- 2-GPU 设置
- 讨论
- DALI 在多 GPU 训练中显示出优势,因为在这些情况下 CPU 的负载本来会相当大。
- 尽管 CPU 未被完全利用,但存在瓶颈导致执行速度变慢。
谢谢!
如果您想尝试/使用 DALI 流水线框架,请告诉我们!