AI推理定制FFmpeg Nvidia硬编码

407 阅读3分钟

背景介绍

随着业务内容、技术和算法的不断变化,我们所熟知的转码不再是指转码这一个单一行为(也就是不只是把A格式转到B格式,或者把A的码流转到某些分发的格式),而是加入了很多的处理,比如推理、画质的提升或计算。而为了让整个过程更快速更高效,我们需要做全流程GPU,即数据驻留在GPU上,不会在CPU和GPU间来回地拷贝。

针对当前我们公司使用的技术栈来举例,视频工程用到的是ffmpeg,算法工程用到的是libtorch。所以我们需要解决的是数据如何从tensor的gpu数据转移到ffmpeg的hw AVFrame,并进行编码。当然我们也有尝试过使用nvidia video codec sdk来对tensor结果进行编码,在使用一段时间之后发现有两个问题,第一个是nvidia sdk的各种参数比较多,需要进行一定的了解,并且sample代码也需要一定的修改,开发效率不是很高,并且需要长期的维护投入。第二个是之前的软编码使用的是ffmpeg的编码api,和nvidia sdk不太一致,也需要做大量的修改兼容工作。

翻阅了网上很多文章,提到使用FFmpeg Nvidia硬编码的部分,基本上都是需要将输入的数据转换成cpu的内存数据,然后封装成AVFrame传给编码器进行编码,编码器内部将sw avframe转为hw avframe,这样做实际上还是需要做一次GPU到CPU,再CPU到GPU的数据拷贝工作,并没有真正的全流程GPU。

技术调研

接下来我分享一下我这边做的一些使用ffmpeg api来进行nvidia video encode的调研,做到全流程GPU。

一、cuda上下文不一致

首先需要解决tensor cuda上下文和ffmpeg nvenc编码器cuda上下文不一致,导致gpu数据无法相互拷贝的问题。尽管ffmpeg提供了使用primary device context来代替编码器内部创建一个cuda上下文的接口参数(AV_CUDA_USE_PRIMARY_CONTEXT),但是primary device context在实际使用过程中还是会存在一些问题或者说是限制,所以我们在ffmpeg源码上做了一些改动,增加了一个使用外部cuda上下文的宏定义,将tensor中创建的cuda上下文传入给ffmpeg nvenc编码器,这样既不需要考虑gpu数据无法拷贝的问题,也避免了cuda上下文不断切换导致的性能问题;

    if (flags & AV_CUDA_USE_PRIMARY_CONTEXT) {
        ...
        ret = CHECK_CU(cu->cuDevicePrimaryCtxRetain(&hwctx->cuda_ctx,
                                                    hwctx->internal->cuda_device));
        if (ret < 0)
            return ret;
    } else if (flags & AV_CUDA_USE_OUTSIDE_CONTEXT) {
        av_log(device_ctx, AV_LOG_INFO, "outside context will be create.\n");
    } else {
        ...
    }
    AVHWDeviceContext *device_ctx = (AVHWDeviceContext *)p->hw_device_ctx_->data;
    AVCUDADeviceContext *hwctx = (AVCUDADeviceContext *)device_ctx->hwctx;
    CUcontext* pctx = (CUcontext*)(&hwctx->cuda_ctx);
    CUresult ret = cuCtxGetCurrent(pctx);
    if (ret != 0)
        std::cout << "init hwencoder cuCtxGetCurrent failed,ret=" << ret;
    else
        std::cout << "init hwencoder cuCtxGetCurrent cuda_ctx=" << *pctx;

二、tensor rgba数据如何拷贝到hw AVFrame

  AVFrame *hw_frame = av_frame_alloc();
  av_hwframe_get_buffer(hw_frames_ctx, hw_frame, 0);
  void* device_ptr = tensor.data_ptr();
  uint8_t* data = hw_frame->data[0];
  cuMemcpyDtoD((CUdeviceptr)data, (CUdeviceptr)device_ptr, tensor.numel());

三、FFmpeg对GPU memory进行了对齐,导致编码后画面错位的问题

  AVFrame *hw_frame = av_frame_alloc();
  av_hwframe_get_buffer(hw_frames_ctx, hw_frame, 0);
  hw_frame->linesize[0] = 4 * tensor_width;

参考的代码/文章

ffmpeg4.4 doc/examples/vaapi_transcode.c

LiveVideoStack:FFmpeg AI推理+图形渲染的可定制GPU管线