视频抽帧的全流程GPU异构加速实践

| 2019-05-17

1. 背景
视频已成为内容和广告的主要媒介形式,但目前的视频内容理解或审核等AI能力,主流依然是先抽帧,再基于图像帧做特征提取和预测。抽帧由于步骤多、计算重,在视频AI推理场景很容易成为性能瓶颈。因此,有必要使用硬件加速等手段,来对视频抽帧做极致的性能优化。
 
在腾讯广告的流量中也是如此,视频所占比例逐年快速提升,视频抽帧这里如果出现时耗或吞吐瓶颈(特别是针对高FPS抽帧的情况),很容易影响到后续的特征提取以及模型预测性能,以及整体的GPU利用率。在当前的广告视频AI推理服务中,抽帧往往占据了其中大部分时耗,因此,视频抽帧的性能对于视频内容理解服务的时耗和整体资源开销,有着举足轻重的地位。
 
视频抽帧的几个步骤,计算量非常大,传统的CPU方式抽帧往往受限于CPU整体的计算吞吐,很难满足低时延高性能要求。使用硬件来做硬解码以及并行计算加速是一个比较理想的替代方案,NVIDIA的GPU从2014年发布的Maxwell架构开始,即增加了单独的硬件编解码计算单元,并且GPU上为数众多的CUDA core也特别适用于图像数据并行处理加速。目前云上广泛使用的推理芯片Tesla T4,解码器已经发展到第四代,包含两个独立于CUDA core的解码单元,且支持大部分主流的视频格式。

<center>▲ NVIDIA GPU NVDEC Architecture</center>
2. 目标
视频抽帧流程大体上包括以下几个步骤:视频解码、帧色彩空间转换、落盘方式的JPEG编码,如果非落盘,则对解码出来的视频帧做预处理,然后交给模型进行特征提取或预测。

其中帧色彩空间转换、JPEG编码都涉及像素级别计算,非常适合使用GPU CUDA kernel来做并行计算加速。此外,视频解码后得到的帧都是未经压缩的原始数据,数据量很大,如果解码是在CPU上进行,或者GPU解码后自动传回了CPU,则需要频繁做 device(显存)与 host(主存)之间的原始帧数据来回拷贝,IO时耗长且数据带宽拥塞,导致时延明显增加。
 
因此,该方案的主要目标是尽可能减少host与device间的数据IO交换,做到抽帧过程全流程GPU异构计算,充分利用腾讯云NVIDIA GPU自带的硬件解码单元NVDEC,最大限度减少视频解码对于CPU以及GPU CUDA core占用的同时,尽可能低延时、高吞吐地处理视频抽帧以及后续的模型推理。

<center>▲ NVIDIA 官方给出的T4卡NVDEC解码性能</center>
 
具体来说,本方案主要从计算和IO两个方面着手,解码部分充分利用了GPU通常闲置的NVDEC解码器,其他步骤以像素或像素块计算为主因此使用CUDA kernel做并行加速。IO方面,由于中间过程是原始帧,GPU数据带宽有限,该方案实现了全流程CPU和GPU无帧数据交换,最大程度提升性能和吞吐,确保云上视频AI推理服务的GPU利用率。
 
3. 具体方案
3.1 计算优化
3.1.1 NVDEC硬解码
当前线网主力的GPU推理卡T4、P40,以及后续即将升级的A系列,主流的视频编码格式基本都已支持,各卡型支持的具体格式如下:

调用GPU硬解码主要有两种方式,一种是直接使用NVIDIA官方提供的Video Codec SDK,另一种方式是使用FFmpeg,其已经封装了对GPU硬解码的支持。考虑到目前T4卡对视频格式的支持还不够完善,因此本文使用的是FFmpeg方式,如果遇到GPU不支持的视频格式,只需修改解码器类型即可快速降级到CPU解码方案,CPU和GPU两种模式抽帧的代码逻辑也较为统一。
以下分别以FFmpeg CPU 4、8、16线程,以及GPU硬解码方式,抽取线网100个广告视频做离线测试,平均时耗对比如下:

<center>(注:视频平均大小约15M,平均时长26s,大部分为720P视频;FFmpeg建议最大解码线程数16)</center>
 
分配给GPU模型推理服务的CPU核数一般不会太多,因此以FFmpeg 8线程、2worker(在本文中是指单进程多实例的方式)做性能压测,1000个广告视频测试数据如下:

