简体中文 | English
视频任务相比于图像任务的训练往往更加耗时,其原因主要有两点:
- 数据:视频解码耗时。mp4/mkv等视频文件都是经过encode后的压缩文件,通过需要经过解码和抽帧步骤才能得到原始的图像数据流,之后经过图像变换/增强操作才能将其喂入网络进行训练。如果视频帧数多,解码过程极其耗时。
- 模型:视频任务使用的模型通常有更大的参数量与计算量。为学习时序特征,视频模型一般会使用3D卷积核/(2+1)D/双流网络,这都会使得模型的参数量与计算量大大增加。
本教程介绍如下视频模型训练加速方法:
- 模型上,通过op融合或混合精度训练的方式提升op运算效率
- 数据上,通过多进程或者并行计算的方式加速数据读取速度
- 训练策略上,通过multigrid策略减少训练耗时
- 多机分布式减少训练耗时
以上训练加速方法都已经集成进PaddleVideo中,欢迎试用~
如非特别说明,本教程所有实验的测试环境如下:
GPU: v100,4卡*16G
CPU: Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz
PaddlePaddle: 2.0.0-rc1
Cuda: 10.2
针对TSM模型,我们实现了temporal shift op,在节省显存的同时加速训练过程。
测试方法: 使用不同形状的Tensor,以不同的方式实现temporal shift,记录显存占用和运行时间。
测试代码:
- temporal shift op实现方式
import time
import numpy as np
import paddle
import paddle.nn.functional as F
SHAPE = [32, 16, 32, 32]
#SHAPE = [128, 64, 128, 128]
otl = []
input = paddle.randn(SHAPE)
for i in range(10000):
t1 = time.time()
out1 = F.temporal_shift(x=input, seg_num=2, shift_ratio=0.2)
t2 = time.time()
ot = t2 - t1
if i > 1000:
otl.append(ot)
print("op time: ", sum(otl)/len(otl))
- 组合op实现方式
import time
import numpy as np
import paddle
import paddle.nn.functional as F
SHAPE = [32, 16, 32, 32]
#SHAPE = [128, 64, 128, 128]
def temporal_shift(x, seg_num, shift_ratio):
shape = x.shape #[N*T, C, H, W]
reshape_x = x.reshape((-1, seg_num, shape[1], shape[2], shape[3])) #[N, T, C, H, W]
pad_x = paddle.fluid.layers.pad(reshape_x, [0,0,1,1,0,0,0,0,0,0,]) #[N, T+2, C, H, W]
c1 = int(shape[1] * shift_ratio)
c2 = int(shape[1] * 2 * shift_ratio)
slice1 = pad_x[:, :seg_num, :c1, :, :]
slice2 = pad_x[:, 2:seg_num+2, c1:c2, :, :]
slice3 = pad_x[:, 1:seg_num+1, c2:, :, :]
concat_x = paddle.concat([slice1, slice2, slice3], axis=2) #[N, T, C, H, W]
return concat_x.reshape(shape)
ctl = []
input = paddle.randn(SHAPE)
for i in range(10000):
t2 = time.time()
out2 = temporal_shift(x=input, seg_num=2, shift_ratio=0.2)
t3 = time.time()
ct = t3 - t2
if i > 1000:
ctl.append(ct)
print("combine time: ", sum(ctl)/len(ctl))
性能数据如下:
输入tensor形状 | 实现方式 | 显存占用/M | 计算时间/s | 加速比 |
---|---|---|---|---|
32*16*32*32 | op组合方式 | 1074 | 0.00029325 | baseline |
32*16*32*32 | temporal shift op | 1058 | 0.000045770 | 6.4x |
128*64*128*128 | op组合方式 | 5160 | 0.0099088 | baseline |
128*64*128*128 | temporal shift op | 2588 | 0.0018617 | 5.3x |
Comming soon~
对于单机训练,视频模型的训练瓶颈大多是在数据预处理上,因此本节主要介绍在数据处理上的一些加速经验。
视频在喂入网络之前,需要经过一系列的数据预处理操作得到数据流,这些操作通常包括:
- 解码: 将视频文件解码成数据流
- 抽帧: 从视频中抽取部分帧用于网络训练
- 数据增强:缩放、裁剪、随机翻转、正则化
其中解码是最为耗时的。相较于传统的opencv或pyAV解码库,这里推荐使用性能更优的解码库decord。目前SlowFast模型使用decord进行视频解码(源码),对单进程的速度提升有较大作用。
我们分别以opencv/decord为解码器,实现SlowFast模型数据预处理pipeline,然后随机从kinetics-400数据集中选取200条视频,计算各pipeline处理每条视频的平均时间。
性能测试数据如下:
解码库 | 版本 | pipeline处理每条视频的平均时间/s | 加速比 |
---|---|---|---|
opencv | 4.2.0 | 0.20965035 | baseline |
decord | 0.4.2 | 0.13788146 | 1.52x |
数据准备好后喂入网络进行训练,网络运算使用GPU并行加速相对较快。对于单个进程来说,速度瓶颈大多在数据处理部分,GPU大部分时间是在等待CPU完成数据预处理。 飞桨2.0使用Dataloader进行数据加载,DataLoader支持单进程和多进程的数据加载方式,当 num_workers 大于0时,将使用多进程方式异步加载数据。多进程加速协作,可以overlap掉GPU大部分等待的时间,提升GPU利用率,显著加速训练过程。
我们分别设置num_workers为0或4,单卡batch_size统一设置为8,统计训练一个batch的平均耗时。
性能测试数据对比如下:
卡数 | 单卡num_workers | batch_cost/s | ips | 加速比 |
---|---|---|---|---|
单卡 | 0 | 1.763 | 4.53887 | 单卡baseline |
单卡 | 4 | 0.578 | 13.83729 | 3.04x |
4卡 | 0 | 1.866 | 4.28733 | 多卡baseline |
4卡 | 4 | 0.615 | 13.00625 | 3.03x |
其中ips = batch_size/batch_cost,即为训练一个instance(一个video)的平均耗时。
结合使用decord和飞桨dataloader,加上在数据增强部分做一些细节优化,SlowFast模型训练速度增益为100%,详细数据可以参考benchmark。
既然GPU等待CPU进行数据处理耗时,能否把数据处理放到GPU上呢?NVIDIA DALI将数据预处理pipeline转移到GPU上执行,可以显著提升训练速度。针对视频文件,DALI提供VideoReader
op进行解码抽帧操作,但目前其仅支持连续采样的方式进行抽帧。而视频领域常用的2D模型TSN或TSM,它们均采用分段采样方式,即把视频均匀分成N段segument,然后在每个segument内随机选取一帧,最后把选取的帧组合作为输入张量。为此,我们基于DALI进行了二次开发,实现了支持分段采样方式的VideoReader
op。为方便用户使用,我们提供了配置好的docker运行环境,具体使用方法参考TSN-DALI使用教程。
测试环境:
机器: Tesla v100
显存: 4卡16G
Cuda: 9.0
单卡batch_size: 32
性能测试数据如下:
加速方式 | batch耗时/s | reader耗时/s | ips:instance/sec | 加速比 |
---|---|---|---|---|
DALI | 2.083 | 1.804 | 15.36597 | 1.41x |
Dataloader: 单卡num_workers=4 | 2.943 | 2.649 | 10.87460 | baseline |
pytorch实现 | TODO | TODO | TODO | TODO |
这是一种简单直接的方法,既然视频解码耗时,那可以事先将视频解码好,存成图片,模型训练时直接读取图像即可。这种方法可以显著提升视频模型训练速度,但它也有一个很明显的缺点,就是需要耗费大量的内存空间。以kinetics-400数据集为例,共包含24万个训练样本,mp4文件约130G,解码存成图像后,占用的内存空间约为2T,所以这种方法比较适用于较小规模的数据集,如ucf-101。PaddleVideo提供了预先解码的脚本,并且TSN模型和TSM模型均支持直接使用frame格式的数据进行训练,详细实现参考源码。
测试方法: 数据集选用UCF-101,模型为ppTSM,模型参数参考默认配置pptsm.yaml,Dataloader的num_workers参数设为0,分别以video和frame格式作为输入,单卡训练,性能数据如下:
数据格式 | batch耗时/s | reader耗时/s | ips:instance/sec | reader加速比 | 加速比 |
---|---|---|---|---|---|
frame | 1.008 | 0.591 | 15.87405 | 4.79x | 3.22x |
video | 3.249 | 2.832 | 4.92392 | baseline | baseline |
前述方法大多从工程的角度思考训练速度的提升,在算法策略上,FAIR在CVPR 2020中提出了Multigrid加速策略算法,它的基本思想如下:
在图像分类任务中,若经过预处理后图像的高度和宽度分别为H和W,batch_size为N,则网络输入batch的Tensor形状为[N, C, H, W]
,其中C等于3,指RGB三个通道。
对应到视频任务,由于增加了时序通道,输入batch的Tensor形状为[N, C, T, H, W]
。
传统的训练策略中,每个batch的输入Tensor形状都是固定的,即都是[N, C, T, H, W]
。若以高分辨的图像作为输入,即设置较大的[T, H, W]
,则模型精度会高一些,但训练会更慢;若以低分辨的图像作为输入,即设置较小的[T, H, W]
,则可以使用更大的batch size,训练更快,但模型精度会降低。在一个epoch中,能否让不同batch的输入Tensor的形状动态变化,既能提升训练速度,又能保证模型精度?
基于以上思想,FAIR在实验的基础上提出了Multigrid训练策略: 固定N*C*T*H*W
的值,降低T*H*W
时增大N
的值,增大T*H*W
时减小N
的值。具体包含两种策略:
- Long cycle: 设完整训练需要N个epoch,将整个训练过程分4个阶段,每个阶段对应的输入tensor形状为:
[8N, T/4, H/sqrt(2), W/sqrt(2)], [4N, T/2, H/sqrt(2), W/sqrt(2)], [2N, T/2, H, W], [N, T, H, W]
- Short cycle: 在Long cycle的基础上,Short-cycle让每个iter的输入Tensor形状都会发生变化,变化策略为:
[H/2, W/2], [H/sqrt(2), W/sqrt(2)], [H, W]
我们基于飞桨实现了Multigrid训练加速策略,对SlowFast模型训练进行加速,使用方法请参考文档SlowFast训练加速。
测试环境:
机器: Tesla v100
显存: 8卡32G
Cuda: 9.0
单卡batch_size: 8
数据集: Kinetics-400
Paddle版本: 2.0-rc0
性能数据如下:
训练策略 | 单个epoch平均耗时/min | 训练总时间/min | 加速比 |
---|---|---|---|
Multigrid | 27.25 | 9758(6.7天) | 2.89x |
Normal | 78.76 | 15438(10.7天) | base |
Comming soon~