前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >视频抽帧的全流程GPU异构加速实践

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

原创
作者头像
乾彪
修改2022-05-30 14:08:00
6.5K7
修改2022-05-30 14:08:00
举报
文章被收录于专栏:性能优化之路

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硬件缓冲区方式的支持:

代码语言:C++
复制
  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推理优化中的重要一环,后续的预处理,以及模型推理、后处理等环节如何优化,并且更好地结合到一起从而实现整体上的性能最优,是一个非常大的课题以及值得探索的点,笔者后续会继续分享这方面的经验心得。

(该方案已得到NVIDIA官方大力认可,并作为优秀案例进行推广:《腾讯广告视频抽帧的全流程GPU加速》

最后,给个人公众号打个小广告,主要分享AI工程领域的加速、性能优化实践经验总结。干货以及个人思考为主,欢迎大家关注&技术交流:

8. 参考资料

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 背景
  • 2. 目标
  • 3. 具体方案
    • 3.1 计算优化
      • 3.1.1 NVDEC硬解码
      • 3.1.2 CUDA 色彩空间转换
      • 3.1.3 CUDA JPEG编码
    • 3.2 IO优化
      • 3.2.1 显存缓存视频帧
    • 3.3 工程优化
    • 4. 整体效果
      • 4.1 全流程时耗对比
        • 4.2 环境相关
        • 5. 通用解决方案
        • 6. 结语
        • 7. 展望
        • 8. 参考资料
        相关产品与服务
        GPU 云服务器
        GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于生成式AI,自动驾驶,深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档