由此可见,在GPU线上推理环境,如果充分利用T4卡2 x NVDEC硬件解码模块,可在几乎不影响线上服务CPU、CUDA原有workloads计算的情况下,额外增加一倍解码算力,抽帧QPS可在原有基础上翻倍。此处应注意,不同架构GPU所附带的NVDEC硬解模块数不同,并且NVDEC不支持外部再用多线程操作解码,应当根据NVDEC模块数选择正确的多实例多worker进行解码。例如T4卡有2个NVDEC硬解码模块,如果只用单实例,则硬解模块利用率将不会超过50%。(如果服务对吞吐的要求高于时延,则此处GPU硬解码的worker数可以设为n+1,充分压榨硬件解码模块)
 
3.1.2 CUDA 色彩空间转换
视频解码后得到的帧为YUV格式,而通常模型预测或其他后续处理一般需要RGB/BGR像素格式,因此需要做一次色彩空间转换,将YUV帧转换为模型需要的RGB格式。传统方式是调用FFmpeg的swscale模块来实现,但是该方式只支持在CPU进行计算,需要做一次device到host的数据IO,并且非常消耗CPU资源,计算并行度也不高。用Perf采集火焰图分析发现,swscale计算耗时占比接近40%:
YUV 到 RGB 格式的转换是 3x3 的常量矩阵与 YUV 三维向量相乘,即逐像素地将明度 Y、色度 U、浓度 V 三个分量按公式线性变换为 R、G、B 三色值(这里的常量矩阵的值取决于视频所采用的颜色标准,比如 BT.601/BT.709/BT.2020,可参见 Video Codec SDK 里面的示例)。
 
以BT.601为例,YUV到RGB格式的转换公式如下:
 
R = Y + 1.402 (Cr-128) 
G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) 
B = Y + 1.772 (Cb-128)
 
由公式可见,可以很方便地将计算过程改为一维或二维的Block线程块CUDA kernel调用,充分利用GPU数以千计的 CUDA 核心并行计算来做提速。
 
性能:对线网100个广告视频做性能对比评测,CUDA kernel调用相对于CPU的swscale方式平均提速在20倍以上,并且视频清晰度越高,优势越明显。

3.1.3 CUDA JPEG编码
如果是在视频预处理等场景,则需要对抽帧结果做JPEG编码后再落盘保存。JPEG编码具体流程如下:
虽然不同于色彩空间转换的逐像素操作,但也是将整张图片划分为8x8像素的小分块分别进行离散余弦变换、量化、Huffman编码等处理,同样非常适合用GPU CUDA core计算单元来做并行加速。NVIDIA从CUDA Toolkit 10开始也已经封装了nvJPEG模块提供JPEG编码能力。
 
需要说明的是,使用GPU做JPEG编码,与CPU JPEG编码存在一定比例的像素差异。确保JPEG文件头中各项参数一致的情况下(压缩质量、量化表、Huffman表均相同),实测像素差异比在0.5%左右。由于JPEG编码为有损压缩,因此解码后依然存在像素差异,有可能导致模型给出的预测结果存在偏差。例如OCR的目标检测模块,分别使用CPU和GPU编码的JPEG图像作为输入,预测得到的检测框坐标值在部分case上存在一定偏差,从而有概率导致文字识别结果出现不一致。NV工程师给出的答复是GPU的浮点计算单元截取的位数或精度可能与CPU存在一定差异,暂时无法解决。一种可行的解决方案,是模型训练也使用GPU JPEG编码的图片作为输入,保证模型训练和推理的输入一致性,从而确保模型推理效果。
性能:实测线网1000个广告视频,CUDA方式JPEG编码约有15~20倍性能提升,同样清晰度越高性能优势越大:

3.2 IO优化
3.2.1 显存缓存视频帧
FFmpeg使用GPU硬解码后,得到的视频帧格式为AV_PIX_FMT_NV12,通过NV提供的cudaPointerGetAttributes API做指针类型检查,为Host端内存指针。也就是说调用NVDEC模块解码后,默认对视频帧做了一次device到host的传输。

由于这里的视频帧均为未压缩的原始像素帧,且原始视频的所有FPS帧都会做该处理,会占用大量GPU与host端内存的数据带宽。以1080P视频为例,解码后单帧大小约5M,30M视频解码后约700帧,总大小可达到3G+。以T4卡为例,与host间数据传输通道为 x16 PCIe Gen3,数据带宽有限,理论传输速度约16GB/s,解码+传输回GPU做色彩转换来回耗时约180ms x 2,不但增加了时延,而且大量占据了原本就不太宽裕的PCIe带宽。在多worker并行情况下更是容易造成数据带宽拥堵,对线上推理服务整体吞吐有较大影响。
 
