#include #include #include "Cache.h" #include "CudaDeviceInterface.h" #include "FFMPEGCommon.h" #include "StableABICompat.h" #include "ValidationUtils.h" extern "C" { #include #include } namespace facebook::torchcodec { namespace { static bool g_cuda = register_device_interface( DeviceInterfaceKey(kStableCUDA, /*variant=*/"ffmpeg"), [](const StableDevice& device) { return new CudaDeviceInterface(device); }); // We reuse cuda contexts across VideoDeoder instances. This is because // creating a cuda context is expensive. The cache mechanism is as follows: // 1. There is a cache of size MAX_CONTEXTS_PER_GPU_IN_CACHE cuda contexts for // each GPU. // 2. When we destroy a SingleStreamDecoder instance we release the cuda context // to // the cache if the cache is not full. // 3. When we create a SingleStreamDecoder instance we try to get a cuda context // from // the cache. If the cache is empty we create a new cuda context. // Set to -1 to have an infinitely sized cache. Set it to 0 to disable caching. // Set to a positive number to have a cache of that size. const int MAX_CONTEXTS_PER_GPU_IN_CACHE = -1; PerGpuCache> g_cached_hw_device_ctxs(MAX_CUDA_GPUS, MAX_CONTEXTS_PER_GPU_IN_CACHE); int get_flags_av_hardware_device_context_create() { // 58.26.100 introduced the concept of reusing the existing cuda context // which is much faster and lower memory than creating a new cuda context. #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(58, 26, 100) return AV_CUDA_USE_CURRENT_CONTEXT; #else return 0; #endif } UniqueAVBufferRef get_hardware_device_context(const StableDevice& device) { enum AVHWDeviceType type = av_hwdevice_find_type_by_name("cuda"); STD_TORCH_CHECK(type != AV_HWDEVICE_TYPE_NONE, "Failed to find cuda device"); int device_index = get_device_index(device); UniqueAVBufferRef hardware_device_ctx = g_cached_hw_device_ctxs.get(device); if (hardware_device_ctx) { return hardware_device_ctx; } // Create hardware device context StableDeviceGuard device_guard(device.index()); // We set the device because we may be called from a different thread than // the one that initialized the cuda context. STD_TORCH_CHECK( cudaSetDevice(device_index) == cudaSuccess, "Failed to set CUDA device"); AVBufferRef* hardware_device_ctx_raw = nullptr; std::string device_ordinal = std::to_string(device_index); int err = av_hwdevice_ctx_create( &hardware_device_ctx_raw, type, device_ordinal.c_str(), nullptr, get_flags_av_hardware_device_context_create()); if (err < 0) { /* clang-format off */ STD_TORCH_CHECK( false, "Failed to create specified HW device. This typically happens when ", "your installed FFmpeg doesn't support CUDA (see ", "https://github.com/pytorch/torchcodec#installing-cuda-enabled-torchcodec", "). FFmpeg error: ", get_ffmpeg_error_string_from_error_code(err)); /* clang-format on */ } return UniqueAVBufferRef(hardware_device_ctx_raw); } } // namespace CudaDeviceInterface::CudaDeviceInterface(const StableDevice& device) : DeviceInterface(device) { STD_TORCH_CHECK(g_cuda, "CudaDeviceInterface was not registered!"); STD_TORCH_CHECK( device_.type() == kStableCUDA, "Unsupported device: must be CUDA"); // Resolve unspecified device index (-1) to the actual current CUDA device. device_.set_index(get_device_index(device_)); initialize_cuda_context_with_pytorch(device_); hardware_device_ctx_ = get_hardware_device_context(device_); } CudaDeviceInterface::~CudaDeviceInterface() { if (hardware_device_ctx_) { g_cached_hw_device_ctxs.add_if_cache_has_capacity( device_, std::move(hardware_device_ctx_)); } } void CudaDeviceInterface::initialize( const SharedAVCodecContext& codec_context) { codec_context_ = codec_context; } void CudaDeviceInterface::initialize_video_decoding( const AVStream* av_stream, const UniqueDecodingAVFormatContext& av_format_ctx, [[maybe_unused]] const VideoStreamOptions& video_stream_options) { STD_TORCH_CHECK(av_stream != nullptr, "avStream is null"); time_base_ = av_stream->time_base; cpu_interface_ = create_device_interface(kStableCPU); STD_TORCH_CHECK( cpu_interface_ != nullptr, "Failed to create CPU device interface"); cpu_interface_->initialize(codec_context_); cpu_interface_->initialize_video( av_stream, av_format_ctx, VideoStreamOptions(), {}, /*resizedOutputDims=*/std::nullopt); } void CudaDeviceInterface::initialize_color_conversion( const VideoStreamOptions& video_stream_options, [[maybe_unused]] const std::vector>& transforms, [[maybe_unused]] const std::optional& resized_output_dims) { video_stream_options_ = video_stream_options; } void CudaDeviceInterface::register_hardware_device_with_codec( AVCodecContext* codec_context) { STD_TORCH_CHECK( hardware_device_ctx_, "Hardware device context has not been initialized"); STD_TORCH_CHECK(codec_context != nullptr, "codecContext is null"); codec_context->hw_device_ctx = av_buffer_ref(hardware_device_ctx_.get()); } UniqueAVFrame CudaDeviceInterface::maybe_convert_av_frame_to_nv12_or_rgb24( const AVFrame& av_frame) { // We need FFmpeg filters to handle those conversion cases which are not // directly implemented in CUDA or CPU device interface (in case of a // fallback). // Input frame is on CPU, we will just pass it to CPU device interface, so // skipping filters context as CPU device interface will handle everything for // us. if (av_frame.format != AV_PIX_FMT_CUDA) { return UniqueAVFrame{}; } auto hw_frames_ctx = reinterpret_cast(av_frame.hw_frames_ctx->data); STD_TORCH_CHECK( hw_frames_ctx != nullptr, "The AVFrame does not have a hw_frames_ctx. " "That's unexpected, please report this to the TorchCodec repo."); AVPixelFormat actual_format = hw_frames_ctx->sw_format; // If the frame is already in NV12 format, we don't need to do anything. if (actual_format == AV_PIX_FMT_NV12) { return UniqueAVFrame{}; } AVPixelFormat output_format; std::stringstream filters; unsigned version_int = avfilter_version(); if (version_int < AV_VERSION_INT(8, 0, 103)) { // Color conversion support ('format=' option) was added to scale_cuda from // n5.0. With the earlier version of ffmpeg we have no choice but use CPU // filters. See: // https://github.com/FFmpeg/FFmpeg/commit/62dc5df941f5e196164c151691e4274195523e95 output_format = AV_PIX_FMT_RGB24; auto actual_format_name = av_get_pix_fmt_name(actual_format); STD_TORCH_CHECK( actual_format_name != nullptr, "The actual format of a frame is unknown to FFmpeg. " "That's unexpected, please report this to the TorchCodec repo."); filters << "hwdownload,format=" << actual_format_name; } else { // Actual output color format will be set via filter options output_format = AV_PIX_FMT_CUDA; filters << "scale_cuda=format=nv12:interp_algo=bilinear"; } enum AVPixelFormat frame_format = static_cast(av_frame.format); auto new_config = std::make_unique( av_frame.width, av_frame.height, frame_format, av_frame.sample_aspect_ratio, av_frame.width, av_frame.height, output_format, filters.str(), time_base_, av_buffer_ref(av_frame.hw_frames_ctx)); if (!nv12_conversion_ || *nv12_conversion_config_ != *new_config) { nv12_conversion_ = std::make_unique(*new_config, video_stream_options_); nv12_conversion_config_ = std::move(new_config); } auto filtered_av_frame = nv12_conversion_->convert(av_frame); // If this check fails it means the frame wasn't // reshaped to its expected dimensions by filtergraph. STD_TORCH_CHECK( (filtered_av_frame->width == nv12_conversion_config_->output_width) && (filtered_av_frame->height == nv12_conversion_config_->output_height), "Expected frame from filter graph of ", nv12_conversion_config_->output_width, "x", nv12_conversion_config_->output_height, ", got ", filtered_av_frame->width, "x", filtered_av_frame->height); return filtered_av_frame; } void CudaDeviceInterface::convert_av_frame_to_frame_output( const AVFrame& input_av_frame, FrameOutput& frame_output, std::optional pre_allocated_output_tensor) { validate_pre_allocated_tensor_shape( pre_allocated_output_tensor, FrameDims(input_av_frame.height, input_av_frame.width)); has_decoded_frame_ = true; // All of our CUDA decoding assumes NV12 format. We handle non-NV12 formats by // converting them to NV12. UniqueAVFrame converted_av_frame = maybe_convert_av_frame_to_nv12_or_rgb24(input_av_frame); const AVFrame& av_frame = converted_av_frame ? *converted_av_frame : input_av_frame; if (av_frame.format != AV_PIX_FMT_CUDA) { // The frame's format is AV_PIX_FMT_CUDA if and only if its content is on // the GPU. In this branch, the frame is on the CPU. There are two possible // reasons: // // 1. During maybeConvertAVFrameToNV12OrRGB24(), we had a non-NV12 format // frame and we're on FFmpeg 4.4 or earlier. In such cases, we had to // use CPU filters and we just converted the frame to RGB24. // 2. This is what NVDEC gave us if it wasn't able to decode a frame, for // whatever reason. Typically that happens if the video's encoder isn't // supported by NVDEC. // // In both cases, we have a frame on the CPU. We send the frame back to the // CUDA device when we're done. enum AVPixelFormat frame_format = static_cast(av_frame.format); FrameOutput cpu_frame_output; if (frame_format == AV_PIX_FMT_RGB24) { // Reason 1 above. The frame is already in RGB24, we just need to convert // it to a tensor. cpu_frame_output.data = rgb_av_frame_to_tensor(av_frame); } else { // Reason 2 above. We need to do a full conversion which requires an // actual CPU device. cpu_interface_->convert_av_frame_to_frame_output( av_frame, cpu_frame_output); } // Finally, we need to send the frame back to the GPU. Note that the // pre-allocated tensor is on the GPU, so we can't send that to the CPU // device interface. We copy it over here. if (pre_allocated_output_tensor.has_value()) { torch::stable::copy_( pre_allocated_output_tensor.value(), cpu_frame_output.data); frame_output.data = pre_allocated_output_tensor.value(); } else { frame_output.data = torch::stable::to(cpu_frame_output.data, device_); } using_cpu_fallback_ = true; return; } using_cpu_fallback_ = false; // Above we checked that the AVFrame was on GPU, but that's not enough, we // also need to check that the AVFrame is in AV_PIX_FMT_NV12 format (8 bits), // because this is what our color conversion kernel expects. This SHOULD // be enforced by our call to maybeConvertAVFrameToNV12OrRGB24() above. STD_TORCH_CHECK( av_frame.hw_frames_ctx != nullptr, "The AVFrame does not have a hw_frames_ctx. This should never happen"); AVHWFramesContext* hw_frames_ctx = reinterpret_cast(av_frame.hw_frames_ctx->data); STD_TORCH_CHECK( hw_frames_ctx != nullptr, "The AVFrame does not have a valid hw_frames_ctx. This should never happen"); AVPixelFormat actual_format = hw_frames_ctx->sw_format; STD_TORCH_CHECK( actual_format == AV_PIX_FMT_NV12, "The AVFrame is ", (av_get_pix_fmt_name(actual_format) ? av_get_pix_fmt_name(actual_format) : "unknown"), ", but we expected AV_PIX_FMT_NV12. " "That's unexpected, please report this to the TorchCodec repo."); // Figure out the NVDEC stream from the avFrame's hardware context. // In reality, we know that this stream is hardcoded to be the default stream // by FFmpeg: // https://github.com/FFmpeg/FFmpeg/blob/66e40840d15b514f275ce3ce2a4bf72ec68c7311/libavutil/hwcontext_cuda.c#L387-L388 STD_TORCH_CHECK( hw_frames_ctx->device_ctx != nullptr, "The AVFrame's hw_frames_ctx does not have a device_ctx. "); auto cuda_device_ctx = static_cast(hw_frames_ctx->device_ctx->hwctx); STD_TORCH_CHECK(cuda_device_ctx != nullptr, "The hardware context is null"); cudaStream_t nvdec_stream = // That's always the default stream. Sad. cuda_device_ctx->stream; frame_output.data = convert_yuv_frame_to_rgb( av_frame, device_, nvdec_stream, pre_allocated_output_tensor, FrameDims(av_frame.height, av_frame.width), /*isP016=*/false, /*bitDepth=*/8, cached_color_matrix_); } // inspired by https://github.com/FFmpeg/FFmpeg/commit/ad67ea9 // we have to do this because of an FFmpeg bug where hardware decoding is not // appropriately set, so we just go off and find the matching codec for the CUDA // device std::optional CudaDeviceInterface::find_codec( const AVCodecID& codec_id, bool is_decoder) { void* i = nullptr; const AVCodec* codec = nullptr; while ((codec = av_codec_iterate(&i)) != nullptr) { STD_TORCH_CHECK( codec != nullptr, "codec returned by av_codec_iterate should not be null"); if (is_decoder) { if (codec->id != codec_id || !av_codec_is_decoder(codec)) { continue; } } else { if (codec->id != codec_id || !av_codec_is_encoder(codec)) { continue; } } const AVCodecHWConfig* config = nullptr; for (int j = 0; (config = avcodec_get_hw_config(codec, j)) != nullptr; ++j) { if (config->device_type == AV_HWDEVICE_TYPE_CUDA) { return codec; } } } return std::nullopt; } std::string CudaDeviceInterface::get_details() { // Note: for this interface specifically the fallback is only known after a // frame has been decoded, not before: that's when FFmpeg decides to fallback, // so we can't know earlier. if (!has_decoded_frame_) { return std::string( "FFmpeg CUDA Device Interface. Fallback status unknown (no frames decoded)."); } return std::string("FFmpeg CUDA Device Interface. Using ") + (using_cpu_fallback_ ? "CPU fallback." : "NVDEC."); } // -------------------------------------------------------------------------- // Below are methods exclusive to video encoding: // -------------------------------------------------------------------------- AVPixelFormat CudaDeviceInterface::get_encoding_pixel_format( [[maybe_unused]] const AVCodec& av_codec, const std::optional& user_pixel_format) const { STD_TORCH_CHECK( !user_pixel_format.has_value(), "Video encoding on GPU currently only supports the nv12 pixel format. " "Do not set pixel_format to use nv12 by default."); return CudaDeviceInterface::CUDA_ENCODING_PIXEL_FORMAT; } UniqueAVFrame CudaDeviceInterface::convert_tensor_to_av_frame_for_encoding( const torch::stable::Tensor& tensor, int frame_index, AVCodecContext* codec_context) { STD_TORCH_CHECK( tensor.dim() == 3 && tensor.sizes()[0] == 3, "Expected 3D RGB tensor (CHW format), got ", tensor.dim(), "D tensor"); STD_TORCH_CHECK( tensor.device().type() == kStableCUDA, "Expected tensor on CUDA device, got: ", device_type_name(tensor.device().type())); UniqueAVFrame av_frame(av_frame_alloc()); STD_TORCH_CHECK(av_frame != nullptr, "Failed to allocate AVFrame"); int height = static_cast(tensor.sizes()[1]); int width = static_cast(tensor.sizes()[2]); // TODO-VideoEncoder: (P1) Unify AVFrame creation with CPU method av_frame->format = AV_PIX_FMT_CUDA; av_frame->height = height; av_frame->width = width; av_frame->pts = frame_index; // FFmpeg's av_hwframe_get_buffer is used to allocate memory on CUDA device. // TODO-VideoEncoder: (P2) Consider using pytorch to allocate CUDA memory for // efficiency int ret = av_hwframe_get_buffer(codec_context->hw_frames_ctx, av_frame.get(), 0); STD_TORCH_CHECK( ret >= 0, "Failed to allocate hardware frame: ", get_ffmpeg_error_string_from_error_code(ret)); STD_TORCH_CHECK( av_frame != nullptr && av_frame->data[0] != nullptr, "avFrame must be pre-allocated with CUDA memory"); // TODO VideoEncoder: Investigate ways to avoid this copy torch::stable::Tensor hwc_frame = torch::stable::contiguous(stable_permute(tensor, {1, 2, 0})); float rgb_to_yuv_matrix[3][4]; compute_rgb_to_yuv_matrix( codec_context->colorspace, codec_context->color_range, rgb_to_yuv_matrix); cudaStream_t stream = get_current_cuda_stream(device_.index()); launch_rgb_to_nv12_kernel( hwc_frame.const_data_ptr(), av_frame->data[0], av_frame->data[1], width, height, validate_int64_to_int( hwc_frame.stride(0) * static_cast(hwc_frame.element_size()), "rgbPitch"), av_frame->linesize[0], av_frame->linesize[1], rgb_to_yuv_matrix, stream); av_frame->colorspace = codec_context->colorspace; av_frame->color_range = codec_context->color_range; return av_frame; } // Allocates and initializes AVHWFramesContext, and sets pixel format fields // to enable encoding with CUDA device. The hw_frames_ctx field is needed by // FFmpeg to allocate frames on GPU's memory. void CudaDeviceInterface::setup_hardware_frame_context_for_encoding( AVCodecContext* codec_context) { STD_TORCH_CHECK(codec_context != nullptr, "codecContext is null"); STD_TORCH_CHECK( hardware_device_ctx_, "Hardware device context has not been initialized"); AVBufferRef* hw_frames_ctx_ref = av_hwframe_ctx_alloc(hardware_device_ctx_.get()); STD_TORCH_CHECK( hw_frames_ctx_ref != nullptr, "Failed to allocate hardware frames context for codec"); codec_context->sw_pix_fmt = CudaDeviceInterface::CUDA_ENCODING_PIXEL_FORMAT; // Always set pixel format to support CUDA encoding. codec_context->pix_fmt = AV_PIX_FMT_CUDA; AVHWFramesContext* hw_frames_ctx = reinterpret_cast(hw_frames_ctx_ref->data); hw_frames_ctx->format = codec_context->pix_fmt; hw_frames_ctx->sw_format = codec_context->sw_pix_fmt; hw_frames_ctx->width = codec_context->width; hw_frames_ctx->height = codec_context->height; int ret = av_hwframe_ctx_init(hw_frames_ctx_ref); if (ret < 0) { av_buffer_unref(&hw_frames_ctx_ref); STD_TORCH_CHECK( false, "Failed to initialize CUDA frames context for codec: ", get_ffmpeg_error_string_from_error_code(ret)); } codec_context->hw_frames_ctx = hw_frames_ctx_ref; } } // namespace facebook::torchcodec