下图为使用nvprof采集到的抽帧过程profiling数据,也验证了存在DtoH & HtoD的两次额外帧数据传输。可见device与host间的数据IO受PCIe带宽影响,耗时较长,并且导致CUDA kernel计算时间片连续性差。

如果有办法做到GPU硬解后的视频帧,不默认传回到host端,而是直接缓存在显存等待后续计算,则可以无缝对接后续的模型推理或JPEG落盘,省去device与host端的来回两次数据交换时耗,且大幅减轻GPU与CPU间的数据IO吞吐压力。答案是可行的,查阅相关资料后,发现FFmpeg已经封装了对于GPU硬件缓冲区方式的支持:  

 if (hw_device_ctx == nullptr) {
    (*dec_ctx)->get_format = hw_get_format;

    // 创建硬件加速器的缓冲区
    if (av_hwdevice_ctx_create(&amp;hw_device_ctx,device_type,NULL,NULL,0) < 0) {
        SoError("av_hwdevice_ctx_create fail!");
        return;
    }
    /** 如果使用软解码则默认有一个软解码的缓冲区(获取AVFrame的),而硬解码则可以额外创建硬件解码的缓冲区
     *  这个缓冲区变量为hw_frames_ctx,不手动创建,则在调用avcodec_send_packet()函数内部自动创建一个
     *  但是必须手动赋值硬件解码缓冲区引用hw_device_ctx(是一个AVBufferRef变量)
     *  即hw_device_ctx有值,则使用硬件缓存方式解码
     */
    SoDebug("av_hwdevice_ctx_create end, ctx: %p.", hw_device_ctx);
  }

  (*dec_ctx)->hw_device_ctx = av_buffer_ref(hw_device_ctx);

  // 配置获取硬件加速器像素格式的函数;该函数实际上就是将AVCodec中AVHWCodecConfig中的pix_fmt返回
  enum AVPixelFormat hw_device_pixel;
  enum AVPixelFormat hw_get_format(AVCodecContext *ctx,const enum AVPixelFormat *fmts)
  {
      const enum AVPixelFormat *p;
      for (p = fmts; *p != AV_PIX_FMT_NONE; p++) {
          if (*p == hw_device_pixel) {
            SoDebug("get hw_get_format res: %d", *p);
            return *p;
          }
      }
      SoWarn("Failed to get HW surface format!");
      return AV_PIX_FMT_NONE;
  }
但是使用硬件缓冲区方式后,得到的视频帧格式变为AV_PIX_FMT_CUDA,且Y和UV plane的data linesize也由1088变为1280,需要做相应转换后才能得到常见的NV12或YUV420P格式。这里相关资料非常少,笔者在尝试过程中也踩了不少坑,后续会将相关代码开源出来。完成这里的转换之后,使用cudaPointerGetAttributes检查frame data指针类型,已经是device端指针,由此打通了全流程异构抽帧的关键一环。

通过nvprof抓取到的性能数据可见,cudaMemcpy由之前的DtoH & HtoD来回传输变为一次显存内部的DtoD,时耗由173ms x 2变为25ms,吞吐也有不少提升。此外,CUDA kernel计算时间片的连续性也得到不少改善。

性能:实测线网1000个广告视频,整体性能相较于非硬件缓冲区方式有25%左右的提升,GPU硬解码器NVDEC资源利用率提升约30%:

3.3 工程优化
本文以介绍GPU全流程抽帧方案为主,过程中为了把性能做到极致也涉及到一些工程优化,由于篇幅原因这里只做简单介绍,部分细节会在后续文章中详细展开。
 
通过显存预分配+复用、AVHWDeviceContext缓冲区 & JPEG编码器复用等手段,单次抽帧时耗可再优化百ms级别。
将NVDEC硬解码、色彩空间转换、JPEG编码、模型推理等步骤,利用CUDA多流,并对每个环节做Pipeline overlap并行化处理,可充分释放每个步骤的最大计算性能,进一步提升计算吞吐和资源利用率。

目前有不少算法服务是基于Python进行开发&部署,本方案为保障高性能,使用C++开发。通过pybind11基于C++封装Python抽帧API,保障算法开发部署的灵活性与效率的同时,确保高性能的抽帧能力。跨语言交互细节可参考我之前整理的文章:《给Python算法插上性能的翅膀——pybind11落地实践》
不落盘方式,对接模型推理之前一般需要先做预处理操作,如果要做到全流程GPU,需要将预处理改写为CUDA kernel调用。这里可以将常用的CV类预处理操作封装为CUDA基础函数库,也可以使用NVIDIA已经封装好的NPP模块、DALI预处理加速框架等方案。
4. 整体效果
4.1 全流程时耗对比

相较于CPU 8线程,全流程在latency上有一倍左右的速度优势。吞吐方面,由于几乎不占用PCIe数据带宽,对模型推理等device&host间数据IO基本无阻塞,亦有不少提升。
相较于Python算法常用的ffmpeg-python方式,有数倍性能提升。
4.2 环境相关
FFmpeg
 
编译配置:
 
./configure --enable-gpl --enable-shared --enable-pthreads --enable-cuda --enable-cuvid --enable-nvenc --enable-nonfree --enable-libnpp --enable-libx264 --enable-libfdk_aac --extra-cflags=-I/usr/local/cuda/include --extra-ldflags=-L/usr/local/cuda/lib64
 
运行后有可能报错:
 
ERROR: cuda requested, but not all dependencies are satisfied: ffnvcodec
 
解决方案:4.x的新版,需要单独安装nvcodec:https://git.videolan.org/git/ffmpeg/nv-codec-headers.git
 
测试环境:
 
机型:GPU机型GN7
CPU:20核
ROM:80G
GPU:NVIDIA Tesla T4 x 1
GPU Driver:430.50
Cuda:10.1
FFmpeg:4.3.2
OpenCV:3.4
 
5. 通用解决方案
不同的视频AI算法,对于抽帧有不同的需求,并且抽帧能力对于算法同学来说并非主要研究方向。因此,如果能沉淀出一套较为通用的抽帧解决方案,对于算法同学来说有很大的帮助。目前该方案仍在迭代中,当前具备的特点和优势如下:
 
高性能:硬解+CUDA并行计算加速,较CPU方案快近一倍,较Python版快数倍
全异构:整个pipeline中间过程无CPU&GPU间帧数据交换,避免PCIe带宽成为瓶颈
算力利用:充分利用通常闲置的NVDEC解码芯片,结合工程优化提升资源利用率,降低视频AI部署成本
灵活性:1. 不同的算法部署环境,可灵活配置GPU/CPU worker数,且支持两种模式间无干扰并行工作;2. 同时支持落盘和非落盘两种场景,且一次解码过程可对接多种抽帧参数
兼容性:对于GPU硬解暂不支持的部分格式,支持快速降级到CPU模式抽帧
便捷性:同时支持C++和Python两种调用方式,针对不同部署环境,可通过配置快速修改部署参数
目前我们团队正在参与腾讯太极机器学习平台共建,主要承担的是公共基础模块建设。该解决方案也会作为太极平台的基础抽帧能力组件,与太极的推理加速组件进行整合。
 
6. 结语
本方案从GPU硬件加速的角度出发,分别针对抽帧各步骤做性能分析&计算优化,解决了中间过程大数据量的原始视频帧host与device端数据IO交换问题,避免GPU与CPU间的PCI-E数据带宽瓶颈,真正做到全流程GPU异构抽帧。基于此,可在GPU无缝对接后续的模型推理(不落盘)以及JPEG编码(落盘)两种主流的抽帧使用场景,是实现全流程GPU视频AI推理能力的先决条件。同时,充分利用了GPU推理环境通常闲置的NVDEC解码芯片,对于整体服务时耗、吞吐,以及硬件资源利用率均有不错的提升,降低了云上视频AI推理服务GPU/CPU算力成本,在算力紧缺的AI2.0时代有着非常重要的意义。
 
目前该方案已在腾讯广告多媒体AI的视频人脸服务落地,解决了最主要的抽帧性能瓶颈,满足广告流水对于服务的性能要求。更多视频AI算法特别是高FPS抽帧场景也正在接入优化中。
 
7. 展望
视频抽帧优化是视频AI推理优化中的重要一环,后续的预处理,以及模型推理、后处理等环节如何优化,并且更好地结合到一起从而实现整体上的性能最优,是一个非常大的课题以及值得探索的点,笔者后续会继续分享这方面的经验心得。

编辑:航网科技 来源:腾讯云 本文版权归原作者所有 转载请注明出处

在线客服

微信扫一扫咨询客服


全国免费服务热线
0755-36300002

返回顶部