From c9c56a27634a72a9476f1296dc5e04a01e26e8a4 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Wed, 15 Oct 2025 16:17:31 +0100 Subject: [PATCH 01/20] Address some TODONVDEC (#960) --- .../_core/BetaCudaDeviceInterface.cpp | 10 +++---- src/torchcodec/_core/CUDACommon.cpp | 27 ++++++++++++++----- src/torchcodec/_core/CUDACommon.h | 7 ++++- src/torchcodec/_core/Cache.h | 26 +++++------------- src/torchcodec/_core/CudaDeviceInterface.cpp | 9 +++---- src/torchcodec/_core/NVDECCache.cpp | 16 +++-------- src/torchcodec/_core/NVDECCache.h | 8 ++---- test/utils.py | 9 +++---- 8 files changed, 49 insertions(+), 63 deletions(-) diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp index 78fa8d635..adf6add28 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp @@ -216,12 +216,11 @@ BetaCudaDeviceInterface::~BetaCudaDeviceInterface() { // unclear. flush(); unmapPreviousFrame(); - NVDECCache::getCache(device_.index()) - .returnDecoder(&videoFormat_, std::move(decoder_)); + NVDECCache::getCache(device_).returnDecoder( + &videoFormat_, std::move(decoder_)); } if (videoParser_) { - // TODONVDEC P2: consider caching this? Does DALI do that? cuvidDestroyVideoParser(videoParser_); videoParser_ = nullptr; } @@ -362,11 +361,12 @@ int BetaCudaDeviceInterface::streamPropertyChange(CUVIDEOFORMAT* videoFormat) { } if (!decoder_) { - decoder_ = NVDECCache::getCache(device_.index()).getDecoder(videoFormat); + decoder_ = NVDECCache::getCache(device_).getDecoder(videoFormat); if (!decoder_) { // TODONVDEC P2: consider re-configuring an existing decoder instead of - // re-creating one. See docs, see DALI. + // re-creating one. See docs, see DALI. Re-configuration doesn't seem to + // be enabled in DALI by default. decoder_ = createDecoder(videoFormat); } diff --git a/src/torchcodec/_core/CUDACommon.cpp b/src/torchcodec/_core/CUDACommon.cpp index 4f3664031..4532e3c76 100644 --- a/src/torchcodec/_core/CUDACommon.cpp +++ b/src/torchcodec/_core/CUDACommon.cpp @@ -5,14 +5,12 @@ // LICENSE file in the root directory of this source tree. #include "src/torchcodec/_core/CUDACommon.h" +#include "src/torchcodec/_core/Cache.h" // for PerGpuCache namespace facebook::torchcodec { namespace { -// Pytorch can only handle up to 128 GPUs. -// https://github.com/pytorch/pytorch/blob/e30c55ee527b40d67555464b9e402b4b7ce03737/c10/cuda/CUDAMacros.h#L44 -const int MAX_CUDA_GPUS = 128; // 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; @@ -249,7 +247,7 @@ torch::Tensor convertNV12FrameToRGB( } UniqueNppContext getNppStreamContext(const torch::Device& device) { - torch::DeviceIndex nonNegativeDeviceIndex = getNonNegativeDeviceIndex(device); + int deviceIndex = getDeviceIndex(device); UniqueNppContext nppCtx = g_cached_npp_ctxs.get(device); if (nppCtx) { @@ -266,13 +264,13 @@ UniqueNppContext getNppStreamContext(const torch::Device& device) { nppCtx = std::make_unique(); cudaDeviceProp prop{}; - cudaError_t err = cudaGetDeviceProperties(&prop, nonNegativeDeviceIndex); + cudaError_t err = cudaGetDeviceProperties(&prop, deviceIndex); TORCH_CHECK( err == cudaSuccess, "cudaGetDeviceProperties failed: ", cudaGetErrorString(err)); - nppCtx->nCudaDeviceId = nonNegativeDeviceIndex; + nppCtx->nCudaDeviceId = deviceIndex; nppCtx->nMultiProcessorCount = prop.multiProcessorCount; nppCtx->nMaxThreadsPerMultiProcessor = prop.maxThreadsPerMultiProcessor; nppCtx->nMaxThreadsPerBlock = prop.maxThreadsPerBlock; @@ -312,4 +310,21 @@ void validatePreAllocatedTensorShape( } } +int getDeviceIndex(const torch::Device& device) { + // PyTorch uses int8_t as its torch::DeviceIndex, but FFmpeg and CUDA + // libraries use int. So we use int, too. + int deviceIndex = static_cast(device.index()); + TORCH_CHECK( + deviceIndex >= -1 && deviceIndex < MAX_CUDA_GPUS, + "Invalid device index = ", + deviceIndex); + + if (deviceIndex == -1) { + TORCH_CHECK( + cudaGetDevice(&deviceIndex) == cudaSuccess, + "Failed to get current CUDA device."); + } + return deviceIndex; +} + } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/CUDACommon.h b/src/torchcodec/_core/CUDACommon.h index b935cd4bf..588f60e49 100644 --- a/src/torchcodec/_core/CUDACommon.h +++ b/src/torchcodec/_core/CUDACommon.h @@ -11,7 +11,6 @@ #include #include -#include "src/torchcodec/_core/Cache.h" #include "src/torchcodec/_core/FFMPEGCommon.h" #include "src/torchcodec/_core/Frame.h" @@ -22,6 +21,10 @@ extern "C" { namespace facebook::torchcodec { +// Pytorch can only handle up to 128 GPUs. +// https://github.com/pytorch/pytorch/blob/e30c55ee527b40d67555464b9e402b4b7ce03737/c10/cuda/CUDAMacros.h#L44 +constexpr int MAX_CUDA_GPUS = 128; + void initializeCudaContextWithPytorch(const torch::Device& device); // Unique pointer type for NPP stream context @@ -43,4 +46,6 @@ void validatePreAllocatedTensorShape( const std::optional& preAllocatedOutputTensor, const UniqueAVFrame& avFrame); +int getDeviceIndex(const torch::Device& device); + } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/Cache.h b/src/torchcodec/_core/Cache.h index 7b088a145..b2c93e8ea 100644 --- a/src/torchcodec/_core/Cache.h +++ b/src/torchcodec/_core/Cache.h @@ -95,30 +95,16 @@ class PerGpuCache { std::vector>> cache_; }; -// Note: this function is inline for convenience, not performance. Because the -// rest of this file is template functions, they must all be defined in this -// header. This function is not a template function, and should, in principle, -// be defined in a .cpp file to preserve the One Definition Rule. That's -// annoying for such a small amount of code, so we just inline it. If this file -// grows, and there are more such functions, we should break them out into a -// .cpp file. -inline torch::DeviceIndex getNonNegativeDeviceIndex( - const torch::Device& device) { - torch::DeviceIndex deviceIndex = device.index(); - // For single GPU machines libtorch returns -1 for the device index. So for - // that case we set the device index to 0. That's used in per-gpu cache - // implementation and during initialization of CUDA and FFmpeg contexts - // which require non negative indices. - deviceIndex = std::max(deviceIndex, 0); - TORCH_CHECK(deviceIndex >= 0, "Device index out of range"); - return deviceIndex; -} +// Forward declaration of getDeviceIndex which exists in CUDACommon.h +// This avoids circular dependency between Cache.h and CUDACommon.cpp which also +// needs to include Cache.h +int getDeviceIndex(const torch::Device& device); template bool PerGpuCache::addIfCacheHasCapacity( const torch::Device& device, element_type&& obj) { - torch::DeviceIndex deviceIndex = getNonNegativeDeviceIndex(device); + int deviceIndex = getDeviceIndex(device); TORCH_CHECK( static_cast(deviceIndex) < cache_.size(), "Device index out of range"); @@ -128,7 +114,7 @@ bool PerGpuCache::addIfCacheHasCapacity( template typename PerGpuCache::element_type PerGpuCache::get( const torch::Device& device) { - torch::DeviceIndex deviceIndex = getNonNegativeDeviceIndex(device); + int deviceIndex = getDeviceIndex(device); TORCH_CHECK( static_cast(deviceIndex) < cache_.size(), "Device index out of range"); diff --git a/src/torchcodec/_core/CudaDeviceInterface.cpp b/src/torchcodec/_core/CudaDeviceInterface.cpp index aea2b2d9a..aee1ecd07 100644 --- a/src/torchcodec/_core/CudaDeviceInterface.cpp +++ b/src/torchcodec/_core/CudaDeviceInterface.cpp @@ -32,9 +32,6 @@ static bool g_cuda = registerDeviceInterface( // from // the cache. If the cache is empty we create a new cuda context. -// Pytorch can only handle up to 128 GPUs. -// https://github.com/pytorch/pytorch/blob/e30c55ee527b40d67555464b9e402b4b7ce03737/c10/cuda/CUDAMacros.h#L44 -const int MAX_CUDA_GPUS = 128; // 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; @@ -54,7 +51,7 @@ int getFlagsAVHardwareDeviceContextCreate() { UniqueAVBufferRef getHardwareDeviceContext(const torch::Device& device) { enum AVHWDeviceType type = av_hwdevice_find_type_by_name("cuda"); TORCH_CHECK(type != AV_HWDEVICE_TYPE_NONE, "Failed to find cuda device"); - torch::DeviceIndex nonNegativeDeviceIndex = getNonNegativeDeviceIndex(device); + int deviceIndex = getDeviceIndex(device); UniqueAVBufferRef hardwareDeviceCtx = g_cached_hw_device_ctxs.get(device); if (hardwareDeviceCtx) { @@ -68,9 +65,9 @@ UniqueAVBufferRef getHardwareDeviceContext(const torch::Device& device) { // So we ensure the deviceIndex is not negative. // We set the device because we may be called from a different thread than // the one that initialized the cuda context. - cudaSetDevice(nonNegativeDeviceIndex); + cudaSetDevice(deviceIndex); AVBufferRef* hardwareDeviceCtxRaw = nullptr; - std::string deviceOrdinal = std::to_string(nonNegativeDeviceIndex); + std::string deviceOrdinal = std::to_string(deviceIndex); int err = av_hwdevice_ctx_create( &hardwareDeviceCtxRaw, diff --git a/src/torchcodec/_core/NVDECCache.cpp b/src/torchcodec/_core/NVDECCache.cpp index 87ab5b0dc..302433cd4 100644 --- a/src/torchcodec/_core/NVDECCache.cpp +++ b/src/torchcodec/_core/NVDECCache.cpp @@ -7,6 +7,7 @@ #include #include +#include "src/torchcodec/_core/CUDACommon.h" #include "src/torchcodec/_core/FFMPEGCommon.h" #include "src/torchcodec/_core/NVDECCache.h" @@ -19,20 +20,9 @@ extern "C" { namespace facebook::torchcodec { -NVDECCache& NVDECCache::getCache(int deviceIndex) { - const int MAX_CUDA_GPUS = 128; - TORCH_CHECK( - deviceIndex >= -1 && deviceIndex < MAX_CUDA_GPUS, - "Invalid device index = ", - deviceIndex); +NVDECCache& NVDECCache::getCache(const torch::Device& device) { static NVDECCache cacheInstances[MAX_CUDA_GPUS]; - if (deviceIndex == -1) { - // TODO NVDEC P3: Unify with existing getNonNegativeDeviceIndex() - TORCH_CHECK( - cudaGetDevice(&deviceIndex) == cudaSuccess, - "Failed to get current CUDA device."); - } - return cacheInstances[deviceIndex]; + return cacheInstances[getDeviceIndex(device)]; } UniqueCUvideodecoder NVDECCache::getDecoder(CUVIDEOFORMAT* videoFormat) { diff --git a/src/torchcodec/_core/NVDECCache.h b/src/torchcodec/_core/NVDECCache.h index 17fc99902..b248ebc68 100644 --- a/src/torchcodec/_core/NVDECCache.h +++ b/src/torchcodec/_core/NVDECCache.h @@ -11,6 +11,7 @@ #include #include +#include #include "src/torchcodec/_core/nvcuvid_include/cuviddec.h" #include "src/torchcodec/_core/nvcuvid_include/nvcuvid.h" @@ -36,7 +37,7 @@ using UniqueCUvideodecoder = // per GPU device, and it is accessed through the static getCache() method. class NVDECCache { public: - static NVDECCache& getCache(int deviceIndex); + static NVDECCache& getCache(const torch::Device& device); // Get decoder from cache - returns nullptr if none available UniqueCUvideodecoder getDecoder(CUVIDEOFORMAT* videoFormat); @@ -68,11 +69,6 @@ class NVDECCache { CacheKey(const CacheKey&) = default; CacheKey& operator=(const CacheKey&) = default; - // TODONVDEC P2: we only implement operator< which is enough for std::map, - // but: - // - we should consider using std::unordered_map - // - we should consider a more sophisticated and potentially less strict - // cache key comparison logic bool operator<(const CacheKey& other) const { return std::tie( codecType, diff --git a/test/utils.py b/test/utils.py index 7c91f307c..7fb00ab59 100644 --- a/test/utils.py +++ b/test/utils.py @@ -41,9 +41,6 @@ def unsplit_device_str(device_str: str) -> str: # It is used: # - before calling `.to(device)` where device can't be "cuda:0:beta" # - before calling add_video_stream(device=device, device_variant=device_variant) - # - # TODONVDEC P2: Find a less clunky way to test the BETA CUDA interface. It - # will ultimately depend on how we want to publicly expose it. if device_str == "cuda:0:beta": return "cuda", "beta" else: @@ -750,7 +747,7 @@ def sample_format(self) -> str: def supports_approximate_mode(asset: TestVideo) -> bool: - # TODONVDEC P2: open an issue about his. That's actually not related to - # NVDEC at all, those don't support approximate mode because they don't set - # a duration. CPU decoder fails too! + # Those are missing the `duration` field so they fail in approximate mode (on all devices). + # TODO: we should address this, see + # https://github.com/meta-pytorch/torchcodec/issues/945 return asset not in (AV1_VIDEO, TEST_SRC_2_720P_VP9, TEST_SRC_2_720P_VP8) From ece7f93343270d1919e6a0464fd388dcf086b8f7 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Wed, 15 Oct 2025 16:19:03 +0100 Subject: [PATCH 02/20] Rename default interface into FFmpeg (#964) --- src/torchcodec/_core/BetaCudaDeviceInterface.cpp | 9 ++++----- src/torchcodec/_core/DeviceInterface.h | 4 ++-- src/torchcodec/_core/SingleStreamDecoder.h | 2 +- src/torchcodec/_core/StreamOptions.h | 4 ++-- src/torchcodec/_core/custom_ops.cpp | 8 ++++---- src/torchcodec/_core/ops.py | 4 ++-- src/torchcodec/decoders/_video_decoder.py | 3 --- test/test_decoders.py | 6 +++--- test/utils.py | 2 +- 9 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp index adf6add28..31317a7fa 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp @@ -129,7 +129,7 @@ static UniqueCUvideodecoder createDecoder(CUVIDEOFORMAT* videoFormat) { // automatically converted to 8bits by NVDEC itself. That is, the raw frames // we get back from cuvidMapVideoFrame will already be in 8bit format. We // won't need to do the conversion ourselves, so that's a lot easier. - // In the default interface, we have to do the 10 -> 8bits conversion + // In the ffmpeg CUDA interface, we have to do the 10 -> 8bits conversion // ourselves later in convertAVFrameToFrameOutput(), because FFmpeg explicitly // requests 10 or 16bits output formats for >8-bit videos! // https://github.com/FFmpeg/FFmpeg/blob/e05f8acabff468c1382277c1f31fa8e9d90c3202/libavcodec/nvdec.c#L376-L403 @@ -480,8 +480,7 @@ int BetaCudaDeviceInterface::receiveFrame(UniqueAVFrame& avFrame) { procParams.top_field_first = dispInfo.top_field_first; procParams.unpaired_field = dispInfo.repeat_first_field < 0; // We set the NVDEC stream to the current stream. It will be waited upon by - // the NPP stream before any color conversion. Currently, that syncing logic - // is in the default interface. + // the NPP stream before any color conversion. // Re types: we get a cudaStream_t from PyTorch but it's interchangeable with // CUstream procParams.output_stream = reinterpret_cast( @@ -618,8 +617,8 @@ void BetaCudaDeviceInterface::convertAVFrameToFrameOutput( UniqueAVFrame& avFrame, FrameOutput& frameOutput, std::optional preAllocatedOutputTensor) { - // TODONVDEC P2: we may need to handle 10bit videos the same way the default - // interface does it with maybeConvertAVFrameToNV12OrRGB24(). + // TODONVDEC P2: we may need to handle 10bit videos the same way the CUDA + // ffmpeg interface does it with maybeConvertAVFrameToNV12OrRGB24(). TORCH_CHECK( avFrame->format == AV_PIX_FMT_CUDA, "Expected CUDA format frame from BETA CUDA interface"); diff --git a/src/torchcodec/_core/DeviceInterface.h b/src/torchcodec/_core/DeviceInterface.h index cac29e838..982f7e732 100644 --- a/src/torchcodec/_core/DeviceInterface.h +++ b/src/torchcodec/_core/DeviceInterface.h @@ -21,7 +21,7 @@ namespace facebook::torchcodec { // Key for device interface registration with device type + variant support struct DeviceInterfaceKey { torch::DeviceType deviceType; - std::string_view variant = "default"; // e.g., "default", "beta", etc. + std::string_view variant = "ffmpeg"; // e.g., "ffmpeg", "beta", etc. bool operator<(const DeviceInterfaceKey& other) const { if (deviceType != other.deviceType) { @@ -141,7 +141,7 @@ void validateDeviceInterface( std::unique_ptr createDeviceInterface( const torch::Device& device, - const std::string_view variant = "default"); + const std::string_view variant = "ffmpeg"); torch::Tensor rgbAVFrameToTensor(const UniqueAVFrame& avFrame); diff --git a/src/torchcodec/_core/SingleStreamDecoder.h b/src/torchcodec/_core/SingleStreamDecoder.h index 48821ff09..cf24aa0c3 100644 --- a/src/torchcodec/_core/SingleStreamDecoder.h +++ b/src/torchcodec/_core/SingleStreamDecoder.h @@ -311,7 +311,7 @@ class SingleStreamDecoder { int streamIndex, AVMediaType mediaType, const torch::Device& device = torch::kCPU, - const std::string_view deviceVariant = "default", + const std::string_view deviceVariant = "ffmpeg", std::optional ffmpegThreadCount = std::nullopt); // Returns the "best" stream index for a given media type. The "best" is diff --git a/src/torchcodec/_core/StreamOptions.h b/src/torchcodec/_core/StreamOptions.h index 7728a676e..e5ab256e1 100644 --- a/src/torchcodec/_core/StreamOptions.h +++ b/src/torchcodec/_core/StreamOptions.h @@ -41,8 +41,8 @@ struct VideoStreamOptions { // By default we use CPU for decoding for both C++ and python users. torch::Device device = torch::kCPU; - // Device variant (e.g., "default", "beta", etc.) - std::string_view deviceVariant = "default"; + // Device variant (e.g., "ffmpeg", "beta", etc.) + std::string_view deviceVariant = "ffmpeg"; // Encoding options // TODO-VideoEncoder: Consider adding other optional fields here diff --git a/src/torchcodec/_core/custom_ops.cpp b/src/torchcodec/_core/custom_ops.cpp index 5ba98e2c1..f29f33395 100644 --- a/src/torchcodec/_core/custom_ops.cpp +++ b/src/torchcodec/_core/custom_ops.cpp @@ -43,9 +43,9 @@ TORCH_LIBRARY(torchcodec_ns, m) { m.def( "_create_from_file_like(int file_like_context, str? seek_mode=None) -> Tensor"); m.def( - "_add_video_stream(Tensor(a!) decoder, *, int? num_threads=None, str? dimension_order=None, int? stream_index=None, str device=\"cpu\", str device_variant=\"default\", str transform_specs=\"\", (Tensor, Tensor, Tensor)? custom_frame_mappings=None, str? color_conversion_library=None) -> ()"); + "_add_video_stream(Tensor(a!) decoder, *, int? num_threads=None, str? dimension_order=None, int? stream_index=None, str device=\"cpu\", str device_variant=\"ffmpeg\", str transform_specs=\"\", (Tensor, Tensor, Tensor)? custom_frame_mappings=None, str? color_conversion_library=None) -> ()"); m.def( - "add_video_stream(Tensor(a!) decoder, *, int? num_threads=None, str? dimension_order=None, int? stream_index=None, str device=\"cpu\", str device_variant=\"default\", str transform_specs=\"\", (Tensor, Tensor, Tensor)? custom_frame_mappings=None) -> ()"); + "add_video_stream(Tensor(a!) decoder, *, int? num_threads=None, str? dimension_order=None, int? stream_index=None, str device=\"cpu\", str device_variant=\"ffmpeg\", str transform_specs=\"\", (Tensor, Tensor, Tensor)? custom_frame_mappings=None) -> ()"); m.def( "add_audio_stream(Tensor(a!) decoder, *, int? stream_index=None, int? sample_rate=None, int? num_channels=None) -> ()"); m.def("seek_to_pts(Tensor(a!) decoder, float seconds) -> ()"); @@ -319,7 +319,7 @@ void _add_video_stream( std::optional dimension_order = std::nullopt, std::optional stream_index = std::nullopt, std::string_view device = "cpu", - std::string_view device_variant = "default", + std::string_view device_variant = "ffmpeg", std::string_view transform_specs = "", std::optional> custom_frame_mappings = std::nullopt, @@ -376,7 +376,7 @@ void add_video_stream( std::optional dimension_order = std::nullopt, std::optional stream_index = std::nullopt, std::string_view device = "cpu", - std::string_view device_variant = "default", + std::string_view device_variant = "ffmpeg", std::string_view transform_specs = "", const std::optional>& custom_frame_mappings = std::nullopt) { diff --git a/src/torchcodec/_core/ops.py b/src/torchcodec/_core/ops.py index 44dc89e2b..9ab1410e8 100644 --- a/src/torchcodec/_core/ops.py +++ b/src/torchcodec/_core/ops.py @@ -304,7 +304,7 @@ def _add_video_stream_abstract( dimension_order: Optional[str] = None, stream_index: Optional[int] = None, device: str = "cpu", - device_variant: str = "default", + device_variant: str = "ffmpeg", transform_specs: str = "", custom_frame_mappings: Optional[ tuple[torch.Tensor, torch.Tensor, torch.Tensor] @@ -322,7 +322,7 @@ def add_video_stream_abstract( dimension_order: Optional[str] = None, stream_index: Optional[int] = None, device: str = "cpu", - device_variant: str = "default", + device_variant: str = "ffmpeg", transform_specs: str = "", custom_frame_mappings: Optional[ tuple[torch.Tensor, torch.Tensor, torch.Tensor] diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 331c7ba79..0229ea459 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -147,9 +147,6 @@ def __init__( device = str(device) device_variant = _get_cuda_backend() - if device_variant == "ffmpeg": - # TODONVDEC P2 rename 'default' into 'ffmpeg' everywhere. - device_variant = "default" # Legacy support for device="cuda:0:beta" syntax # TODONVDEC P2: remove support for this everywhere. This will require diff --git a/test/test_decoders.py b/test/test_decoders.py index 300c953bf..f9c7d2ff6 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1303,7 +1303,7 @@ def test_10bit_videos(self, device, asset): # RuntimeError: Codec configuration not supported on this GPU. # Codec: 4, chroma format: 1, bit depth: 10 # - # It works on the default interface because FFmpeg fallsback to the + # It works on the ffmpeg interface because FFmpeg fallsback to the # CPU, while the BETA interface doesn't. pytest.skip("Asset not supported by NVDEC") @@ -1692,8 +1692,8 @@ def test_beta_cuda_interface_backwards(self, asset, seek_mode): @needs_cuda def test_beta_cuda_interface_small_h265(self): # Test to illustrate current difference in behavior between the BETA and - # the default interface: this video isn't supported by NVDEC, but in the - # default interface, FFMPEG fallsback to the CPU while we don't. + # the ffmpeg interface: this video isn't supported by NVDEC, but in the + # ffmpeg interface, FFMPEG fallsback to the CPU while we don't. VideoDecoder(H265_VIDEO.path, device="cuda").get_frame_at(0) with pytest.raises( diff --git a/test/utils.py b/test/utils.py index 7fb00ab59..b9b003239 100644 --- a/test/utils.py +++ b/test/utils.py @@ -44,7 +44,7 @@ def unsplit_device_str(device_str: str) -> str: if device_str == "cuda:0:beta": return "cuda", "beta" else: - return device_str, "default" + return device_str, "ffmpeg" def get_ffmpeg_major_version(): From a05f902d6a33c79c388fb1228929be48c3ba4d7d Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 16 Oct 2025 12:26:53 +0100 Subject: [PATCH 03/20] Remove legacy support of `device='cuda:0:beta'` (#965) --- src/torchcodec/decoders/_video_decoder.py | 7 -- test/test_decoders.py | 130 ++++++++++++---------- test/utils.py | 37 +++++- 3 files changed, 105 insertions(+), 69 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 0229ea459..130927c2e 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -148,13 +148,6 @@ def __init__( device_variant = _get_cuda_backend() - # Legacy support for device="cuda:0:beta" syntax - # TODONVDEC P2: remove support for this everywhere. This will require - # updating our tests. - if device == "cuda:0:beta": - device = "cuda:0" - device_variant = "beta" - core.add_video_stream( self._decoder, stream_index=stream_index, diff --git a/test/test_decoders.py b/test/test_decoders.py index f9c7d2ff6..32367b438 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -35,6 +35,7 @@ H265_10BITS, H265_VIDEO, in_fbcode, + make_video_decoder, NASA_AUDIO, NASA_AUDIO_MP3, NASA_AUDIO_MP3_44100, @@ -51,7 +52,6 @@ TEST_SRC_2_720P_MPEG4, TEST_SRC_2_720P_VP8, TEST_SRC_2_720P_VP9, - unsplit_device_str, ) @@ -179,13 +179,12 @@ def test_create_fails(self): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_getitem_int(self, num_ffmpeg_threads, device, seek_mode): - decoder = VideoDecoder( + decoder, device = make_video_decoder( NASA_VIDEO.path, num_ffmpeg_threads=num_ffmpeg_threads, device=device, seek_mode=seek_mode, ) - device, _ = unsplit_device_str(device) ref_frame0 = NASA_VIDEO.get_frame_data_by_index(0).to(device) ref_frame1 = NASA_VIDEO.get_frame_data_by_index(1).to(device) @@ -230,8 +229,9 @@ def test_getitem_numpy_int(self): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_getitem_slice(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) - device, _ = unsplit_device_str(device) + decoder, device = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) # ensure that the degenerate case of a range of size 1 works @@ -391,7 +391,9 @@ def test_device_instance(self): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_getitem_fails(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) + decoder, _ = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) with pytest.raises(IndexError, match="Invalid frame index"): frame = decoder[1000] # noqa @@ -408,8 +410,9 @@ def test_getitem_fails(self, device, seek_mode): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_iteration(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) - device, _ = unsplit_device_str(device) + decoder, device = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) ref_frame0 = NASA_VIDEO.get_frame_data_by_index(0).to(device) ref_frame1 = NASA_VIDEO.get_frame_data_by_index(1).to(device) @@ -456,8 +459,9 @@ def test_iteration_slow(self): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frame_at(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) - device, _ = unsplit_device_str(device) + decoder, device = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) ref_frame9 = NASA_VIDEO.get_frame_data_by_index(9).to(device) frame9 = decoder.get_frame_at(9) @@ -494,7 +498,7 @@ def test_get_frame_at(self, device, seek_mode): @pytest.mark.parametrize("device", all_supported_devices()) def test_get_frame_at_tuple_unpacking(self, device): - decoder = VideoDecoder(NASA_VIDEO.path, device=device) + decoder, _ = make_video_decoder(NASA_VIDEO.path, device=device) frame = decoder.get_frame_at(50) data, pts, duration = decoder.get_frame_at(50) @@ -506,7 +510,9 @@ def test_get_frame_at_tuple_unpacking(self, device): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frame_at_fails(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) + decoder, _ = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) with pytest.raises( IndexError, @@ -520,8 +526,9 @@ def test_get_frame_at_fails(self, device, seek_mode): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frames_at(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) - device, _ = unsplit_device_str(device) + decoder, device = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) # test positive and negative frame index frames = decoder.get_frames_at([35, 25, -1, -2]) @@ -572,7 +579,9 @@ def test_get_frames_at(self, device, seek_mode): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frames_at_fails(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) + decoder, _ = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) with pytest.raises( IndexError, @@ -596,8 +605,7 @@ def test_get_frame_at_av1(self, device): if "cuda" in device and in_fbcode(): pytest.skip("decoding on CUDA is not supported internally") - decoder = VideoDecoder(AV1_VIDEO.path, device=device) - device, _ = unsplit_device_str(device) + decoder, device = make_video_decoder(AV1_VIDEO.path, device=device) ref_frame10 = AV1_VIDEO.get_frame_data_by_index(10) ref_frame_info10 = AV1_VIDEO.get_frame_info(10) decoded_frame10 = decoder.get_frame_at(10) @@ -608,8 +616,9 @@ def test_get_frame_at_av1(self, device): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frame_played_at(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) - device, _ = unsplit_device_str(device) + decoder, device = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) ref_frame_played_at_6 = NASA_VIDEO.get_frame_data_by_index(180).to(device) assert_frames_equal( @@ -638,7 +647,9 @@ def test_get_frame_played_at_h265(self): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frame_played_at_fails(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) + decoder, _ = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) with pytest.raises(IndexError, match="Invalid pts in seconds"): frame = decoder.get_frame_played_at(-1.0) # noqa @@ -650,8 +661,9 @@ def test_get_frame_played_at_fails(self, device, seek_mode): @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) @pytest.mark.parametrize("input_type", ("list", "tensor")) def test_get_frames_played_at(self, device, seek_mode, input_type): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) - device, _ = unsplit_device_str(device) + decoder, device = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) # Note: We know the frame at ~0.84s has index 25, the one at 1.16s has # index 35. We use those indices as reference to test against. @@ -693,7 +705,9 @@ def test_get_frames_played_at(self, device, seek_mode, input_type): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frames_played_at_fails(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) + decoder, _ = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) with pytest.raises(RuntimeError, match="must be greater than or equal to"): decoder.get_frames_played_at([-1]) @@ -710,13 +724,12 @@ def test_get_frames_played_at_fails(self, device, seek_mode): @pytest.mark.parametrize("stream_index", [0, 3, None]) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frames_in_range(self, stream_index, device, seek_mode): - decoder = VideoDecoder( + decoder, device = make_video_decoder( NASA_VIDEO.path, stream_index=stream_index, device=device, seek_mode=seek_mode, ) - device, _ = unsplit_device_str(device) # test degenerate case where we only actually get 1 frame ref_frames9 = NASA_VIDEO.get_frame_data_by_range( @@ -815,13 +828,12 @@ def test_get_frames_in_range(self, stream_index, device, seek_mode): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frames_in_range_slice_indices_syntax(self, device, seek_mode): - decoder = VideoDecoder( + decoder, device = make_video_decoder( NASA_VIDEO.path, stream_index=3, device=device, seek_mode=seek_mode, ) - device, _ = unsplit_device_str(device) # high range ends get capped to num_frames frames387_389 = decoder.get_frames_in_range(start=387, stop=1000) @@ -891,13 +903,12 @@ def test_get_frames_with_missing_num_frames_metadata( # Set the return value of the mock to be the mock_stream_dict mock_get_stream_json_metadata.return_value = json.dumps(mock_stream_dict) - decoder = VideoDecoder( + decoder, device = make_video_decoder( NASA_VIDEO.path, stream_index=3, device=device, seek_mode=seek_mode, ) - device, _ = unsplit_device_str(device) assert decoder.metadata.num_frames_from_header is None assert decoder.metadata.num_frames_from_content is None @@ -932,7 +943,7 @@ def test_get_frames_with_missing_num_frames_metadata( @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_dimension_order(self, dimension_order, frame_getter, device, seek_mode): - decoder = VideoDecoder( + decoder, _ = make_video_decoder( NASA_VIDEO.path, dimension_order=dimension_order, device=device, @@ -960,13 +971,12 @@ def test_dimension_order_fails(self): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frames_by_pts_in_range(self, stream_index, device, seek_mode): - decoder = VideoDecoder( + decoder, device = make_video_decoder( NASA_VIDEO.path, stream_index=stream_index, device=device, seek_mode=seek_mode, ) - device, _ = unsplit_device_str(device) # Note that we are comparing the results of VideoDecoder's method: # get_frames_played_in_range() @@ -1100,7 +1110,9 @@ def test_get_frames_by_pts_in_range(self, stream_index, device, seek_mode): @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("seek_mode", ("exact", "approximate")) def test_get_frames_by_pts_in_range_fails(self, device, seek_mode): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode=seek_mode) + decoder, _ = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode=seek_mode + ) with pytest.raises(ValueError, match="Invalid start seconds"): frame = decoder.get_frames_played_in_range(100.0, 1.0) # noqa @@ -1113,7 +1125,9 @@ def test_get_frames_by_pts_in_range_fails(self, device, seek_mode): @pytest.mark.parametrize("device", all_supported_devices()) def test_get_key_frame_indices(self, device): - decoder = VideoDecoder(NASA_VIDEO.path, device=device, seek_mode="exact") + decoder, _ = make_video_decoder( + NASA_VIDEO.path, device=device, seek_mode="exact" + ) key_frame_indices = decoder._get_key_frame_indices() # The key frame indices were generated from the following command: @@ -1134,7 +1148,9 @@ def test_get_key_frame_indices(self, device): key_frame_indices, nasa_reference_key_frame_indices, atol=0, rtol=0 ) - decoder = VideoDecoder(AV1_VIDEO.path, device=device, seek_mode="exact") + decoder, _ = make_video_decoder( + AV1_VIDEO.path, device=device, seek_mode="exact" + ) key_frame_indices = decoder._get_key_frame_indices() # $ ffprobe -v error -hide_banner -select_streams v:0 -show_frames -of csv test/resources/av1_video.mkv | grep -n ",I," | cut -d ':' -f 1 > key_frames.txt @@ -1144,7 +1160,9 @@ def test_get_key_frame_indices(self, device): key_frame_indices, av1_reference_key_frame_indices, atol=0, rtol=0 ) - decoder = VideoDecoder(H265_VIDEO.path, device=device, seek_mode="exact") + decoder, _ = make_video_decoder( + H265_VIDEO.path, device=device, seek_mode="exact" + ) key_frame_indices = decoder._get_key_frame_indices() # ffprobe -v error -hide_banner -select_streams v:0 -show_frames -of csv test/resources/h265_video.mp4 | grep -n ",I," | cut -d ':' -f 1 > key_frames.txt @@ -1158,8 +1176,7 @@ def test_get_key_frame_indices(self, device): @pytest.mark.skipif(in_fbcode(), reason="Compile test fails internally.") @pytest.mark.parametrize("device", all_supported_devices()) def test_compile(self, device): - decoder = VideoDecoder(NASA_VIDEO.path, device=device) - device, _ = unsplit_device_str(device) + decoder, device = make_video_decoder(NASA_VIDEO.path, device=device) @contextlib.contextmanager def restore_capture_scalar_outputs(): @@ -1297,7 +1314,7 @@ def test_10bit_videos(self, device, asset): # This just validates that we can decode 10-bit videos. # TODO validate against the ref that the decoded frames are correct - if device == "cuda:0:beta" and asset is H264_10BITS: + if device == "cuda:beta" and asset is H264_10BITS: # This fails on the BETA interface with: # # RuntimeError: Codec configuration not supported on this GPU. @@ -1307,7 +1324,7 @@ def test_10bit_videos(self, device, asset): # CPU, while the BETA interface doesn't. pytest.skip("Asset not supported by NVDEC") - decoder = VideoDecoder(asset.path, device=device) + decoder, _ = make_video_decoder(asset.path, device=device) decoder.get_frame_at(10) def setup_frame_mappings(tmp_path, file, stream_index): @@ -1346,13 +1363,12 @@ def test_custom_frame_mappings_json_and_bytes( if hasattr(custom_frame_mappings, "read") else contextlib.nullcontext() ) as custom_frame_mappings: - decoder = VideoDecoder( + decoder, device = make_video_decoder( NASA_VIDEO.path, stream_index=stream_index, device=device, custom_frame_mappings=custom_frame_mappings, ) - device, _ = unsplit_device_str(device) frame_0 = decoder.get_frame_at(0) frame_5 = decoder.get_frame_at(5) assert_frames_equal( @@ -1483,9 +1499,8 @@ def test_beta_cuda_interface_get_frame_at( pytest.skip("AV1 CUDA not supported internally") ref_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) - beta_decoder = VideoDecoder( - asset.path, device="cuda:0:beta", seek_mode=seek_mode - ) + with set_cuda_backend("beta"): + beta_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) assert ref_decoder.metadata == beta_decoder.metadata @@ -1531,9 +1546,8 @@ def test_beta_cuda_interface_get_frames_at( pytest.skip("AV1 CUDA not supported internally") ref_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) - beta_decoder = VideoDecoder( - asset.path, device="cuda:0:beta", seek_mode=seek_mode - ) + with set_cuda_backend("beta"): + beta_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) assert ref_decoder.metadata == beta_decoder.metadata @@ -1577,9 +1591,8 @@ def test_beta_cuda_interface_get_frame_played_at(self, asset, seek_mode): pytest.skip("AV1 CUDA not supported internally") ref_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) - beta_decoder = VideoDecoder( - asset.path, device="cuda:0:beta", seek_mode=seek_mode - ) + with set_cuda_backend("beta"): + beta_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) assert ref_decoder.metadata == beta_decoder.metadata @@ -1620,9 +1633,8 @@ def test_beta_cuda_interface_get_frames_played_at(self, asset, seek_mode): pytest.skip("AV1 CUDA not supported internally") ref_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) - beta_decoder = VideoDecoder( - asset.path, device="cuda:0:beta", seek_mode=seek_mode - ) + with set_cuda_backend("beta"): + beta_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) assert ref_decoder.metadata == beta_decoder.metadata @@ -1664,9 +1676,8 @@ def test_beta_cuda_interface_backwards(self, asset, seek_mode): pytest.skip("AV1 CUDA not supported internally") ref_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) - beta_decoder = VideoDecoder( - asset.path, device="cuda:0:beta", seek_mode=seek_mode - ) + with set_cuda_backend("beta"): + beta_decoder = VideoDecoder(asset.path, device="cuda", seek_mode=seek_mode) assert ref_decoder.metadata == beta_decoder.metadata @@ -1696,11 +1707,14 @@ def test_beta_cuda_interface_small_h265(self): # ffmpeg interface, FFMPEG fallsback to the CPU while we don't. VideoDecoder(H265_VIDEO.path, device="cuda").get_frame_at(0) + + with set_cuda_backend("beta"): + dec = VideoDecoder(H265_VIDEO.path, device="cuda") with pytest.raises( RuntimeError, match="Video is too small in at least one dimension. Provided: 128x128 vs supported:144x144", ): - VideoDecoder(H265_VIDEO.path, device="cuda:0:beta").get_frame_at(0) + dec.get_frame_at(0) @needs_cuda def test_beta_cuda_interface_error(self): diff --git a/test/utils.py b/test/utils.py index b9b003239..61bf06295 100644 --- a/test/utils.py +++ b/test/utils.py @@ -14,6 +14,7 @@ import torch from torchcodec._core import get_ffmpeg_library_versions +from torchcodec.decoders import set_cuda_backend, VideoDecoder from torchcodec.decoders._video_decoder import _read_custom_frame_mappings IS_WINDOWS = sys.platform in ("win32", "cygwin") @@ -26,27 +27,55 @@ def needs_cuda(test_item): return pytest.mark.needs_cuda(test_item) +# This is a special device string that we use to test the "beta" CUDA backend. +# It only exists here, in this test utils file. Public and core APIs have no +# idea that this is how we're tesing them. That is, that's not a supported +# `device` parameter for the VideoDecoder or for the _core APIs. +# Tests using all_supported_devices() will get this device string, and the test +# need to clean it up by calling either make_video_decoder for VideoDecoder, or +# unsplit_device_str for core APIs. +_CUDA_BETA_DEVICE_STR = "cuda:beta" + + def all_supported_devices(): return ( "cpu", pytest.param("cuda", marks=pytest.mark.needs_cuda), - pytest.param("cuda:0:beta", marks=pytest.mark.needs_cuda), + pytest.param(_CUDA_BETA_DEVICE_STR, marks=pytest.mark.needs_cuda), ) def unsplit_device_str(device_str: str) -> str: # helper meant to be used as # device, device_variant = unsplit_device_str(device) - # when `device` comes from all_supported_devices() and may be "cuda:0:beta". + # when `device` comes from all_supported_devices() and may be _CUDA_BETA_DEVICE_STR. # It is used: - # - before calling `.to(device)` where device can't be "cuda:0:beta" + # - before calling `.to(device)` where device can't be _CUDA_BETA_DEVICE_STR. # - before calling add_video_stream(device=device, device_variant=device_variant) - if device_str == "cuda:0:beta": + if device_str == _CUDA_BETA_DEVICE_STR: return "cuda", "beta" else: return device_str, "ffmpeg" +def make_video_decoder(*args, **kwargs) -> tuple[VideoDecoder, str]: + # Helper to create a VideoDecoder with the right cuda backend if needed. + # kwargs is expected to have a "device" key which comes from + # all_supported_devices(), and can be _CUDA_BETA_DEVICE_STR. + device = kwargs.pop("device", "cpu") + if device == _CUDA_BETA_DEVICE_STR: + clean_device, backend = "cuda", "beta" + else: + clean_device, backend = device, "ffmpeg" + + # set_cuda_backend is a no-op if the device is "cpu", so we can use it + # unconditionally. + with set_cuda_backend(backend): + dec = VideoDecoder(*args, **kwargs, device=clean_device) + + return dec, clean_device + + def get_ffmpeg_major_version(): ffmpeg_version = get_ffmpeg_library_versions()["ffmpeg_version"] # When building FFmpeg from source there can be a `n` prefix in the version From e5a2876f89558775353002d1345eee9424208951 Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Thu, 16 Oct 2025 10:13:49 -0400 Subject: [PATCH 04/20] Mark slow tests (#968) --- test/test_ops.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/test_ops.py b/test/test_ops.py index fddd4043c..0c1d90cfc 100644 --- a/test/test_ops.py +++ b/test/test_ops.py @@ -1384,7 +1384,9 @@ def decode(self, file_path) -> torch.Tensor: frames, *_ = get_frames_in_range(decoder, start=0, stop=60) return frames - @pytest.mark.parametrize("format", ("mov", "mp4", "mkv", "webm")) + @pytest.mark.parametrize( + "format", ("mov", "mp4", "mkv", pytest.param("webm", marks=pytest.mark.slow)) + ) def test_video_encoder_round_trip(self, tmp_path, format): # Test that decode(encode(decode(asset))) == decode(asset) ffmpeg_version = get_ffmpeg_major_version() @@ -1424,7 +1426,16 @@ def test_video_encoder_round_trip(self, tmp_path, format): @pytest.mark.skipif(in_fbcode(), reason="ffmpeg CLI not available") @pytest.mark.parametrize( - "format", ("mov", "mp4", "avi", "mkv", "webm", "flv", "gif") + "format", + ( + "mov", + "mp4", + "avi", + "mkv", + "flv", + "gif", + pytest.param("webm", marks=pytest.mark.slow), + ), ) def test_video_encoder_against_ffmpeg_cli(self, tmp_path, format): ffmpeg_version = get_ffmpeg_major_version() From 08c3089ad9df6353cb12080498bc7b5bbfd25ac7 Mon Sep 17 00:00:00 2001 From: Dan-Flores Date: Thu, 16 Oct 2025 11:25:53 -0400 Subject: [PATCH 05/20] Update version.txt to 0.9.0a0 (#970) Co-authored-by: Daniel Flores --- README.md | 1 + version.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8050cf2a3..f99679cff 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ The following table indicates the compatibility between versions of | `torchcodec` | `torch` | Python | | ------------------ | ------------------ | ------------------- | | `main` / `nightly` | `main` / `nightly` | `>=3.10`, `<=3.13` | +| `0.8` | `2.9` | `>=3.10`, `<=3.13` | | `0.7` | `2.8` | `>=3.9`, `<=3.13` | | `0.6` | `2.8` | `>=3.9`, `<=3.13` | | `0.5` | `2.7` | `>=3.9`, `<=3.13` | diff --git a/version.txt b/version.txt index a3df0a695..c18d72be3 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.8.0 +0.8.1 \ No newline at end of file From ed4a58675f6e96d496cae032f3bcc86bf90acfaa Mon Sep 17 00:00:00 2001 From: Dan-Flores Date: Thu, 16 Oct 2025 11:37:58 -0400 Subject: [PATCH 06/20] Indicate FFmpeg8 support in docs (#971) Co-authored-by: Daniel Flores --- README.md | 7 ++++--- src/torchcodec/_core/ops.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f99679cff..6c1721036 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,8 @@ ffmpeg -f lavfi -i \ `torch` and `torchcodec`. 2. Install FFmpeg, if it's not already installed. Linux distributions usually - come with FFmpeg pre-installed. TorchCodec supports all major FFmpeg versions - in [4, 7]. + come with FFmpeg pre-installed. TorchCodec supports major FFmpeg versions + in [4, 7] on all platforms, and FFmpeg version 8 is supported on Mac and Linux. If FFmpeg is not already installed, or you need a more recent version, an easy way to install it is to use `conda`: @@ -148,7 +148,8 @@ format you want. Refer to Nvidia's GPU support matrix for more details [here](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new). 1. Install FFmpeg with NVDEC support. - TorchCodec with CUDA should work with FFmpeg versions in [4, 7]. + TorchCodec with CUDA should work with FFmpeg versions in [4, 7] on all platforms, + and FFmpeg version 8 is supported on Linux. If FFmpeg is not already installed, or you need a more recent version, an easy way to install it is to use `conda`: diff --git a/src/torchcodec/_core/ops.py b/src/torchcodec/_core/ops.py index 9ab1410e8..6fc30e5af 100644 --- a/src/torchcodec/_core/ops.py +++ b/src/torchcodec/_core/ops.py @@ -69,7 +69,7 @@ def load_torchcodec_shared_libraries(): raise RuntimeError( f"""Could not load libtorchcodec. Likely causes: 1. FFmpeg is not properly installed in your environment. We support - versions 4, 5, 6 and 7. + versions 4, 5, 6, and 7 on all platforms, and 8 on Mac and Linux. 2. The PyTorch version ({torch.__version__}) is not compatible with this version of TorchCodec. Refer to the version compatibility table: From 03eef72b517f5beeca37363077d66eef78a25d76 Mon Sep 17 00:00:00 2001 From: Silvio Traversaro Date: Thu, 16 Oct 2025 17:44:53 +0200 Subject: [PATCH 07/20] Do not mix defaults and conda-forge in linux_wheel job (#975) --- .github/workflows/linux_wheel.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux_wheel.yaml b/.github/workflows/linux_wheel.yaml index 099a905c4..25a5a564e 100644 --- a/.github/workflows/linux_wheel.yaml +++ b/.github/workflows/linux_wheel.yaml @@ -72,10 +72,14 @@ jobs: name: meta-pytorch_torchcodec__${{ matrix.python-version }}_cpu_x86_64 path: pytorch/torchcodec/dist/ - name: Setup conda env - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true - miniconda-version: "latest" + # Using miniforge instead of miniconda ensures that the default + # conda channel is conda-forge instead of main/default. This ensures + # ABI consistency between dependencies: + # https://conda-forge.org/docs/user/transitioning_from_defaults/ + miniforge-version: latest activate-environment: test python-version: ${{ matrix.python-version }} - name: Update pip From 0d243d63619b4abd24d4cd7fcbc12f3ebfca16c4 Mon Sep 17 00:00:00 2001 From: Molly Xu <64995721+mollyxu@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:59:39 -0400 Subject: [PATCH 08/20] Refactor receiveFrame and sendPacket logic to dispatch directly to interface (#954) Co-authored-by: Molly Xu --- .../_core/BetaCudaDeviceInterface.cpp | 3 +- .../_core/BetaCudaDeviceInterface.h | 7 +-- src/torchcodec/_core/CpuDeviceInterface.cpp | 4 +- src/torchcodec/_core/CpuDeviceInterface.h | 3 +- src/torchcodec/_core/CudaDeviceInterface.cpp | 6 ++- src/torchcodec/_core/CudaDeviceInterface.h | 3 +- src/torchcodec/_core/DeviceInterface.h | 46 +++++++++---------- src/torchcodec/_core/FFMPEGCommon.cpp | 2 +- src/torchcodec/_core/FFMPEGCommon.h | 10 +++- src/torchcodec/_core/SingleStreamDecoder.cpp | 46 ++++++------------- src/torchcodec/_core/SingleStreamDecoder.h | 2 +- 11 files changed, 62 insertions(+), 70 deletions(-) diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp index 31317a7fa..d55bb1137 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp @@ -230,7 +230,8 @@ BetaCudaDeviceInterface::~BetaCudaDeviceInterface() { void BetaCudaDeviceInterface::initialize( const AVStream* avStream, - const UniqueDecodingAVFormatContext& avFormatCtx) { + const UniqueDecodingAVFormatContext& avFormatCtx, + [[maybe_unused]] const SharedAVCodecContext& codecContext) { TORCH_CHECK(avStream != nullptr, "AVStream cannot be null"); timeBase_ = avStream->time_base; frameRateAvgFromFFmpeg_ = avStream->r_frame_rate; diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.h b/src/torchcodec/_core/BetaCudaDeviceInterface.h index 0bf9951d6..fb01415d4 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.h +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.h @@ -40,7 +40,8 @@ class BetaCudaDeviceInterface : public DeviceInterface { void initialize( const AVStream* avStream, - const UniqueDecodingAVFormatContext& avFormatCtx) override; + const UniqueDecodingAVFormatContext& avFormatCtx, + const SharedAVCodecContext& codecContext) override; void convertAVFrameToFrameOutput( UniqueAVFrame& avFrame, @@ -48,10 +49,6 @@ class BetaCudaDeviceInterface : public DeviceInterface { std::optional preAllocatedOutputTensor = std::nullopt) override; - bool canDecodePacketDirectly() const override { - return true; - } - int sendPacket(ReferenceAVPacket& packet) override; int sendEOFPacket() override; int receiveFrame(UniqueAVFrame& avFrame) override; diff --git a/src/torchcodec/_core/CpuDeviceInterface.cpp b/src/torchcodec/_core/CpuDeviceInterface.cpp index e6b96e3e4..0e9b46434 100644 --- a/src/torchcodec/_core/CpuDeviceInterface.cpp +++ b/src/torchcodec/_core/CpuDeviceInterface.cpp @@ -48,8 +48,10 @@ CpuDeviceInterface::CpuDeviceInterface(const torch::Device& device) void CpuDeviceInterface::initialize( const AVStream* avStream, - [[maybe_unused]] const UniqueDecodingAVFormatContext& avFormatCtx) { + [[maybe_unused]] const UniqueDecodingAVFormatContext& avFormatCtx, + const SharedAVCodecContext& codecContext) { TORCH_CHECK(avStream != nullptr, "avStream is null"); + codecContext_ = codecContext; timeBase_ = avStream->time_base; } diff --git a/src/torchcodec/_core/CpuDeviceInterface.h b/src/torchcodec/_core/CpuDeviceInterface.h index 399b0c6be..9f44c4e8c 100644 --- a/src/torchcodec/_core/CpuDeviceInterface.h +++ b/src/torchcodec/_core/CpuDeviceInterface.h @@ -25,7 +25,8 @@ class CpuDeviceInterface : public DeviceInterface { virtual void initialize( const AVStream* avStream, - const UniqueDecodingAVFormatContext& avFormatCtx) override; + const UniqueDecodingAVFormatContext& avFormatCtx, + const SharedAVCodecContext& codecContext) override; virtual void initializeVideo( const VideoStreamOptions& videoStreamOptions, diff --git a/src/torchcodec/_core/CudaDeviceInterface.cpp b/src/torchcodec/_core/CudaDeviceInterface.cpp index aee1ecd07..c9387fbd9 100644 --- a/src/torchcodec/_core/CudaDeviceInterface.cpp +++ b/src/torchcodec/_core/CudaDeviceInterface.cpp @@ -114,15 +114,17 @@ CudaDeviceInterface::~CudaDeviceInterface() { void CudaDeviceInterface::initialize( const AVStream* avStream, - const UniqueDecodingAVFormatContext& avFormatCtx) { + const UniqueDecodingAVFormatContext& avFormatCtx, + const SharedAVCodecContext& codecContext) { TORCH_CHECK(avStream != nullptr, "avStream is null"); + codecContext_ = codecContext; timeBase_ = avStream->time_base; // TODO: Ideally, we should keep all interface implementations independent. cpuInterface_ = createDeviceInterface(torch::kCPU); TORCH_CHECK( cpuInterface_ != nullptr, "Failed to create CPU device interface"); - cpuInterface_->initialize(avStream, avFormatCtx); + cpuInterface_->initialize(avStream, avFormatCtx, codecContext); cpuInterface_->initializeVideo( VideoStreamOptions(), {}, diff --git a/src/torchcodec/_core/CudaDeviceInterface.h b/src/torchcodec/_core/CudaDeviceInterface.h index 1a8f184ec..d240066f4 100644 --- a/src/torchcodec/_core/CudaDeviceInterface.h +++ b/src/torchcodec/_core/CudaDeviceInterface.h @@ -22,7 +22,8 @@ class CudaDeviceInterface : public DeviceInterface { void initialize( const AVStream* avStream, - const UniqueDecodingAVFormatContext& avFormatCtx) override; + const UniqueDecodingAVFormatContext& avFormatCtx, + const SharedAVCodecContext& codecContext) override; void initializeVideo( const VideoStreamOptions& videoStreamOptions, diff --git a/src/torchcodec/_core/DeviceInterface.h b/src/torchcodec/_core/DeviceInterface.h index 982f7e732..8aad60f24 100644 --- a/src/torchcodec/_core/DeviceInterface.h +++ b/src/torchcodec/_core/DeviceInterface.h @@ -54,7 +54,8 @@ class DeviceInterface { // Initialize the device with parameters generic to all kinds of decoding. virtual void initialize( const AVStream* avStream, - const UniqueDecodingAVFormatContext& avFormatCtx) = 0; + const UniqueDecodingAVFormatContext& avFormatCtx, + const SharedAVCodecContext& codecContext) = 0; // Initialize the device with parameters specific to video decoding. There is // a default empty implementation. @@ -80,52 +81,47 @@ class DeviceInterface { // Extension points for custom decoding paths // ------------------------------------------ - // Override to return true if this device interface can decode packets - // directly. This means that the following two member functions can both - // be called: - // - // 1. sendPacket() - // 2. receiveFrame() - virtual bool canDecodePacketDirectly() const { - return false; - } - - // Moral equivalent of avcodec_send_packet() // Returns AVSUCCESS on success, AVERROR(EAGAIN) if decoder queue full, or // other AVERROR on failure - virtual int sendPacket([[maybe_unused]] ReferenceAVPacket& avPacket) { + // Default implementation uses FFmpeg directly + virtual int sendPacket(ReferenceAVPacket& avPacket) { TORCH_CHECK( - false, - "Send/receive packet decoding not implemented for this device interface"); - return AVERROR(ENOSYS); + codecContext_ != nullptr, + "Codec context not available for default packet sending"); + return avcodec_send_packet(codecContext_.get(), avPacket.get()); } // Send an EOF packet to flush the decoder // Returns AVSUCCESS on success, or other AVERROR on failure + // Default implementation uses FFmpeg directly virtual int sendEOFPacket() { TORCH_CHECK( - false, "Send EOF packet not implemented for this device interface"); - return AVERROR(ENOSYS); + codecContext_ != nullptr, + "Codec context not available for default EOF packet sending"); + return avcodec_send_packet(codecContext_.get(), nullptr); } - // Moral equivalent of avcodec_receive_frame() // Returns AVSUCCESS on success, AVERROR(EAGAIN) if no frame ready, // AVERROR_EOF if end of stream, or other AVERROR on failure - virtual int receiveFrame([[maybe_unused]] UniqueAVFrame& avFrame) { + // Default implementation uses FFmpeg directly + virtual int receiveFrame(UniqueAVFrame& avFrame) { TORCH_CHECK( - false, - "Send/receive packet decoding not implemented for this device interface"); - return AVERROR(ENOSYS); + codecContext_ != nullptr, + "Codec context not available for default frame receiving"); + return avcodec_receive_frame(codecContext_.get(), avFrame.get()); } // Flush remaining frames from decoder virtual void flush() { - // Default implementation is no-op for standard decoders - // Custom decoders can override this method + TORCH_CHECK( + codecContext_ != nullptr, + "Codec context not available for default flushing"); + avcodec_flush_buffers(codecContext_.get()); } protected: torch::Device device_; + SharedAVCodecContext codecContext_; }; using CreateDeviceInterfaceFn = diff --git a/src/torchcodec/_core/FFMPEGCommon.cpp b/src/torchcodec/_core/FFMPEGCommon.cpp index 0570f06cf..97ff082e1 100644 --- a/src/torchcodec/_core/FFMPEGCommon.cpp +++ b/src/torchcodec/_core/FFMPEGCommon.cpp @@ -149,7 +149,7 @@ int getNumChannels(const UniqueAVFrame& avFrame) { #endif } -int getNumChannels(const UniqueAVCodecContext& avCodecContext) { +int getNumChannels(const SharedAVCodecContext& avCodecContext) { #if LIBAVFILTER_VERSION_MAJOR > 8 || \ (LIBAVFILTER_VERSION_MAJOR == 8 && LIBAVFILTER_VERSION_MINOR >= 44) return avCodecContext->ch_layout.nb_channels; diff --git a/src/torchcodec/_core/FFMPEGCommon.h b/src/torchcodec/_core/FFMPEGCommon.h index 19cddcc37..337616ddc 100644 --- a/src/torchcodec/_core/FFMPEGCommon.h +++ b/src/torchcodec/_core/FFMPEGCommon.h @@ -71,6 +71,14 @@ using UniqueEncodingAVFormatContext = std::unique_ptr< using UniqueAVCodecContext = std::unique_ptr< AVCodecContext, Deleterp>; +using SharedAVCodecContext = std::shared_ptr; + +// create SharedAVCodecContext with custom deleter +inline SharedAVCodecContext makeSharedAVCodecContext(AVCodecContext* ctx) { + return SharedAVCodecContext( + ctx, Deleterp{}); +} + using UniqueAVFrame = std::unique_ptr>; using UniqueAVFilterGraph = std::unique_ptr< @@ -171,7 +179,7 @@ const AVSampleFormat* getSupportedOutputSampleFormats(const AVCodec& avCodec); const AVPixelFormat* getSupportedPixelFormats(const AVCodec& avCodec); int getNumChannels(const UniqueAVFrame& avFrame); -int getNumChannels(const UniqueAVCodecContext& avCodecContext); +int getNumChannels(const SharedAVCodecContext& avCodecContext); void setDefaultChannelLayout( UniqueAVCodecContext& avCodecContext, diff --git a/src/torchcodec/_core/SingleStreamDecoder.cpp b/src/torchcodec/_core/SingleStreamDecoder.cpp index d06c47922..ba7382c67 100644 --- a/src/torchcodec/_core/SingleStreamDecoder.cpp +++ b/src/torchcodec/_core/SingleStreamDecoder.cpp @@ -429,7 +429,6 @@ void SingleStreamDecoder::addStream( TORCH_CHECK( deviceInterface_ != nullptr, "Failed to create device interface. This should never happen, please report."); - deviceInterface_->initialize(streamInfo.stream, formatContext_); // TODO_CODE_QUALITY it's pretty meh to have a video-specific logic within // addStream() which is supposed to be generic @@ -441,7 +440,7 @@ void SingleStreamDecoder::addStream( AVCodecContext* codecContext = avcodec_alloc_context3(avCodec); TORCH_CHECK(codecContext != nullptr); - streamInfo.codecContext.reset(codecContext); + streamInfo.codecContext = makeSharedAVCodecContext(codecContext); int retVal = avcodec_parameters_to_context( streamInfo.codecContext.get(), streamInfo.stream->codecpar); @@ -453,14 +452,19 @@ void SingleStreamDecoder::addStream( // Note that we must make sure to register the harware device context // with the codec context before calling avcodec_open2(). Otherwise, decoding // will happen on the CPU and not the hardware device. - deviceInterface_->registerHardwareDeviceWithCodec(codecContext); + deviceInterface_->registerHardwareDeviceWithCodec( + streamInfo.codecContext.get()); retVal = avcodec_open2(streamInfo.codecContext.get(), avCodec, nullptr); TORCH_CHECK(retVal >= AVSUCCESS, getFFMPEGErrorStringFromErrorCode(retVal)); - codecContext->time_base = streamInfo.stream->time_base; + streamInfo.codecContext->time_base = streamInfo.stream->time_base; + + // Initialize the device interface with the codec context + deviceInterface_->initialize( + streamInfo.stream, formatContext_, streamInfo.codecContext); containerMetadata_.allStreamMetadata[activeStreamIndex_].codecName = - std::string(avcodec_get_name(codecContext->codec_id)); + std::string(avcodec_get_name(streamInfo.codecContext->codec_id)); // We will only need packets from the active stream, so we tell FFmpeg to // discard packets from the other streams. Note that av_read_frame() may still @@ -1149,8 +1153,6 @@ void SingleStreamDecoder::maybeSeekToBeforeDesiredPts() { getFFMPEGErrorStringFromErrorCode(status)); decodeStats_.numFlushes++; - avcodec_flush_buffers(streamInfo.codecContext.get()); - deviceInterface_->flush(); } @@ -1169,24 +1171,16 @@ UniqueAVFrame SingleStreamDecoder::decodeAVFrame( cursorWasJustSet_ = false; } - StreamInfo& streamInfo = streamInfos_[activeStreamIndex_]; UniqueAVFrame avFrame(av_frame_alloc()); AutoAVPacket autoAVPacket; int status = AVSUCCESS; bool reachedEOF = false; - // TODONVDEC P2: Instead of calling canDecodePacketDirectly() and rely on - // if/else blocks to dispatch to the interface or to FFmpeg, consider *always* - // dispatching to the interface. The default implementation of the interface's - // receiveFrame and sendPacket could just be calling avcodec_receive_frame and - // avcodec_send_packet. This would make the decoding loop even more generic. + // The default implementation uses avcodec_receive_frame and + // avcodec_send_packet, while specialized interfaces can override for + // hardware-specific optimizations. while (true) { - if (deviceInterface_->canDecodePacketDirectly()) { - status = deviceInterface_->receiveFrame(avFrame); - } else { - status = - avcodec_receive_frame(streamInfo.codecContext.get(), avFrame.get()); - } + status = deviceInterface_->receiveFrame(avFrame); if (status != AVSUCCESS && status != AVERROR(EAGAIN)) { // Non-retriable error @@ -1222,13 +1216,7 @@ UniqueAVFrame SingleStreamDecoder::decodeAVFrame( if (status == AVERROR_EOF) { // End of file reached. We must drain the decoder - if (deviceInterface_->canDecodePacketDirectly()) { - status = deviceInterface_->sendEOFPacket(); - } else { - status = avcodec_send_packet( - streamInfo.codecContext.get(), - /*avpkt=*/nullptr); - } + status = deviceInterface_->sendEOFPacket(); TORCH_CHECK( status >= AVSUCCESS, "Could not flush decoder: ", @@ -1253,11 +1241,7 @@ UniqueAVFrame SingleStreamDecoder::decodeAVFrame( // We got a valid packet. Send it to the decoder, and we'll receive it in // the next iteration. - if (deviceInterface_->canDecodePacketDirectly()) { - status = deviceInterface_->sendPacket(packet); - } else { - status = avcodec_send_packet(streamInfo.codecContext.get(), packet.get()); - } + status = deviceInterface_->sendPacket(packet); TORCH_CHECK( status >= AVSUCCESS, "Could not push packet to decoder: ", diff --git a/src/torchcodec/_core/SingleStreamDecoder.h b/src/torchcodec/_core/SingleStreamDecoder.h index cf24aa0c3..06ea0cd04 100644 --- a/src/torchcodec/_core/SingleStreamDecoder.h +++ b/src/torchcodec/_core/SingleStreamDecoder.h @@ -221,7 +221,7 @@ class SingleStreamDecoder { AVMediaType avMediaType = AVMEDIA_TYPE_UNKNOWN; AVRational timeBase = {}; - UniqueAVCodecContext codecContext; + SharedAVCodecContext codecContext; // The FrameInfo indices we built when scanFileAndUpdateMetadataAndIndex was // called. From f39ebfd9090611c93455213dcc3a0a9ca26e777f Mon Sep 17 00:00:00 2001 From: Dan-Flores Date: Fri, 17 Oct 2025 09:30:46 -0400 Subject: [PATCH 09/20] Add to_tensor support for VideoEncoder (#957) Co-authored-by: Daniel Flores --- src/torchcodec/_core/AVIOTensorContext.cpp | 39 +++++--- src/torchcodec/_core/AVIOTensorContext.h | 3 +- src/torchcodec/_core/Encoder.cpp | 58 +++++++++-- src/torchcodec/_core/Encoder.h | 11 +++ src/torchcodec/_core/__init__.py | 1 + src/torchcodec/_core/custom_ops.cpp | 56 +++++++---- src/torchcodec/_core/ops.py | 39 +++++--- test/test_ops.py | 108 +++++++++++++++------ 8 files changed, 231 insertions(+), 84 deletions(-) diff --git a/src/torchcodec/_core/AVIOTensorContext.cpp b/src/torchcodec/_core/AVIOTensorContext.cpp index 3f45f5be5..238475761 100644 --- a/src/torchcodec/_core/AVIOTensorContext.cpp +++ b/src/torchcodec/_core/AVIOTensorContext.cpp @@ -18,15 +18,15 @@ constexpr int64_t MAX_TENSOR_SIZE = 320'000'000; // 320 MB int read(void* opaque, uint8_t* buf, int buf_size) { auto tensorContext = static_cast(opaque); TORCH_CHECK( - tensorContext->current <= tensorContext->data.numel(), - "Tried to read outside of the buffer: current=", - tensorContext->current, + tensorContext->current_pos <= tensorContext->data.numel(), + "Tried to read outside of the buffer: current_pos=", + tensorContext->current_pos, ", size=", tensorContext->data.numel()); int64_t numBytesRead = std::min( static_cast(buf_size), - tensorContext->data.numel() - tensorContext->current); + tensorContext->data.numel() - tensorContext->current_pos); TORCH_CHECK( numBytesRead >= 0, @@ -34,8 +34,8 @@ int read(void* opaque, uint8_t* buf, int buf_size) { numBytesRead, ", size=", tensorContext->data.numel(), - ", current=", - tensorContext->current); + ", current_pos=", + tensorContext->current_pos); if (numBytesRead == 0) { return AVERROR_EOF; @@ -43,9 +43,9 @@ int read(void* opaque, uint8_t* buf, int buf_size) { std::memcpy( buf, - tensorContext->data.data_ptr() + tensorContext->current, + tensorContext->data.data_ptr() + tensorContext->current_pos, numBytesRead); - tensorContext->current += numBytesRead; + tensorContext->current_pos += numBytesRead; return numBytesRead; } @@ -54,7 +54,7 @@ int write(void* opaque, const uint8_t* buf, int buf_size) { auto tensorContext = static_cast(opaque); int64_t bufSize = static_cast(buf_size); - if (tensorContext->current + bufSize > tensorContext->data.numel()) { + if (tensorContext->current_pos + bufSize > tensorContext->data.numel()) { TORCH_CHECK( tensorContext->data.numel() * 2 <= MAX_TENSOR_SIZE, "We tried to allocate an output encoded tensor larger than ", @@ -68,13 +68,17 @@ int write(void* opaque, const uint8_t* buf, int buf_size) { } TORCH_CHECK( - tensorContext->current + bufSize <= tensorContext->data.numel(), + tensorContext->current_pos + bufSize <= tensorContext->data.numel(), "Re-allocation of the output tensor didn't work. ", "This should not happen, please report on TorchCodec bug tracker"); uint8_t* outputTensorData = tensorContext->data.data_ptr(); - std::memcpy(outputTensorData + tensorContext->current, buf, bufSize); - tensorContext->current += bufSize; + std::memcpy(outputTensorData + tensorContext->current_pos, buf, bufSize); + tensorContext->current_pos += bufSize; + // Track the maximum position written so getOutputTensor's narrow() does not + // truncate the file if final seek was backwards + tensorContext->max_pos = + std::max(tensorContext->current_pos, tensorContext->max_pos); return buf_size; } @@ -88,7 +92,7 @@ int64_t seek(void* opaque, int64_t offset, int whence) { ret = tensorContext->data.numel(); break; case SEEK_SET: - tensorContext->current = offset; + tensorContext->current_pos = offset; ret = offset; break; default: @@ -101,7 +105,7 @@ int64_t seek(void* opaque, int64_t offset, int whence) { } // namespace AVIOFromTensorContext::AVIOFromTensorContext(torch::Tensor data) - : tensorContext_{data, 0} { + : tensorContext_{data, 0, 0} { TORCH_CHECK(data.numel() > 0, "data must not be empty"); TORCH_CHECK(data.is_contiguous(), "data must be contiguous"); TORCH_CHECK(data.scalar_type() == torch::kUInt8, "data must be kUInt8"); @@ -110,14 +114,17 @@ AVIOFromTensorContext::AVIOFromTensorContext(torch::Tensor data) } AVIOToTensorContext::AVIOToTensorContext() - : tensorContext_{torch::empty({INITIAL_TENSOR_SIZE}, {torch::kUInt8}), 0} { + : tensorContext_{ + torch::empty({INITIAL_TENSOR_SIZE}, {torch::kUInt8}), + 0, + 0} { createAVIOContext( nullptr, &write, &seek, &tensorContext_, /*isForWriting=*/true); } torch::Tensor AVIOToTensorContext::getOutputTensor() { return tensorContext_.data.narrow( - /*dim=*/0, /*start=*/0, /*length=*/tensorContext_.current); + /*dim=*/0, /*start=*/0, /*length=*/tensorContext_.max_pos); } } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/AVIOTensorContext.h b/src/torchcodec/_core/AVIOTensorContext.h index 15f97da55..bcd97052b 100644 --- a/src/torchcodec/_core/AVIOTensorContext.h +++ b/src/torchcodec/_core/AVIOTensorContext.h @@ -15,7 +15,8 @@ namespace detail { struct TensorContext { torch::Tensor data; - int64_t current; + int64_t current_pos; + int64_t max_pos; }; } // namespace detail diff --git a/src/torchcodec/_core/Encoder.cpp b/src/torchcodec/_core/Encoder.cpp index 14ef1cb94..1d9c2c089 100644 --- a/src/torchcodec/_core/Encoder.cpp +++ b/src/torchcodec/_core/Encoder.cpp @@ -4,10 +4,6 @@ #include "src/torchcodec/_core/Encoder.h" #include "torch/types.h" -extern "C" { -#include -} - namespace facebook::torchcodec { namespace { @@ -542,10 +538,17 @@ torch::Tensor validateFrames(const torch::Tensor& frames) { } // namespace VideoEncoder::~VideoEncoder() { + // TODO-VideoEncoder: Unify destructor with ~AudioEncoder() if (avFormatContext_ && avFormatContext_->pb) { - avio_flush(avFormatContext_->pb); - avio_close(avFormatContext_->pb); - avFormatContext_->pb = nullptr; + if (avFormatContext_->pb->error == 0) { + avio_flush(avFormatContext_->pb); + } + if (!avioContextHolder_) { + if (avFormatContext_->pb->error == 0) { + avio_close(avFormatContext_->pb); + } + avFormatContext_->pb = nullptr; + } } } @@ -581,6 +584,36 @@ VideoEncoder::VideoEncoder( initializeEncoder(videoStreamOptions); } +VideoEncoder::VideoEncoder( + const torch::Tensor& frames, + int frameRate, + std::string_view formatName, + std::unique_ptr avioContextHolder, + const VideoStreamOptions& videoStreamOptions) + : frames_(validateFrames(frames)), + inFrameRate_(frameRate), + avioContextHolder_(std::move(avioContextHolder)) { + setFFmpegLogLevel(); + // Map mkv -> matroska when used as format name + formatName = (formatName == "mkv") ? "matroska" : formatName; + AVFormatContext* avFormatContext = nullptr; + int status = avformat_alloc_output_context2( + &avFormatContext, nullptr, formatName.data(), nullptr); + + TORCH_CHECK( + avFormatContext != nullptr, + "Couldn't allocate AVFormatContext. ", + "Check the desired format? Got format=", + formatName, + ". ", + getFFMPEGErrorStringFromErrorCode(status)); + avFormatContext_.reset(avFormatContext); + + avFormatContext_->pb = avioContextHolder_->getAVIOContext(); + + initializeEncoder(videoStreamOptions); +} + void VideoEncoder::initializeEncoder( const VideoStreamOptions& videoStreamOptions) { const AVCodec* avCodec = @@ -751,6 +784,17 @@ UniqueAVFrame VideoEncoder::convertTensorToAVFrame( return avFrame; } +torch::Tensor VideoEncoder::encodeToTensor() { + TORCH_CHECK( + avioContextHolder_ != nullptr, + "Cannot encode to tensor, avio tensor context doesn't exist."); + encode(); + auto avioToTensorContext = + dynamic_cast(avioContextHolder_.get()); + TORCH_CHECK(avioToTensorContext != nullptr, "Invalid AVIO context holder."); + return avioToTensorContext->getOutputTensor(); +} + void VideoEncoder::encodeFrame( AutoAVPacket& autoAVPacket, const UniqueAVFrame& avFrame) { diff --git a/src/torchcodec/_core/Encoder.h b/src/torchcodec/_core/Encoder.h index 62d30a624..7aff0bdbc 100644 --- a/src/torchcodec/_core/Encoder.h +++ b/src/torchcodec/_core/Encoder.h @@ -141,8 +141,17 @@ class VideoEncoder { std::string_view fileName, const VideoStreamOptions& videoStreamOptions); + VideoEncoder( + const torch::Tensor& frames, + int frameRate, + std::string_view formatName, + std::unique_ptr avioContextHolder, + const VideoStreamOptions& videoStreamOptions); + void encode(); + torch::Tensor encodeToTensor(); + private: void initializeEncoder(const VideoStreamOptions& videoStreamOptions); UniqueAVFrame convertTensorToAVFrame( @@ -167,6 +176,8 @@ class VideoEncoder { int outHeight_ = -1; AVPixelFormat outPixelFormat_ = AV_PIX_FMT_NONE; + std::unique_ptr avioContextHolder_; + bool encodeWasCalled_ = false; }; diff --git a/src/torchcodec/_core/__init__.py b/src/torchcodec/_core/__init__.py index 24e54af0e..eb8dd9697 100644 --- a/src/torchcodec/_core/__init__.py +++ b/src/torchcodec/_core/__init__.py @@ -26,6 +26,7 @@ encode_audio_to_file_like, encode_audio_to_tensor, encode_video_to_file, + encode_video_to_tensor, get_ffmpeg_library_versions, get_frame_at_index, get_frame_at_pts, diff --git a/src/torchcodec/_core/custom_ops.cpp b/src/torchcodec/_core/custom_ops.cpp index f29f33395..94a3fba1b 100644 --- a/src/torchcodec/_core/custom_ops.cpp +++ b/src/torchcodec/_core/custom_ops.cpp @@ -32,12 +32,14 @@ TORCH_LIBRARY(torchcodec_ns, m) { m.def("create_from_file(str filename, str? seek_mode=None) -> Tensor"); m.def( "encode_audio_to_file(Tensor samples, int sample_rate, str filename, int? bit_rate=None, int? num_channels=None, int? desired_sample_rate=None) -> ()"); - m.def( - "encode_video_to_file(Tensor frames, int frame_rate, str filename, int? crf=None) -> ()"); m.def( "encode_audio_to_tensor(Tensor samples, int sample_rate, str format, int? bit_rate=None, int? num_channels=None, int? desired_sample_rate=None) -> Tensor"); m.def( "_encode_audio_to_file_like(Tensor samples, int sample_rate, str format, int file_like_context, int? bit_rate=None, int? num_channels=None, int? desired_sample_rate=None) -> ()"); + m.def( + "encode_video_to_file(Tensor frames, int frame_rate, str filename, int? crf=None) -> ()"); + m.def( + "encode_video_to_tensor(Tensor frames, int frame_rate, str format, int? crf=None) -> Tensor"); m.def( "create_from_tensor(Tensor video_tensor, str? seek_mode=None) -> Tensor"); m.def( @@ -498,21 +500,6 @@ OpsAudioFramesOutput get_frames_by_pts_in_range_audio( return makeOpsAudioFramesOutput(result); } -void encode_video_to_file( - const at::Tensor& frames, - int64_t frame_rate, - std::string_view file_name, - std::optional crf = std::nullopt) { - VideoStreamOptions videoStreamOptions; - videoStreamOptions.crf = crf; - VideoEncoder( - frames, - validateInt64ToInt(frame_rate, "frame_rate"), - file_name, - videoStreamOptions) - .encode(); -} - void encode_audio_to_file( const at::Tensor& samples, int64_t sample_rate, @@ -587,6 +574,38 @@ void _encode_audio_to_file_like( encoder.encode(); } +void encode_video_to_file( + const at::Tensor& frames, + int64_t frame_rate, + std::string_view file_name, + std::optional crf = std::nullopt) { + VideoStreamOptions videoStreamOptions; + videoStreamOptions.crf = crf; + VideoEncoder( + frames, + validateInt64ToInt(frame_rate, "frame_rate"), + file_name, + videoStreamOptions) + .encode(); +} + +at::Tensor encode_video_to_tensor( + const at::Tensor& frames, + int64_t frame_rate, + std::string_view format, + std::optional crf = std::nullopt) { + auto avioContextHolder = std::make_unique(); + VideoStreamOptions videoStreamOptions; + videoStreamOptions.crf = crf; + return VideoEncoder( + frames, + validateInt64ToInt(frame_rate, "frame_rate"), + format, + std::move(avioContextHolder), + videoStreamOptions) + .encodeToTensor(); +} + // For testing only. We need to implement this operation as a core library // function because what we're testing is round-tripping pts values as // double-precision floating point numbers from C++ to Python and back to C++. @@ -847,9 +866,10 @@ TORCH_LIBRARY_IMPL(torchcodec_ns, BackendSelect, m) { TORCH_LIBRARY_IMPL(torchcodec_ns, CPU, m) { m.impl("encode_audio_to_file", &encode_audio_to_file); - m.impl("encode_video_to_file", &encode_video_to_file); m.impl("encode_audio_to_tensor", &encode_audio_to_tensor); m.impl("_encode_audio_to_file_like", &_encode_audio_to_file_like); + m.impl("encode_video_to_file", &encode_video_to_file); + m.impl("encode_video_to_tensor", &encode_video_to_tensor); m.impl("seek_to_pts", &seek_to_pts); m.impl("add_video_stream", &add_video_stream); m.impl("_add_video_stream", &_add_video_stream); diff --git a/src/torchcodec/_core/ops.py b/src/torchcodec/_core/ops.py index 6fc30e5af..03cf8cf6d 100644 --- a/src/torchcodec/_core/ops.py +++ b/src/torchcodec/_core/ops.py @@ -92,15 +92,18 @@ def load_torchcodec_shared_libraries(): encode_audio_to_file = torch._dynamo.disallow_in_graph( torch.ops.torchcodec_ns.encode_audio_to_file.default ) -encode_video_to_file = torch._dynamo.disallow_in_graph( - torch.ops.torchcodec_ns.encode_video_to_file.default -) encode_audio_to_tensor = torch._dynamo.disallow_in_graph( torch.ops.torchcodec_ns.encode_audio_to_tensor.default ) _encode_audio_to_file_like = torch._dynamo.disallow_in_graph( torch.ops.torchcodec_ns._encode_audio_to_file_like.default ) +encode_video_to_file = torch._dynamo.disallow_in_graph( + torch.ops.torchcodec_ns.encode_video_to_file.default +) +encode_video_to_tensor = torch._dynamo.disallow_in_graph( + torch.ops.torchcodec_ns.encode_video_to_tensor.default +) create_from_tensor = torch._dynamo.disallow_in_graph( torch.ops.torchcodec_ns.create_from_tensor.default ) @@ -254,16 +257,6 @@ def encode_audio_to_file_abstract( return -@register_fake("torchcodec_ns::encode_video_to_file") -def encode_video_to_file_abstract( - frames: torch.Tensor, - frame_rate: int, - filename: str, - crf: Optional[int] = None, -) -> None: - return - - @register_fake("torchcodec_ns::encode_audio_to_tensor") def encode_audio_to_tensor_abstract( samples: torch.Tensor, @@ -289,6 +282,26 @@ def _encode_audio_to_file_like_abstract( return +@register_fake("torchcodec_ns::encode_video_to_file") +def encode_video_to_file_abstract( + frames: torch.Tensor, + frame_rate: int, + filename: str, + crf: Optional[int], +) -> None: + return + + +@register_fake("torchcodec_ns::encode_video_to_tensor") +def encode_video_to_tensor_abstract( + frames: torch.Tensor, + frame_rate: int, + format: str, + crf: Optional[int], +) -> torch.Tensor: + return torch.empty([], dtype=torch.long) + + @register_fake("torchcodec_ns::create_from_tensor") def create_from_tensor_abstract( video_tensor: torch.Tensor, seek_mode: Optional[str] diff --git a/test/test_ops.py b/test/test_ops.py index 0c1d90cfc..31afbdd14 100644 --- a/test/test_ops.py +++ b/test/test_ops.py @@ -29,6 +29,7 @@ create_from_tensor, encode_audio_to_file, encode_video_to_file, + encode_video_to_tensor, get_ffmpeg_library_versions, get_frame_at_index, get_frame_at_pts, @@ -41,6 +42,7 @@ get_next_frame, seek_to_pts, ) +from torchcodec.decoders import VideoDecoder from .utils import ( all_supported_devices, @@ -1328,6 +1330,7 @@ def test_bad_input(self, tmp_path): class TestVideoEncoderOps: + # TODO-VideoEncoder: Parametrize test after moving to test_encoders def test_bad_input(self, tmp_path): output_file = str(tmp_path / ".mp4") @@ -1378,17 +1381,25 @@ def test_bad_input(self, tmp_path): filename="./bad/path.mp3", ) - def decode(self, file_path) -> torch.Tensor: - decoder = create_from_file(str(file_path), seek_mode="approximate") - add_video_stream(decoder) - frames, *_ = get_frames_in_range(decoder, start=0, stop=60) - return frames + with pytest.raises( + RuntimeError, + match=r"Couldn't allocate AVFormatContext. Check the desired format\? Got format=bad_format", + ): + encode_video_to_tensor( + frames=torch.randint(high=255, size=(10, 3, 60, 60), dtype=torch.uint8), + frame_rate=10, + format="bad_format", + ) + + def decode(self, source=None) -> torch.Tensor: + return VideoDecoder(source).get_frames_in_range(start=0, stop=60) @pytest.mark.parametrize( "format", ("mov", "mp4", "mkv", pytest.param("webm", marks=pytest.mark.slow)) ) - def test_video_encoder_round_trip(self, tmp_path, format): - # Test that decode(encode(decode(asset))) == decode(asset) + @pytest.mark.parametrize("method", ("to_file", "to_tensor")) + def test_video_encoder_round_trip(self, tmp_path, format, method): + # Test that decode(encode(decode(frames))) == decode(frames) ffmpeg_version = get_ffmpeg_major_version() # In FFmpeg6, the default codec's best pixel format is lossy for all container formats but webm. # As a result, we skip the round trip test. @@ -1400,15 +1411,25 @@ def test_video_encoder_round_trip(self, tmp_path, format): ffmpeg_version == 4 or (IS_WINDOWS and ffmpeg_version in (6, 7)) ): pytest.skip("Codec for webm is not available in this FFmpeg installation.") - asset = TEST_SRC_2_720P - source_frames = self.decode(str(asset.path)).data + source_frames = self.decode(TEST_SRC_2_720P.path).data + + params = dict( + frame_rate=30, crf=0 + ) # Frame rate is fixed with num frames decoded + if method == "to_file": + encoded_path = str(tmp_path / f"encoder_output.{format}") + encode_video_to_file( + frames=source_frames, + filename=encoded_path, + **params, + ) + round_trip_frames = self.decode(encoded_path).data + else: # to_tensor + encoded_tensor = encode_video_to_tensor( + source_frames, format=format, **params + ) + round_trip_frames = self.decode(encoded_tensor).data - encoded_path = str(tmp_path / f"encoder_output.{format}") - frame_rate = 30 # Frame rate is fixed with num frames decoded - encode_video_to_file( - frames=source_frames, frame_rate=frame_rate, filename=encoded_path, crf=0 - ) - round_trip_frames = self.decode(encoded_path).data assert source_frames.shape == round_trip_frames.shape assert source_frames.dtype == round_trip_frames.dtype @@ -1424,6 +1445,40 @@ def test_video_encoder_round_trip(self, tmp_path, format): assert psnr(s_frame, rt_frame) > 30 assert_close(s_frame, rt_frame, atol=atol, rtol=0) + @pytest.mark.parametrize( + "format", + ( + "mov", + "mp4", + "avi", + "mkv", + "flv", + "gif", + pytest.param("webm", marks=pytest.mark.slow), + ), + ) + def test_against_to_file(self, tmp_path, format): + # Test that to_file and to_tensor produce the same results + ffmpeg_version = get_ffmpeg_major_version() + if format == "webm" and ( + ffmpeg_version == 4 or (IS_WINDOWS and ffmpeg_version in (6, 7)) + ): + pytest.skip("Codec for webm is not available in this FFmpeg installation.") + + source_frames = self.decode(TEST_SRC_2_720P.path).data + params = dict(frame_rate=30, crf=0) + + encoded_file = tmp_path / f"output.{format}" + encode_video_to_file(frames=source_frames, filename=str(encoded_file), **params) + encoded_tensor = encode_video_to_tensor(source_frames, format=format, **params) + + torch.testing.assert_close( + self.decode(encoded_file).data, + self.decode(encoded_tensor).data, + atol=0, + rtol=0, + ) + @pytest.mark.skipif(in_fbcode(), reason="ffmpeg CLI not available") @pytest.mark.parametrize( "format", @@ -1439,18 +1494,12 @@ def test_video_encoder_round_trip(self, tmp_path, format): ) def test_video_encoder_against_ffmpeg_cli(self, tmp_path, format): ffmpeg_version = get_ffmpeg_major_version() - if format == "webm": - if ffmpeg_version == 4: - pytest.skip( - "Codec for webm is not available in the FFmpeg4 installation." - ) - if IS_WINDOWS and ffmpeg_version in (6, 7): - pytest.skip( - "Codec for webm is not available in the FFmpeg6/7 installation on Windows." - ) - asset = TEST_SRC_2_720P - source_frames = self.decode(str(asset.path)).data - frame_rate = 30 + if format == "webm" and ( + ffmpeg_version == 4 or (IS_WINDOWS and ffmpeg_version in (6, 7)) + ): + pytest.skip("Codec for webm is not available in this FFmpeg installation.") + + source_frames = self.decode(TEST_SRC_2_720P.path).data # Encode with FFmpeg CLI temp_raw_path = str(tmp_path / "temp_input.raw") @@ -1458,8 +1507,8 @@ def test_video_encoder_against_ffmpeg_cli(self, tmp_path, format): f.write(source_frames.permute(0, 2, 3, 1).cpu().numpy().tobytes()) ffmpeg_encoded_path = str(tmp_path / f"ffmpeg_output.{format}") + frame_rate = 30 crf = 0 - quality_params = ["-crf", str(crf)] # Some codecs (ex. MPEG4) do not support CRF. # Flags not supported by the selected codec will be ignored. ffmpeg_cmd = [ @@ -1475,7 +1524,8 @@ def test_video_encoder_against_ffmpeg_cli(self, tmp_path, format): str(frame_rate), "-i", temp_raw_path, - *quality_params, + "-crf", + str(crf), ffmpeg_encoded_path, ] subprocess.run(ffmpeg_cmd, check=True) From d272e4dc5d03e96257070c7d35b4f8c5d7346c5e Mon Sep 17 00:00:00 2001 From: Silvio Traversaro Date: Fri, 17 Oct 2025 17:17:00 +0200 Subject: [PATCH 10/20] Enable tests with ffmpeg8 on Windows (#973) --- .github/workflows/windows_wheel.yaml | 9 ++++++--- test/test_encoders.py | 26 ++++++++++++++++++++------ test/utils.py | 13 ++++++++++++- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/.github/workflows/windows_wheel.yaml b/.github/workflows/windows_wheel.yaml index 39247f770..2fd773fec 100644 --- a/.github/workflows/windows_wheel.yaml +++ b/.github/workflows/windows_wheel.yaml @@ -71,8 +71,7 @@ jobs: # TODO: FFmpeg 5 on Windows segfaults in avcodec_open2() when passing # bad parameters. # See https://github.com/pytorch/torchcodec/pull/806 - # TODO: Support FFmpeg 8 on Windows - ffmpeg-version-for-tests: ['4.4.2', '5.1.2', '6.1.1', '7.0.1'] + ffmpeg-version-for-tests: ['4.4.2', '5.1.2', '6.1.1', '7.0.1', '8.0'] needs: build steps: - uses: actions/download-artifact@v4 @@ -83,7 +82,11 @@ jobs: uses: conda-incubator/setup-miniconda@v2 with: auto-update-conda: true - miniconda-version: "latest" + # Using miniforge instead of miniconda ensures that the default + # conda channel is conda-forge instead of main/default. This ensures + # ABI consistency between dependencies: + # https://conda-forge.org/docs/user/transitioning_from_defaults/ + miniforge-version: latest activate-environment: test python-version: ${{ matrix.python-version }} - name: Update pip diff --git a/test/test_encoders.py b/test/test_encoders.py index f8b5b3519..c5946654d 100644 --- a/test/test_encoders.py +++ b/test/test_encoders.py @@ -16,6 +16,7 @@ from .utils import ( assert_tensor_close_on_at_least, get_ffmpeg_major_version, + get_ffmpeg_minor_version, in_fbcode, IS_WINDOWS, NASA_AUDIO_MP3, @@ -23,6 +24,11 @@ TestContainerFile, ) +IS_WINDOWS_WITH_FFMPEG_LE_70 = IS_WINDOWS and ( + get_ffmpeg_major_version() < 7 + or (get_ffmpeg_major_version() == 7 and get_ffmpeg_minor_version() == 0) +) + @pytest.fixture def with_ffmpeg_debug_logs(): @@ -155,7 +161,11 @@ def test_bad_input_parametrized(self, method, tmp_path): avcodec_open2_failed_msg = "avcodec_open2 failed: Invalid argument" with pytest.raises( RuntimeError, - match=avcodec_open2_failed_msg if IS_WINDOWS else "invalid sample rate=10", + match=( + avcodec_open2_failed_msg + if IS_WINDOWS_WITH_FFMPEG_LE_70 + else "invalid sample rate=10" + ), ): getattr(decoder, method)(**valid_params) @@ -164,14 +174,18 @@ def test_bad_input_parametrized(self, method, tmp_path): ) with pytest.raises( RuntimeError, - match=avcodec_open2_failed_msg if IS_WINDOWS else "invalid sample rate=10", + match=( + avcodec_open2_failed_msg + if IS_WINDOWS_WITH_FFMPEG_LE_70 + else "invalid sample rate=10" + ), ): getattr(decoder, method)(sample_rate=10, **valid_params) with pytest.raises( RuntimeError, match=( avcodec_open2_failed_msg - if IS_WINDOWS + if IS_WINDOWS_WITH_FFMPEG_LE_70 else "invalid sample rate=99999999" ), ): @@ -192,7 +206,7 @@ def test_bad_input_parametrized(self, method, tmp_path): for num_channels in (0, 3): match = ( avcodec_open2_failed_msg - if IS_WINDOWS + if IS_WINDOWS_WITH_FFMPEG_LE_70 else re.escape( f"Desired number of channels ({num_channels}) is not supported" ) @@ -316,7 +330,7 @@ def test_against_cli( else: rtol, atol = None, None - if IS_WINDOWS and format == "mp3": + if IS_WINDOWS_WITH_FFMPEG_LE_70 and format == "mp3": # We're getting a "Could not open input file" on Windows mp3 files when decoding. # TODO: https://github.com/pytorch/torchcodec/issues/837 return @@ -370,7 +384,7 @@ def test_against_to_file( else: raise ValueError(f"Unknown method: {method}") - if not (IS_WINDOWS and format == "mp3"): + if not (IS_WINDOWS_WITH_FFMPEG_LE_70 and format == "mp3"): # We're getting a "Could not open input file" on Windows mp3 files when decoding. # TODO: https://github.com/pytorch/torchcodec/issues/837 torch.testing.assert_close( diff --git a/test/utils.py b/test/utils.py index 61bf06295..e11411bd2 100644 --- a/test/utils.py +++ b/test/utils.py @@ -76,16 +76,27 @@ def make_video_decoder(*args, **kwargs) -> tuple[VideoDecoder, str]: return dec, clean_device -def get_ffmpeg_major_version(): +def _get_ffmpeg_version_string(): ffmpeg_version = get_ffmpeg_library_versions()["ffmpeg_version"] # When building FFmpeg from source there can be a `n` prefix in the version # string. This is quite brittle as we're using av_version_info(), which has # no stable format. See https://github.com/pytorch/torchcodec/issues/100 if ffmpeg_version.startswith("n"): ffmpeg_version = ffmpeg_version.removeprefix("n") + + return ffmpeg_version + + +def get_ffmpeg_major_version(): + ffmpeg_version = _get_ffmpeg_version_string() return int(ffmpeg_version.split(".")[0]) +def get_ffmpeg_minor_version(): + ffmpeg_version = _get_ffmpeg_version_string() + return int(ffmpeg_version.split(".")[1]) + + def cuda_version_used_for_building_torch() -> Optional[tuple[int, int]]: # Return the CUDA version that was used to build PyTorch. That's not always # the same as the CUDA version that is currently installed on the running From 9c3c5d23a6e44e5074c9dc4b3b1d023ec19e9181 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Fri, 17 Oct 2025 17:08:59 +0100 Subject: [PATCH 11/20] Check return value of cudaSetDevice (#979) --- src/torchcodec/_core/CudaDeviceInterface.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/torchcodec/_core/CudaDeviceInterface.cpp b/src/torchcodec/_core/CudaDeviceInterface.cpp index c9387fbd9..01fdac827 100644 --- a/src/torchcodec/_core/CudaDeviceInterface.cpp +++ b/src/torchcodec/_core/CudaDeviceInterface.cpp @@ -60,12 +60,10 @@ UniqueAVBufferRef getHardwareDeviceContext(const torch::Device& device) { // Create hardware device context c10::cuda::CUDAGuard deviceGuard(device); - // Valid values for the argument to cudaSetDevice are 0 to maxDevices - 1: - // https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__DEVICE.html#group__CUDART__DEVICE_1g159587909ffa0791bbe4b40187a4c6bb - // So we ensure the deviceIndex is not negative. // We set the device because we may be called from a different thread than // the one that initialized the cuda context. - cudaSetDevice(deviceIndex); + TORCH_CHECK( + cudaSetDevice(deviceIndex) == cudaSuccess, "Failed to set CUDA device"); AVBufferRef* hardwareDeviceCtxRaw = nullptr; std::string deviceOrdinal = std::to_string(deviceIndex); From 3f63c855c1b2174e0b7505a0cdd58325679edb3d Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Fri, 17 Oct 2025 13:37:07 -0400 Subject: [PATCH 12/20] C++ implementation of crop transform (#967) --- .github/workflows/reference_resources.yaml | 46 ++- src/torchcodec/_core/FilterGraph.cpp | 3 +- src/torchcodec/_core/Frame.cpp | 5 + src/torchcodec/_core/Frame.h | 2 +- src/torchcodec/_core/SingleStreamDecoder.cpp | 2 + src/torchcodec/_core/Transform.cpp | 27 ++ src/torchcodec/_core/Transform.h | 25 ++ src/torchcodec/_core/custom_ops.cpp | 22 ++ test/generate_reference_resources.py | 54 +++- ...0_200_50_35_exact_1.stream3.frame000000.pt | Bin 0 -> 181674 bytes ...0_200_50_35_exact_1.stream3.frame000015.pt | Bin 0 -> 181674 bytes ...0_200_50_35_exact_1.stream3.frame000200.pt | Bin 0 -> 181674 bytes ...0_200_50_35_exact_1.stream3.frame000389.pt | Bin 0 -> 181674 bytes test/test_ops.py | 178 ----------- test/test_transform_ops.py | 279 ++++++++++++++++++ test/utils.py | 22 +- 16 files changed, 467 insertions(+), 198 deletions(-) create mode 100644 test/resources/nasa_13013.mp4.crop_300_200_50_35_exact_1.stream3.frame000000.pt create mode 100644 test/resources/nasa_13013.mp4.crop_300_200_50_35_exact_1.stream3.frame000015.pt create mode 100644 test/resources/nasa_13013.mp4.crop_300_200_50_35_exact_1.stream3.frame000200.pt create mode 100644 test/resources/nasa_13013.mp4.crop_300_200_50_35_exact_1.stream3.frame000389.pt create mode 100644 test/test_transform_ops.py diff --git a/.github/workflows/reference_resources.yaml b/.github/workflows/reference_resources.yaml index 25353d70c..8f97378f1 100644 --- a/.github/workflows/reference_resources.yaml +++ b/.github/workflows/reference_resources.yaml @@ -14,7 +14,40 @@ defaults: shell: bash -l -eo pipefail {0} jobs: + generate-matrix: + uses: pytorch/test-infra/.github/workflows/generate_binary_build_matrix.yml@main + with: + package-type: wheel + os: linux + test-infra-repository: pytorch/test-infra + test-infra-ref: main + with-xpu: disable + with-rocm: disable + with-cuda: disable + build-python-only: "disable" + + build: + needs: generate-matrix + strategy: + fail-fast: false + name: Build and Upload Linux wheel + uses: pytorch/test-infra/.github/workflows/build_wheels_linux.yml@main + with: + repository: meta-pytorch/torchcodec + ref: "" + test-infra-repository: pytorch/test-infra + test-infra-ref: main + build-matrix: ${{ needs.generate-matrix.outputs.matrix }} + pre-script: packaging/pre_build_script.sh + post-script: packaging/post_build_script.sh + smoke-test-script: packaging/fake_smoke_test.py + package-name: torchcodec + trigger-event: ${{ github.event_name }} + build-platform: "python-build-package" + build-command: "BUILD_AGAINST_ALL_FFMPEG_FROM_S3=1 python -m build --wheel -vvv --no-isolation" + test-reference-resource-generation: + needs: build runs-on: ubuntu-latest strategy: fail-fast: false @@ -22,6 +55,10 @@ jobs: python-version: ['3.10'] ffmpeg-version-for-tests: ['4.4.2', '5.1.2', '6.1.1', '7.0.1'] steps: + - uses: actions/download-artifact@v4 + with: + name: meta-pytorch_torchcodec__${{ matrix.python-version }}_cpu_x86_64 + path: pytorch/torchcodec/dist/ - name: Setup conda env uses: conda-incubator/setup-miniconda@v2 with: @@ -43,11 +80,16 @@ jobs: # Note that we're installing stable - this is for running a script where we're a normal PyTorch # user, not for building TorhCodec. python -m pip install torch --index-url https://download.pytorch.org/whl/cpu - python -m pip install numpy pillow + python -m pip install numpy pillow pytest + - name: Install torchcodec from the wheel + run: | + wheel_path=`find pytorch/torchcodec/dist -type f -name "*.whl"` + echo Installing $wheel_path + python -m pip install $wheel_path -vvv - name: Check out repo uses: actions/checkout@v3 - name: Run generation reference resources run: | - python test/generate_reference_resources.py + python -m test.generate_reference_resources diff --git a/src/torchcodec/_core/FilterGraph.cpp b/src/torchcodec/_core/FilterGraph.cpp index afc44d96d..605b814a8 100644 --- a/src/torchcodec/_core/FilterGraph.cpp +++ b/src/torchcodec/_core/FilterGraph.cpp @@ -130,7 +130,8 @@ FilterGraph::FilterGraph( TORCH_CHECK( status >= 0, "Failed to configure filter graph: ", - getFFMPEGErrorStringFromErrorCode(status)); + getFFMPEGErrorStringFromErrorCode(status), + ", provided filters: " + filtersContext.filtergraphStr); } UniqueAVFrame FilterGraph::convert(const UniqueAVFrame& avFrame) { diff --git a/src/torchcodec/_core/Frame.cpp b/src/torchcodec/_core/Frame.cpp index 9fa87a1cb..62fb46c65 100644 --- a/src/torchcodec/_core/Frame.cpp +++ b/src/torchcodec/_core/Frame.cpp @@ -8,6 +8,11 @@ namespace facebook::torchcodec { +FrameDims::FrameDims(int height, int width) : height(height), width(width) { + TORCH_CHECK(height > 0, "FrameDims.height must be > 0, got: ", height); + TORCH_CHECK(width > 0, "FrameDims.width must be > 0, got: ", width); +} + FrameBatchOutput::FrameBatchOutput( int64_t numFrames, const FrameDims& outputDims, diff --git a/src/torchcodec/_core/Frame.h b/src/torchcodec/_core/Frame.h index 4b27d5bdd..67e4d2b79 100644 --- a/src/torchcodec/_core/Frame.h +++ b/src/torchcodec/_core/Frame.h @@ -19,7 +19,7 @@ struct FrameDims { FrameDims() = default; - FrameDims(int h, int w) : height(h), width(w) {} + FrameDims(int h, int w); }; // All public video decoding entry points return either a FrameOutput or a diff --git a/src/torchcodec/_core/SingleStreamDecoder.cpp b/src/torchcodec/_core/SingleStreamDecoder.cpp index ba7382c67..2fbc111c1 100644 --- a/src/torchcodec/_core/SingleStreamDecoder.cpp +++ b/src/torchcodec/_core/SingleStreamDecoder.cpp @@ -12,6 +12,7 @@ #include #include #include +#include "Metadata.h" #include "torch/types.h" namespace facebook::torchcodec { @@ -527,6 +528,7 @@ void SingleStreamDecoder::addVideoStream( if (transform->getOutputFrameDims().has_value()) { resizedOutputDims_ = transform->getOutputFrameDims().value(); } + transform->validate(streamMetadata); // Note that we are claiming ownership of the transform objects passed in to // us. diff --git a/src/torchcodec/_core/Transform.cpp b/src/torchcodec/_core/Transform.cpp index d0a5104f3..6083986e1 100644 --- a/src/torchcodec/_core/Transform.cpp +++ b/src/torchcodec/_core/Transform.cpp @@ -57,4 +57,31 @@ int ResizeTransform::getSwsFlags() const { return toSwsInterpolation(interpolationMode_); } +CropTransform::CropTransform(const FrameDims& dims, int x, int y) + : outputDims_(dims), x_(x), y_(y) { + TORCH_CHECK(x_ >= 0, "Crop x position must be >= 0, got: ", x_); + TORCH_CHECK(y_ >= 0, "Crop y position must be >= 0, got: ", y_); +} + +std::string CropTransform::getFilterGraphCpu() const { + return "crop=" + std::to_string(outputDims_.width) + ":" + + std::to_string(outputDims_.height) + ":" + std::to_string(x_) + ":" + + std::to_string(y_) + ":exact=1"; +} + +std::optional CropTransform::getOutputFrameDims() const { + return outputDims_; +} + +void CropTransform::validate(const StreamMetadata& streamMetadata) const { + TORCH_CHECK(x_ <= streamMetadata.width, "Crop x position out of bounds"); + TORCH_CHECK( + x_ + outputDims_.width <= streamMetadata.width, + "Crop x position out of bounds") + TORCH_CHECK(y_ <= streamMetadata.height, "Crop y position out of bounds"); + TORCH_CHECK( + y_ + outputDims_.height <= streamMetadata.height, + "Crop y position out of bounds"); +} + } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/Transform.h b/src/torchcodec/_core/Transform.h index 6aea255ab..28d8c28a2 100644 --- a/src/torchcodec/_core/Transform.h +++ b/src/torchcodec/_core/Transform.h @@ -9,6 +9,7 @@ #include #include #include "src/torchcodec/_core/Frame.h" +#include "src/torchcodec/_core/Metadata.h" namespace facebook::torchcodec { @@ -33,6 +34,16 @@ class Transform { virtual bool isResize() const { return false; } + + // The validity of some transforms depends on the characteristics of the + // AVStream they're being applied to. For example, some transforms will + // specify coordinates inside a frame, we need to validate that those are + // within the frame's bounds. + // + // Note that the validation function does not return anything. We expect + // invalid configurations to throw an exception. + virtual void validate( + [[maybe_unused]] const StreamMetadata& streamMetadata) const {} }; class ResizeTransform : public Transform { @@ -56,4 +67,18 @@ class ResizeTransform : public Transform { InterpolationMode interpolationMode_; }; +class CropTransform : public Transform { + public: + CropTransform(const FrameDims& dims, int x, int y); + + std::string getFilterGraphCpu() const override; + std::optional getOutputFrameDims() const override; + void validate(const StreamMetadata& streamMetadata) const override; + + private: + FrameDims outputDims_; + int x_; + int y_; +}; + } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/custom_ops.cpp b/src/torchcodec/_core/custom_ops.cpp index 94a3fba1b..466ebe50d 100644 --- a/src/torchcodec/_core/custom_ops.cpp +++ b/src/torchcodec/_core/custom_ops.cpp @@ -214,6 +214,26 @@ Transform* makeResizeTransform( return new ResizeTransform(FrameDims(height, width)); } +// Crop transform specs take the form: +// +// "crop, , , , " +// +// Where "crop" is the string literal and , , and are +// positive integers. and are the x and y coordinates of the top left +// corner of the crop. Note that we follow the PyTorch convention of (height, +// width) for specifying image dimensions; FFmpeg uses (width, height). +Transform* makeCropTransform( + const std::vector& cropTransformSpec) { + TORCH_CHECK( + cropTransformSpec.size() == 5, + "cropTransformSpec must have 5 elements including its name"); + int height = checkedToPositiveInt(cropTransformSpec[1]); + int width = checkedToPositiveInt(cropTransformSpec[2]); + int x = checkedToPositiveInt(cropTransformSpec[3]); + int y = checkedToPositiveInt(cropTransformSpec[4]); + return new CropTransform(FrameDims(height, width), x, y); +} + std::vector split(const std::string& str, char delimiter) { std::vector tokens; std::string token; @@ -241,6 +261,8 @@ std::vector makeTransforms(const std::string& transformSpecsRaw) { auto name = transformSpec[0]; if (name == "resize") { transforms.push_back(makeResizeTransform(transformSpec)); + } else if (name == "crop") { + transforms.push_back(makeCropTransform(transformSpec)); } else { TORCH_CHECK(false, "Invalid transform name: " + name); } diff --git a/test/generate_reference_resources.py b/test/generate_reference_resources.py index 5ae062111..fe515ebde 100644 --- a/test/generate_reference_resources.py +++ b/test/generate_reference_resources.py @@ -12,9 +12,15 @@ import torch from PIL import Image +from .utils import sanitize_filtergraph_expression + # Run this script to update the resources used in unit tests. The resources are all derived # from source media already checked into the repo. +SCRIPT_DIR = Path(__file__).resolve().parent +TORCHCODEC_PATH = SCRIPT_DIR.parent +RESOURCES_DIR = TORCHCODEC_PATH / "test" / "resources" + def convert_image_to_tensor(image_path): image_path = Path(image_path) @@ -31,7 +37,18 @@ def convert_image_to_tensor(image_path): image_path.unlink() -def get_frame_by_index(video_path, frame, output_path, stream): +def get_frame_by_index(video_path, frame, output_path, stream, filters=None): + # Note that we have an exlicit format conversion to rgb24 in our filtergraph specification, + # which always happens BEFORE any of the filters that we receive as input. We do this to + # ensure that the color conversion happens BEFORE the filters, matching the behavior of the + # torchcodec filtergraph implementation. + # + # Not doing this would result in the color conversion happening AFTER the filters, which + # would result in different color values for the same frame. + filtergraph = f"select='eq(n\\,{frame})',format=rgb24" + if filters is not None: + filtergraph = filtergraph + f",{filters}" + cmd = [ "ffmpeg", "-y", @@ -40,11 +57,11 @@ def get_frame_by_index(video_path, frame, output_path, stream): "-map", f"0:{stream}", "-vf", - f"select=eq(n\\,{frame})", - "-vsync", - "vfr", - "-q:v", - "2", + filtergraph, + "-fps_mode", + "passthrough", + "-update", + "1", output_path, ] subprocess.run(cmd, check=True) @@ -65,14 +82,9 @@ def get_frame_by_timestamp(video_path, timestamp, output_path): subprocess.run(cmd, check=True) -def main(): - SCRIPT_DIR = Path(__file__).resolve().parent - TORCHCODEC_PATH = SCRIPT_DIR.parent - RESOURCES_DIR = TORCHCODEC_PATH / "test" / "resources" +def generate_nasa_13013_references(): VIDEO_PATH = RESOURCES_DIR / "nasa_13013.mp4" - # Last generated with ffmpeg version 4.3 - # # Note: The naming scheme used here must match the naming scheme used to load # tensors in ./utils.py. STREAMS = [0, 3] @@ -95,6 +107,16 @@ def main(): get_frame_by_timestamp(VIDEO_PATH, timestamp, output_bmp) convert_image_to_tensor(output_bmp) + # Extract frames with specific filters. We have tests that assume these exact filters. + FRAMES = [0, 15, 200, 389] + crop_filter = "crop=300:200:50:35:exact=1" + for frame in FRAMES: + output_bmp = f"{VIDEO_PATH}.{sanitize_filtergraph_expression(crop_filter)}.stream3.frame{frame:06d}.bmp" + get_frame_by_index(VIDEO_PATH, frame, output_bmp, stream=3, filters=crop_filter) + convert_image_to_tensor(output_bmp) + + +def generate_h265_video_references(): # This video was generated by running the following: # conda install -c conda-forge x265 # ./configure --enable-nonfree --enable-gpl --prefix=$(readlink -f ../bin) --enable-libx265 --enable-rpath --extra-ldflags=-Wl,-rpath=$CONDA_PREFIX/lib --enable-filter=drawtext --enable-libfontconfig --enable-libfreetype --enable-libharfbuzz @@ -107,6 +129,8 @@ def main(): get_frame_by_index(VIDEO_PATH, frame, output_bmp, stream=0) convert_image_to_tensor(output_bmp) + +def generate_av1_video_references(): # This video was generated by running the following: # ffmpeg -f lavfi -i testsrc=duration=5:size=640x360:rate=25,format=yuv420p -c:v libaom-av1 -crf 30 -colorspace bt709 -color_primaries bt709 -color_trc bt709 av1_video.mkv # Note that this video only has 1 stream, at index 0. @@ -119,5 +143,11 @@ def main(): convert_image_to_tensor(output_bmp) +def main(): + generate_nasa_13013_references() + generate_h265_video_references() + generate_av1_video_references() + + if __name__ == "__main__": main() diff --git a/test/resources/nasa_13013.mp4.crop_300_200_50_35_exact_1.stream3.frame000000.pt b/test/resources/nasa_13013.mp4.crop_300_200_50_35_exact_1.stream3.frame000000.pt new file mode 100644 index 0000000000000000000000000000000000000000..c69af7cee9257e6a32f5128015d49685616dc3b0 GIT binary patch literal 181674 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%0AQy#;FEW0C`w0G}4Je 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 D@5N%d literal 0 HcmV?d00001 diff --git a/test/resources/nasa_13013.mp4.crop_300_200_50_35_exact_1.stream3.frame000200.pt b/test/resources/nasa_13013.mp4.crop_300_200_50_35_exact_1.stream3.frame000200.pt new file mode 100644 index 0000000000000000000000000000000000000000..9849cfa993a108ed1a9d50fe57388513a40d3779 GIT binary patch literal 181674 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

cz@MruRZzYTPvQpefG4` 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%0AbJ!gq~;wXUQ72 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

ef)kE*~0$GQ#@rD#5Xtq9@) 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 GkNy9tEu!oI literal 0 HcmV?d00001 diff --git a/test/test_ops.py b/test/test_ops.py index 31afbdd14..7d996f259 100644 --- a/test/test_ops.py +++ b/test/test_ops.py @@ -4,7 +4,6 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. -import contextlib import io import os from functools import partial @@ -609,103 +608,6 @@ def test_color_conversion_library(self, color_conversion_library): ) assert_frames_equal(frame_time6, reference_frame_time6) - # We choose arbitrary values for width and height scaling to get better - # test coverage. Some pairs upscale the image while others downscale it. - @pytest.mark.parametrize( - "width_scaling_factor,height_scaling_factor", - ((1.31, 1.5), (0.71, 0.5), (1.31, 0.7), (0.71, 1.5), (1.0, 1.0)), - ) - @pytest.mark.parametrize("input_video", [NASA_VIDEO]) - def test_color_conversion_library_with_scaling( - self, input_video, width_scaling_factor, height_scaling_factor - ): - decoder = create_from_file(str(input_video.path)) - add_video_stream(decoder) - metadata = get_json_metadata(decoder) - metadata_dict = json.loads(metadata) - assert metadata_dict["width"] == input_video.width - assert metadata_dict["height"] == input_video.height - - target_height = int(input_video.height * height_scaling_factor) - target_width = int(input_video.width * width_scaling_factor) - if width_scaling_factor != 1.0: - assert target_width != input_video.width - if height_scaling_factor != 1.0: - assert target_height != input_video.height - - filtergraph_decoder = create_from_file(str(input_video.path)) - _add_video_stream( - filtergraph_decoder, - transform_specs=f"resize, {target_height}, {target_width}", - color_conversion_library="filtergraph", - ) - filtergraph_frame0, _, _ = get_next_frame(filtergraph_decoder) - - swscale_decoder = create_from_file(str(input_video.path)) - _add_video_stream( - swscale_decoder, - transform_specs=f"resize, {target_height}, {target_width}", - color_conversion_library="swscale", - ) - swscale_frame0, _, _ = get_next_frame(swscale_decoder) - assert_frames_equal(filtergraph_frame0, swscale_frame0) - assert filtergraph_frame0.shape == (3, target_height, target_width) - - @needs_cuda - def test_scaling_on_cuda_fails(self): - decoder = create_from_file(str(NASA_VIDEO.path)) - with pytest.raises( - RuntimeError, - match="Transforms are only supported for CPU devices.", - ): - add_video_stream(decoder, device="cuda", transform_specs="resize, 100, 100") - - def test_transform_fails(self): - decoder = create_from_file(str(NASA_VIDEO.path)) - with pytest.raises( - RuntimeError, - match="Invalid transform spec", - ): - add_video_stream(decoder, transform_specs=";") - - with pytest.raises( - RuntimeError, - match="Invalid transform name", - ): - add_video_stream(decoder, transform_specs="invalid, 1, 2") - - def test_resize_transform_fails(self): - decoder = create_from_file(str(NASA_VIDEO.path)) - with pytest.raises( - RuntimeError, - match="must have 3 elements", - ): - add_video_stream(decoder, transform_specs="resize, 100, 100, 100") - - with pytest.raises( - RuntimeError, - match="must be a positive integer", - ): - add_video_stream(decoder, transform_specs="resize, -10, 100") - - with pytest.raises( - RuntimeError, - match="must be a positive integer", - ): - add_video_stream(decoder, transform_specs="resize, 100, 0") - - with pytest.raises( - RuntimeError, - match="cannot be converted to an int", - ): - add_video_stream(decoder, transform_specs="resize, blah, 100") - - with pytest.raises( - RuntimeError, - match="out of range", - ): - add_video_stream(decoder, transform_specs="resize, 100, 1000000000000") - @pytest.mark.parametrize("dimension_order", ("NHWC", "NCHW")) @pytest.mark.parametrize("color_conversion_library", ("filtergraph", "swscale")) def test_color_conversion_library_with_dimension_order( @@ -745,86 +647,6 @@ def test_color_conversion_library_with_dimension_order( assert frames.shape[1:] == expected_shape assert_frames_equal(frames[0], frame0_ref) - @pytest.mark.parametrize( - "width_scaling_factor,height_scaling_factor", - ((1.31, 1.5), (0.71, 0.5), (1.31, 0.7), (0.71, 1.5), (1.0, 1.0)), - ) - @pytest.mark.parametrize("width", [30, 32, 300]) - @pytest.mark.parametrize("height", [128]) - def test_color_conversion_library_with_generated_videos( - self, tmp_path, width, height, width_scaling_factor, height_scaling_factor - ): - - # We consider filtergraph to be the reference color conversion library. - # However the video decoder sometimes uses swscale as that is faster. - # The exact color conversion library used is an implementation detail - # of the video decoder and depends on the video's width. - # - # In this test we compare the output of filtergraph (which is the - # reference) with the output of the video decoder (which may use - # swscale if it chooses for certain video widths) to make sure they are - # always the same. - video_path = f"{tmp_path}/frame_numbers_{width}x{height}.mp4" - # We don't specify a particular encoder because the ffmpeg binary could - # be configured with different encoders. For the purposes of this test, - # the actual encoder is irrelevant. - with contextlib.ExitStack() as stack: - ffmpeg_cli = "ffmpeg" - - if os.environ.get("IN_FBCODE_TORCHCODEC") == "1": - import importlib.resources - - ffmpeg_cli = stack.enter_context( - importlib.resources.path(__package__, "ffmpeg") - ) - - command = [ - ffmpeg_cli, - "-y", - "-f", - "lavfi", - "-i", - "color=blue", - "-pix_fmt", - "yuv420p", - "-s", - f"{width}x{height}", - "-frames:v", - "1", - video_path, - ] - subprocess.check_call(command) - - decoder = create_from_file(str(video_path)) - add_video_stream(decoder) - metadata = get_json_metadata(decoder) - metadata_dict = json.loads(metadata) - assert metadata_dict["width"] == width - assert metadata_dict["height"] == height - - target_height = int(height * height_scaling_factor) - target_width = int(width * width_scaling_factor) - if width_scaling_factor != 1.0: - assert target_width != width - if height_scaling_factor != 1.0: - assert target_height != height - - filtergraph_decoder = create_from_file(str(video_path)) - _add_video_stream( - filtergraph_decoder, - transform_specs=f"resize, {target_height}, {target_width}", - color_conversion_library="filtergraph", - ) - filtergraph_frame0, _, _ = get_next_frame(filtergraph_decoder) - - auto_decoder = create_from_file(str(video_path)) - add_video_stream( - auto_decoder, - transform_specs=f"resize, {target_height}, {target_width}", - ) - auto_frame0, _, _ = get_next_frame(auto_decoder) - assert_frames_equal(filtergraph_frame0, auto_frame0) - @needs_cuda def test_cuda_decoder(self): decoder = create_from_file(str(NASA_VIDEO.path)) diff --git a/test/test_transform_ops.py b/test/test_transform_ops.py new file mode 100644 index 000000000..8d1ba5e53 --- /dev/null +++ b/test/test_transform_ops.py @@ -0,0 +1,279 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import contextlib + +import json +import os +import subprocess + +import pytest + +import torch + +from torchcodec._core import ( + _add_video_stream, + add_video_stream, + create_from_file, + get_frame_at_index, + get_json_metadata, + get_next_frame, +) + +from torchvision.transforms import v2 + +from .utils import assert_frames_equal, NASA_VIDEO, needs_cuda + +torch._dynamo.config.capture_dynamic_output_shape_ops = True + + +class TestVideoDecoderTransformOps: + # We choose arbitrary values for width and height scaling to get better + # test coverage. Some pairs upscale the image while others downscale it. + @pytest.mark.parametrize( + "width_scaling_factor,height_scaling_factor", + ((1.31, 1.5), (0.71, 0.5), (1.31, 0.7), (0.71, 1.5), (1.0, 1.0)), + ) + @pytest.mark.parametrize("input_video", [NASA_VIDEO]) + def test_color_conversion_library_with_scaling( + self, input_video, width_scaling_factor, height_scaling_factor + ): + decoder = create_from_file(str(input_video.path)) + add_video_stream(decoder) + metadata = get_json_metadata(decoder) + metadata_dict = json.loads(metadata) + assert metadata_dict["width"] == input_video.width + assert metadata_dict["height"] == input_video.height + + target_height = int(input_video.height * height_scaling_factor) + target_width = int(input_video.width * width_scaling_factor) + if width_scaling_factor != 1.0: + assert target_width != input_video.width + if height_scaling_factor != 1.0: + assert target_height != input_video.height + + filtergraph_decoder = create_from_file(str(input_video.path)) + _add_video_stream( + filtergraph_decoder, + transform_specs=f"resize, {target_height}, {target_width}", + color_conversion_library="filtergraph", + ) + filtergraph_frame0, _, _ = get_next_frame(filtergraph_decoder) + + swscale_decoder = create_from_file(str(input_video.path)) + _add_video_stream( + swscale_decoder, + transform_specs=f"resize, {target_height}, {target_width}", + color_conversion_library="swscale", + ) + swscale_frame0, _, _ = get_next_frame(swscale_decoder) + assert_frames_equal(filtergraph_frame0, swscale_frame0) + assert filtergraph_frame0.shape == (3, target_height, target_width) + + @pytest.mark.parametrize( + "width_scaling_factor,height_scaling_factor", + ((1.31, 1.5), (0.71, 0.5), (1.31, 0.7), (0.71, 1.5), (1.0, 1.0)), + ) + @pytest.mark.parametrize("width", [30, 32, 300]) + @pytest.mark.parametrize("height", [128]) + def test_color_conversion_library_with_generated_videos( + self, tmp_path, width, height, width_scaling_factor, height_scaling_factor + ): + # We consider filtergraph to be the reference color conversion library. + # However the video decoder sometimes uses swscale as that is faster. + # The exact color conversion library used is an implementation detail + # of the video decoder and depends on the video's width. + # + # In this test we compare the output of filtergraph (which is the + # reference) with the output of the video decoder (which may use + # swscale if it chooses for certain video widths) to make sure they are + # always the same. + video_path = f"{tmp_path}/frame_numbers_{width}x{height}.mp4" + # We don't specify a particular encoder because the ffmpeg binary could + # be configured with different encoders. For the purposes of this test, + # the actual encoder is irrelevant. + with contextlib.ExitStack() as stack: + ffmpeg_cli = "ffmpeg" + + if os.environ.get("IN_FBCODE_TORCHCODEC") == "1": + import importlib.resources + + ffmpeg_cli = stack.enter_context( + importlib.resources.path(__package__, "ffmpeg") + ) + + command = [ + ffmpeg_cli, + "-y", + "-f", + "lavfi", + "-i", + "color=blue", + "-pix_fmt", + "yuv420p", + "-s", + f"{width}x{height}", + "-frames:v", + "1", + video_path, + ] + subprocess.check_call(command) + + decoder = create_from_file(str(video_path)) + add_video_stream(decoder) + metadata = get_json_metadata(decoder) + metadata_dict = json.loads(metadata) + assert metadata_dict["width"] == width + assert metadata_dict["height"] == height + + target_height = int(height * height_scaling_factor) + target_width = int(width * width_scaling_factor) + if width_scaling_factor != 1.0: + assert target_width != width + if height_scaling_factor != 1.0: + assert target_height != height + + filtergraph_decoder = create_from_file(str(video_path)) + _add_video_stream( + filtergraph_decoder, + transform_specs=f"resize, {target_height}, {target_width}", + color_conversion_library="filtergraph", + ) + filtergraph_frame0, _, _ = get_next_frame(filtergraph_decoder) + + auto_decoder = create_from_file(str(video_path)) + add_video_stream( + auto_decoder, + transform_specs=f"resize, {target_height}, {target_width}", + ) + auto_frame0, _, _ = get_next_frame(auto_decoder) + assert_frames_equal(filtergraph_frame0, auto_frame0) + + @needs_cuda + def test_scaling_on_cuda_fails(self): + decoder = create_from_file(str(NASA_VIDEO.path)) + with pytest.raises( + RuntimeError, + match="Transforms are only supported for CPU devices.", + ): + add_video_stream(decoder, device="cuda", transform_specs="resize, 100, 100") + + def test_transform_fails(self): + decoder = create_from_file(str(NASA_VIDEO.path)) + with pytest.raises( + RuntimeError, + match="Invalid transform spec", + ): + add_video_stream(decoder, transform_specs=";") + + with pytest.raises( + RuntimeError, + match="Invalid transform name", + ): + add_video_stream(decoder, transform_specs="invalid, 1, 2") + + def test_resize_transform_fails(self): + decoder = create_from_file(str(NASA_VIDEO.path)) + with pytest.raises( + RuntimeError, + match="must have 3 elements", + ): + add_video_stream(decoder, transform_specs="resize, 100, 100, 100") + + with pytest.raises( + RuntimeError, + match="must be a positive integer", + ): + add_video_stream(decoder, transform_specs="resize, -10, 100") + + with pytest.raises( + RuntimeError, + match="must be a positive integer", + ): + add_video_stream(decoder, transform_specs="resize, 100, 0") + + with pytest.raises( + RuntimeError, + match="cannot be converted to an int", + ): + add_video_stream(decoder, transform_specs="resize, blah, 100") + + with pytest.raises( + RuntimeError, + match="out of range", + ): + add_video_stream(decoder, transform_specs="resize, 100, 1000000000000") + + def test_crop_transform(self): + # Note that filtergraph accepts dimensions as (w, h) and we accept them as (h, w). + width = 300 + height = 200 + x = 50 + y = 35 + crop_spec = f"crop, {height}, {width}, {x}, {y}" + crop_filtergraph = f"crop={width}:{height}:{x}:{y}:exact=1" + expected_shape = (NASA_VIDEO.get_num_color_channels(), height, width) + + decoder_crop = create_from_file(str(NASA_VIDEO.path)) + add_video_stream(decoder_crop, transform_specs=crop_spec) + + decoder_full = create_from_file(str(NASA_VIDEO.path)) + add_video_stream(decoder_full) + + for frame_index in [0, 15, 200, 389]: + frame, *_ = get_frame_at_index(decoder_crop, frame_index=frame_index) + frame_ref = NASA_VIDEO.get_frame_data_by_index( + frame_index, filters=crop_filtergraph + ) + + frame_full, *_ = get_frame_at_index(decoder_full, frame_index=frame_index) + frame_tv = v2.functional.crop( + frame_full, top=y, left=x, height=height, width=width + ) + + assert frame.shape == expected_shape + assert frame_ref.shape == expected_shape + assert frame_tv.shape == expected_shape + + assert_frames_equal(frame, frame_tv) + assert_frames_equal(frame, frame_ref) + + def test_crop_transform_fails(self): + + with pytest.raises( + RuntimeError, + match="must have 5 elements", + ): + decoder = create_from_file(str(NASA_VIDEO.path)) + add_video_stream(decoder, transform_specs="crop, 100, 100") + + with pytest.raises( + RuntimeError, + match="must be a positive integer", + ): + decoder = create_from_file(str(NASA_VIDEO.path)) + add_video_stream(decoder, transform_specs="crop, -10, 100, 100, 100") + + with pytest.raises( + RuntimeError, + match="cannot be converted to an int", + ): + decoder = create_from_file(str(NASA_VIDEO.path)) + add_video_stream(decoder, transform_specs="crop, 100, 100, blah, 100") + + with pytest.raises( + RuntimeError, + match="x position out of bounds", + ): + decoder = create_from_file(str(NASA_VIDEO.path)) + add_video_stream(decoder, transform_specs="crop, 100, 100, 9999, 100") + + with pytest.raises( + RuntimeError, + match="y position out of bounds", + ): + decoder = create_from_file(str(NASA_VIDEO.path)) + add_video_stream(decoder, transform_specs="crop, 999, 100, 100, 100") diff --git a/test/utils.py b/test/utils.py index e11411bd2..b59681b37 100644 --- a/test/utils.py +++ b/test/utils.py @@ -167,6 +167,12 @@ def assert_tensor_close_on_at_least( ) +# We embed filtergraph expressions in filenames, but they contain characters that +# some filesystems don't like. We turn all special characters into underscores. +def sanitize_filtergraph_expression(expression: str) -> str: + return "".join(c if c.isalnum() else "_" for c in expression) + + def in_fbcode() -> bool: return os.environ.get("IN_FBCODE_TORCHCODEC") == "1" @@ -366,14 +372,22 @@ class TestVideo(TestContainerFile): """Base class for the *video* streams of a video container""" def get_frame_data_by_index( - self, idx: int, *, stream_index: Optional[int] = None + self, + idx: int, + *, + stream_index: Optional[int] = None, + filters: Optional[str] = None, ) -> torch.Tensor: if stream_index is None: stream_index = self.default_stream_index - file_path = _get_file_path( - f"{self.filename}.stream{stream_index}.frame{idx:06d}.pt" - ) + stream_and_frame = f"stream{stream_index}.frame{idx:06d}" + if filters is not None: + full_name = f"{self.filename}.{sanitize_filtergraph_expression(filters)}.{stream_and_frame}.pt" + else: + full_name = f"{self.filename}.{stream_and_frame}.pt" + + file_path = _get_file_path(full_name) return torch.load(file_path, weights_only=True).permute(2, 0, 1) def get_frame_data_by_range( From 4ea66851287210b17f526dd5bdec6ad4e78e4a69 Mon Sep 17 00:00:00 2001 From: Dan-Flores Date: Sat, 18 Oct 2025 18:58:27 -0400 Subject: [PATCH 13/20] Resolve lint in VideoEncoder by setting avStream_ (#981) Co-authored-by: Daniel Flores --- src/torchcodec/_core/Encoder.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchcodec/_core/Encoder.h b/src/torchcodec/_core/Encoder.h index 7aff0bdbc..168591616 100644 --- a/src/torchcodec/_core/Encoder.h +++ b/src/torchcodec/_core/Encoder.h @@ -162,7 +162,7 @@ class VideoEncoder { UniqueEncodingAVFormatContext avFormatContext_; UniqueAVCodecContext avCodecContext_; - AVStream* avStream_; + AVStream* avStream_ = nullptr; UniqueSwsContext swsContext_; const torch::Tensor frames_; From 80826d3efcc57ea650c9a05eac06fa379efe2b12 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Mon, 20 Oct 2025 17:25:43 +0100 Subject: [PATCH 14/20] Fallback to CPU when video isn't supported by NVDEC (#977) Co-authored-by: Molly Xu Co-authored-by: Molly Xu <64995721+mollyxu@users.noreply.github.com> --- .../_core/BetaCudaDeviceInterface.cpp | 214 ++++++++++++------ .../_core/BetaCudaDeviceInterface.h | 2 + test/test_decoders.py | 40 ++-- 3 files changed, 164 insertions(+), 92 deletions(-) diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp index d55bb1137..7124e4309 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp @@ -53,74 +53,6 @@ pfnDisplayPictureCallback(void* pUserData, CUVIDPARSERDISPINFO* dispInfo) { } static UniqueCUvideodecoder createDecoder(CUVIDEOFORMAT* videoFormat) { - // Check decoder capabilities - same checks as DALI - auto caps = CUVIDDECODECAPS{}; - caps.eCodecType = videoFormat->codec; - caps.eChromaFormat = videoFormat->chroma_format; - caps.nBitDepthMinus8 = videoFormat->bit_depth_luma_minus8; - CUresult result = cuvidGetDecoderCaps(&caps); - TORCH_CHECK(result == CUDA_SUCCESS, "Failed to get decoder caps: ", result); - - TORCH_CHECK( - caps.bIsSupported, - "Codec configuration not supported on this GPU. " - "Codec: ", - static_cast(videoFormat->codec), - ", chroma format: ", - static_cast(videoFormat->chroma_format), - ", bit depth: ", - videoFormat->bit_depth_luma_minus8 + 8); - - TORCH_CHECK( - videoFormat->coded_width >= caps.nMinWidth && - videoFormat->coded_height >= caps.nMinHeight, - "Video is too small in at least one dimension. Provided: ", - videoFormat->coded_width, - "x", - videoFormat->coded_height, - " vs supported:", - caps.nMinWidth, - "x", - caps.nMinHeight); - - TORCH_CHECK( - videoFormat->coded_width <= caps.nMaxWidth && - videoFormat->coded_height <= caps.nMaxHeight, - "Video is too large in at least one dimension. Provided: ", - videoFormat->coded_width, - "x", - videoFormat->coded_height, - " vs supported:", - caps.nMaxWidth, - "x", - caps.nMaxHeight); - - // See nMaxMBCount in cuviddec.h - constexpr unsigned int macroblockConstant = 256; - TORCH_CHECK( - videoFormat->coded_width * videoFormat->coded_height / - macroblockConstant <= - caps.nMaxMBCount, - "Video is too large (too many macroblocks). " - "Provided (width * height / ", - macroblockConstant, - "): ", - videoFormat->coded_width * videoFormat->coded_height / macroblockConstant, - " vs supported:", - caps.nMaxMBCount); - - // Below we'll set the decoderParams.OutputFormat to NV12, so we need to make - // sure it's actually supported. - TORCH_CHECK( - (caps.nOutputFormatMask >> cudaVideoSurfaceFormat_NV12) & 1, - "NV12 output format is not supported for this configuration. ", - "Codec: ", - static_cast(videoFormat->codec), - ", chroma format: ", - static_cast(videoFormat->chroma_format), - ", bit depth: ", - videoFormat->bit_depth_luma_minus8 + 8); - // Decoder creation parameters, most are taken from DALI CUVIDDECODECREATEINFO decoderParams = {}; decoderParams.bitDepthMinus8 = videoFormat->bit_depth_luma_minus8; @@ -157,13 +89,39 @@ static UniqueCUvideodecoder createDecoder(CUVIDEOFORMAT* videoFormat) { decoderParams.display_area.bottom = videoFormat->display_area.bottom; CUvideodecoder* decoder = new CUvideodecoder(); - result = cuvidCreateDecoder(decoder, &decoderParams); + CUresult result = cuvidCreateDecoder(decoder, &decoderParams); TORCH_CHECK( result == CUDA_SUCCESS, "Failed to create NVDEC decoder: ", result); return UniqueCUvideodecoder(decoder, CUvideoDecoderDeleter{}); } -cudaVideoCodec validateCodecSupport(AVCodecID codecId) { +std::optional validateChromaSupport( + const AVPixFmtDescriptor* desc) { + // Return the corresponding cudaVideoChromaFormat if supported, std::nullopt + // otherwise. + TORCH_CHECK(desc != nullptr, "desc can't be null"); + + if (desc->nb_components == 1) { + return cudaVideoChromaFormat_Monochrome; + } else if (desc->nb_components >= 3 && !(desc->flags & AV_PIX_FMT_FLAG_RGB)) { + // Make sure it's YUV: has chroma planes and isn't RGB + if (desc->log2_chroma_w == 0 && desc->log2_chroma_h == 0) { + return cudaVideoChromaFormat_444; // 1x1 subsampling = 4:4:4 + } else if (desc->log2_chroma_w == 1 && desc->log2_chroma_h == 1) { + return cudaVideoChromaFormat_420; // 2x2 subsampling = 4:2:0 + } else if (desc->log2_chroma_w == 1 && desc->log2_chroma_h == 0) { + return cudaVideoChromaFormat_422; // 2x1 subsampling = 4:2:2 + } + } + + return std::nullopt; +} + +std::optional validateCodecSupport(AVCodecID codecId) { + // Return the corresponding cudaVideoCodec if supported, std::nullopt + // otherwise + // Note that we currently return nullopt (and thus fallback to CPU) for some + // codecs that are technically supported by NVDEC, see comment below. switch (codecId) { case AV_CODEC_ID_H264: return cudaVideoCodec_H264; @@ -189,10 +147,69 @@ cudaVideoCodec validateCodecSupport(AVCodecID codecId) { // return cudaVideoCodec_JPEG; // case AV_CODEC_ID_VC1: // return cudaVideoCodec_VC1; - default: { - TORCH_CHECK(false, "Unsupported codec type: ", avcodec_get_name(codecId)); - } + default: + return std::nullopt; + } +} + +bool nativeNVDECSupport(const SharedAVCodecContext& codecContext) { + // Return true iff the input video stream is supported by our NVDEC + // implementation. + auto codecType = validateCodecSupport(codecContext->codec_id); + if (!codecType.has_value()) { + return false; + } + + const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(codecContext->pix_fmt); + if (!desc) { + return false; + } + + auto chromaFormat = validateChromaSupport(desc); + if (!chromaFormat.has_value()) { + return false; + } + + auto caps = CUVIDDECODECAPS{}; + caps.eCodecType = codecType.value(); + caps.eChromaFormat = chromaFormat.value(); + caps.nBitDepthMinus8 = desc->comp[0].depth - 8; + + CUresult result = cuvidGetDecoderCaps(&caps); + if (result != CUDA_SUCCESS) { + return false; + } + + if (!caps.bIsSupported) { + return false; + } + + auto coded_width = static_cast(codecContext->coded_width); + auto coded_height = static_cast(codecContext->coded_height); + if (coded_width < static_cast(caps.nMinWidth) || + coded_height < static_cast(caps.nMinHeight) || + coded_width > caps.nMaxWidth || coded_height > caps.nMaxHeight) { + return false; + } + + // See nMaxMBCount in cuviddec.h + constexpr unsigned int macroblockConstant = 256; + if (coded_width * coded_height / macroblockConstant > caps.nMaxMBCount) { + return false; + } + + // We'll set the decoderParams.OutputFormat to NV12, so we need to make + // sure it's actually supported. + // TODO: If this fail, we could consider decoding to something else than NV12 + // (like cudaVideoSurfaceFormat_P016) instead of falling back to CPU. This is + // what FFmpeg does. + bool supportsNV12Output = + (caps.nOutputFormatMask >> cudaVideoSurfaceFormat_NV12) & 1; + if (!supportsNV12Output) { + return false; } + + return true; } } // namespace @@ -232,6 +249,19 @@ void BetaCudaDeviceInterface::initialize( const AVStream* avStream, const UniqueDecodingAVFormatContext& avFormatCtx, [[maybe_unused]] const SharedAVCodecContext& codecContext) { + if (!nativeNVDECSupport(codecContext)) { + cpuFallback_ = createDeviceInterface(torch::kCPU); + TORCH_CHECK( + cpuFallback_ != nullptr, "Failed to create CPU device interface"); + cpuFallback_->initialize(avStream, avFormatCtx, codecContext); + cpuFallback_->initializeVideo( + VideoStreamOptions(), + {}, + /*resizedOutputDims=*/std::nullopt); + // We'll always use the CPU fallback from now on, so we can return early. + return; + } + TORCH_CHECK(avStream != nullptr, "AVStream cannot be null"); timeBase_ = avStream->time_base; frameRateAvgFromFFmpeg_ = avStream->r_frame_rate; @@ -243,7 +273,11 @@ void BetaCudaDeviceInterface::initialize( // Create parser. Default values that aren't obvious are taken from DALI. CUVIDPARSERPARAMS parserParams = {}; - parserParams.CodecType = validateCodecSupport(codecPar->codec_id); + auto codecType = validateCodecSupport(codecPar->codec_id); + TORCH_CHECK( + codecType.has_value(), + "This should never happen, we should be using the CPU fallback by now. Please report a bug."); + parserParams.CodecType = codecType.value(); parserParams.ulMaxNumDecodeSurfaces = 8; parserParams.ulMaxDisplayDelay = 0; // Callback setup, all are triggered by the parser within a call @@ -383,6 +417,10 @@ int BetaCudaDeviceInterface::streamPropertyChange(CUVIDEOFORMAT* videoFormat) { // Moral equivalent of avcodec_send_packet(). Here, we pass the AVPacket down to // the NVCUVID parser. int BetaCudaDeviceInterface::sendPacket(ReferenceAVPacket& packet) { + if (cpuFallback_) { + return cpuFallback_->sendPacket(packet); + } + TORCH_CHECK( packet.get() && packet->data && packet->size > 0, "sendPacket received an empty packet, this is unexpected, please report."); @@ -406,6 +444,10 @@ int BetaCudaDeviceInterface::sendPacket(ReferenceAVPacket& packet) { } int BetaCudaDeviceInterface::sendEOFPacket() { + if (cpuFallback_) { + return cpuFallback_->sendEOFPacket(); + } + CUVIDSOURCEDATAPACKET cuvidPacket = {}; cuvidPacket.flags = CUVID_PKT_ENDOFSTREAM; eofSent_ = true; @@ -467,6 +509,10 @@ int BetaCudaDeviceInterface::frameReadyInDisplayOrder( // Moral equivalent of avcodec_receive_frame(). int BetaCudaDeviceInterface::receiveFrame(UniqueAVFrame& avFrame) { + if (cpuFallback_) { + return cpuFallback_->receiveFrame(avFrame); + } + if (readyFrames_.empty()) { // No frame found, instruct caller to try again later after sending more // packets, or to stop if EOF was already sent. @@ -601,6 +647,11 @@ UniqueAVFrame BetaCudaDeviceInterface::convertCudaFrameToAVFrame( } void BetaCudaDeviceInterface::flush() { + if (cpuFallback_) { + cpuFallback_->flush(); + return; + } + // The NVCUVID docs mention that after seeking, i.e. when flush() is called, // we should send a packet with the CUVID_PKT_DISCONTINUITY flag. The docs // don't say whether this should be an empty packet, or whether it should be a @@ -618,6 +669,21 @@ void BetaCudaDeviceInterface::convertAVFrameToFrameOutput( UniqueAVFrame& avFrame, FrameOutput& frameOutput, std::optional preAllocatedOutputTensor) { + if (cpuFallback_) { + // CPU decoded frame - need to do CPU color conversion then transfer to GPU + FrameOutput cpuFrameOutput; + cpuFallback_->convertAVFrameToFrameOutput(avFrame, cpuFrameOutput); + + // Transfer CPU frame to GPU + if (preAllocatedOutputTensor.has_value()) { + preAllocatedOutputTensor.value().copy_(cpuFrameOutput.data); + frameOutput.data = preAllocatedOutputTensor.value(); + } else { + frameOutput.data = cpuFrameOutput.data.to(device_); + } + return; + } + // TODONVDEC P2: we may need to handle 10bit videos the same way the CUDA // ffmpeg interface does it with maybeConvertAVFrameToNV12OrRGB24(). TORCH_CHECK( diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.h b/src/torchcodec/_core/BetaCudaDeviceInterface.h index fb01415d4..7424a877d 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.h +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.h @@ -94,6 +94,8 @@ class BetaCudaDeviceInterface : public DeviceInterface { // NPP context for color conversion UniqueNppContext nppCtx_; + + std::unique_ptr cpuFallback_; }; } // namespace facebook::torchcodec diff --git a/test/test_decoders.py b/test/test_decoders.py index 32367b438..098e4e969 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1701,20 +1701,19 @@ def test_beta_cuda_interface_backwards(self, asset, seek_mode): assert beta_frame.duration_seconds == ref_frame.duration_seconds @needs_cuda - def test_beta_cuda_interface_small_h265(self): - # Test to illustrate current difference in behavior between the BETA and - # the ffmpeg interface: this video isn't supported by NVDEC, but in the - # ffmpeg interface, FFMPEG fallsback to the CPU while we don't. - - VideoDecoder(H265_VIDEO.path, device="cuda").get_frame_at(0) - + def test_beta_cuda_interface_cpu_fallback(self): + # Non-regression test for the CPU fallback behavior of the BETA CUDA + # interface. + # We know that the H265_VIDEO asset isn't supported by NVDEC, its + # dimensions are too small. We also know that the FFmpeg CUDA interface + # fallbacks to the CPU path in such cases. We assert that we fall back + # to the CPU path, too. + + ffmpeg = VideoDecoder(H265_VIDEO.path, device="cuda").get_frame_at(0) with set_cuda_backend("beta"): - dec = VideoDecoder(H265_VIDEO.path, device="cuda") - with pytest.raises( - RuntimeError, - match="Video is too small in at least one dimension. Provided: 128x128 vs supported:144x144", - ): - dec.get_frame_at(0) + beta = VideoDecoder(H265_VIDEO.path, device="cuda").get_frame_at(0) + + torch.testing.assert_close(ffmpeg.data, beta.data, rtol=0, atol=0) @needs_cuda def test_beta_cuda_interface_error(self): @@ -1740,15 +1739,20 @@ def test_set_cuda_backend(self): assert _get_cuda_backend() == "beta" def assert_decoder_uses(decoder, *, expected_backend): + # TODO: This doesn't work anymore after + # https://github.com/meta-pytorch/torchcodec/pull/977 + # We need to define a better way to assert which backend a decoder + # is using. + return # Assert that a decoder instance is using a given backend. # # We know H265_VIDEO fails on the BETA backend while it works on the # ffmpeg one. - if expected_backend == "ffmpeg": - decoder.get_frame_at(0) # this would fail if this was BETA - else: - with pytest.raises(RuntimeError, match="Video is too small"): - decoder.get_frame_at(0) + # if expected_backend == "ffmpeg": + # decoder.get_frame_at(0) # this would fail if this was BETA + # else: + # with pytest.raises(RuntimeError, match="Video is too small"): + # decoder.get_frame_at(0) # Check that the default is the ffmpeg backend assert _get_cuda_backend() == "ffmpeg" From 9ea65ea0734aef82f8cf70d898ffbe288903114b Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Mon, 20 Oct 2025 17:47:01 -0400 Subject: [PATCH 15/20] Refactor gen resources (#984) --- test/generate_reference_resources.py | 93 +++++------ test/utils.py | 239 ++++++++++++++------------- 2 files changed, 171 insertions(+), 161 deletions(-) diff --git a/test/generate_reference_resources.py b/test/generate_reference_resources.py index fe515ebde..953fb996e 100644 --- a/test/generate_reference_resources.py +++ b/test/generate_reference_resources.py @@ -6,23 +6,20 @@ import subprocess from pathlib import Path +from typing import Optional import numpy as np import torch from PIL import Image -from .utils import sanitize_filtergraph_expression +from .utils import AV1_VIDEO, H265_VIDEO, NASA_VIDEO, TestVideo # Run this script to update the resources used in unit tests. The resources are all derived # from source media already checked into the repo. -SCRIPT_DIR = Path(__file__).resolve().parent -TORCHCODEC_PATH = SCRIPT_DIR.parent -RESOURCES_DIR = TORCHCODEC_PATH / "test" / "resources" - -def convert_image_to_tensor(image_path): +def convert_image_to_tensor(image_path: str) -> None: image_path = Path(image_path) if not image_path.exists(): return @@ -37,7 +34,23 @@ def convert_image_to_tensor(image_path): image_path.unlink() -def get_frame_by_index(video_path, frame, output_path, stream, filters=None): +def generate_frame_by_index( + video: TestVideo, + *, + frame_index: int, + stream_index: int, + filters: Optional[str] = None, +) -> None: + # Note that we are using 0-based index naming. As a result, we are + # generating files one-by-one, giving the actual file name that we want. + # ffmpeg does have an option to generate multiple files for us, but it uses + # 1-based indexing. We can't use 1-based indexing because we want to match + # the 0-based indexing in our tests. + base_path = video.get_base_path_by_index( + frame_index, stream_index=stream_index, filters=filters + ) + output_bmp = f"{base_path}.bmp" + # Note that we have an exlicit format conversion to rgb24 in our filtergraph specification, # which always happens BEFORE any of the filters that we receive as input. We do this to # ensure that the color conversion happens BEFORE the filters, matching the behavior of the @@ -45,7 +58,7 @@ def get_frame_by_index(video_path, frame, output_path, stream, filters=None): # # Not doing this would result in the color conversion happening AFTER the filters, which # would result in different color values for the same frame. - filtergraph = f"select='eq(n\\,{frame})',format=rgb24" + filtergraph = f"select='eq(n\\,{frame_index})',format=rgb24" if filters is not None: filtergraph = filtergraph + f",{filters}" @@ -53,21 +66,24 @@ def get_frame_by_index(video_path, frame, output_path, stream, filters=None): "ffmpeg", "-y", "-i", - video_path, + video.path, "-map", - f"0:{stream}", + f"0:{stream_index}", "-vf", filtergraph, "-fps_mode", "passthrough", "-update", "1", - output_path, + output_bmp, ] subprocess.run(cmd, check=True) + convert_image_to_tensor(output_bmp) -def get_frame_by_timestamp(video_path, timestamp, output_path): +def generate_frame_by_timestamp( + video_path: str, timestamp: float, output_path: str +) -> None: cmd = [ "ffmpeg", "-y", @@ -80,40 +96,32 @@ def get_frame_by_timestamp(video_path, timestamp, output_path): output_path, ] subprocess.run(cmd, check=True) + convert_image_to_tensor(output_path) def generate_nasa_13013_references(): - VIDEO_PATH = RESOURCES_DIR / "nasa_13013.mp4" - # Note: The naming scheme used here must match the naming scheme used to load # tensors in ./utils.py. - STREAMS = [0, 3] - FRAMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 20, 25, 30, 35, 386, 387, 388, 389] - for stream in STREAMS: - for frame in FRAMES: - # Note that we are using 0-based index naming. Asking ffmpeg to number output - # frames would result in 1-based index naming. We enforce 0-based index naming - # so that the name of reference frames matches the index when accessing that - # frame in the Python decoder. - output_bmp = f"{VIDEO_PATH}.stream{stream}.frame{frame:06d}.bmp" - get_frame_by_index(VIDEO_PATH, frame, output_bmp, stream=stream) - convert_image_to_tensor(output_bmp) + streams = [0, 3] + frames = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 20, 25, 30, 35, 386, 387, 388, 389] + for stream in streams: + for frame in frames: + generate_frame_by_index(NASA_VIDEO, frame_index=frame, stream_index=stream) # Extract individual frames at specific timestamps, including the last frame of the video. seek_timestamp = [6.0, 6.1, 10.0, 12.979633] timestamp_name = [f"{seek_timestamp:06f}" for seek_timestamp in seek_timestamp] for timestamp, name in zip(seek_timestamp, timestamp_name): - output_bmp = f"{VIDEO_PATH}.time{name}.bmp" - get_frame_by_timestamp(VIDEO_PATH, timestamp, output_bmp) - convert_image_to_tensor(output_bmp) + output_bmp = f"{NASA_VIDEO.path}.time{name}.bmp" + generate_frame_by_timestamp(NASA_VIDEO.path, timestamp, output_bmp) # Extract frames with specific filters. We have tests that assume these exact filters. - FRAMES = [0, 15, 200, 389] + frames = [0, 15, 200, 389] crop_filter = "crop=300:200:50:35:exact=1" - for frame in FRAMES: - output_bmp = f"{VIDEO_PATH}.{sanitize_filtergraph_expression(crop_filter)}.stream3.frame{frame:06d}.bmp" - get_frame_by_index(VIDEO_PATH, frame, output_bmp, stream=3, filters=crop_filter) - convert_image_to_tensor(output_bmp) + for frame in frames: + generate_frame_by_index( + NASA_VIDEO, frame_index=frame, stream_index=3, filters=crop_filter + ) def generate_h265_video_references(): @@ -122,25 +130,18 @@ def generate_h265_video_references(): # ./configure --enable-nonfree --enable-gpl --prefix=$(readlink -f ../bin) --enable-libx265 --enable-rpath --extra-ldflags=-Wl,-rpath=$CONDA_PREFIX/lib --enable-filter=drawtext --enable-libfontconfig --enable-libfreetype --enable-libharfbuzz # ffmpeg -f lavfi -i color=size=128x128:duration=1:rate=10:color=blue -vf "drawtext=fontsize=30:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text='Frame %{frame_num}'" -vcodec libx265 -pix_fmt yuv420p -g 2 -crf 10 h265_video.mp4 -y # Note that this video only has 1 stream, at index 0. - VIDEO_PATH = RESOURCES_DIR / "h265_video.mp4" - FRAMES = [5] - for frame in FRAMES: - output_bmp = f"{VIDEO_PATH}.stream0.frame{frame:06d}.bmp" - get_frame_by_index(VIDEO_PATH, frame, output_bmp, stream=0) - convert_image_to_tensor(output_bmp) + frames = [5] + for frame in frames: + generate_frame_by_index(H265_VIDEO, frame_index=frame, stream_index=0) def generate_av1_video_references(): # This video was generated by running the following: # ffmpeg -f lavfi -i testsrc=duration=5:size=640x360:rate=25,format=yuv420p -c:v libaom-av1 -crf 30 -colorspace bt709 -color_primaries bt709 -color_trc bt709 av1_video.mkv # Note that this video only has 1 stream, at index 0. - VIDEO_PATH = RESOURCES_DIR / "av1_video.mkv" - FRAMES = [10] - - for frame in FRAMES: - output_bmp = f"{VIDEO_PATH}.stream0.frame{frame:06d}.bmp" - get_frame_by_index(VIDEO_PATH, frame, output_bmp, stream=0) - convert_image_to_tensor(output_bmp) + frames = [10] + for frame in frames: + generate_frame_by_index(AV1_VIDEO, frame_index=frame, stream_index=0) def main(): diff --git a/test/utils.py b/test/utils.py index b59681b37..cbd6a5bf4 100644 --- a/test/utils.py +++ b/test/utils.py @@ -371,6 +371,17 @@ def empty_duration_seconds(self) -> torch.Tensor: class TestVideo(TestContainerFile): """Base class for the *video* streams of a video container""" + def get_base_path_by_index( + self, idx: int, *, stream_index: int, filters: Optional[str] = None + ) -> pathlib.Path: + stream_and_frame = f"stream{stream_index}.frame{idx:06d}" + if filters is not None: + full_name = f"{self.filename}.{sanitize_filtergraph_expression(filters)}.{stream_and_frame}" + else: + full_name = f"{self.filename}.{stream_and_frame}" + + return _get_file_path(full_name) + def get_frame_data_by_index( self, idx: int, @@ -381,14 +392,11 @@ def get_frame_data_by_index( if stream_index is None: stream_index = self.default_stream_index - stream_and_frame = f"stream{stream_index}.frame{idx:06d}" - if filters is not None: - full_name = f"{self.filename}.{sanitize_filtergraph_expression(filters)}.{stream_and_frame}.pt" - else: - full_name = f"{self.filename}.{stream_and_frame}.pt" - - file_path = _get_file_path(full_name) - return torch.load(file_path, weights_only=True).permute(2, 0, 1) + base_path = self.get_base_path_by_index( + idx, stream_index=stream_index, filters=filters + ) + tensor_file_path = f"{base_path}.pt" + return torch.load(tensor_file_path, weights_only=True).permute(2, 0, 1) def get_frame_data_by_range( self, @@ -485,6 +493,114 @@ def get_empty_chw_tensor(self, *, stream_index: int) -> torch.Tensor: ) +H265_VIDEO = TestVideo( + filename="h265_video.mp4", + default_stream_index=0, + # This metadata is extracted manually. + # $ ffprobe -v error -hide_banner -select_streams v:0 -show_frames -of json test/resources/h265_video.mp4 > out.json + stream_infos={ + 0: TestVideoStreamInfo(width=128, height=128, num_color_channels=3), + }, + frames={ + 0: { + 6: TestFrameInfo(pts_seconds=0.6, duration_seconds=0.1), + }, + }, +) + +AV1_VIDEO = TestVideo( + filename="av1_video.mkv", + default_stream_index=0, + # This metadata is extracted manually. + # $ ffprobe -v error -hide_banner -select_streams v:0 -show_frames -of json test/resources/av1_video.mkv > out.json + stream_infos={ + 0: TestVideoStreamInfo(width=640, height=360, num_color_channels=3), + }, + frames={ + 0: { + 10: TestFrameInfo(pts_seconds=0.400000, duration_seconds=0.040000), + }, + }, +) + + +# This is a BT.709 full range video, generated with: +# ffmpeg -f lavfi -i testsrc2=duration=1:size=1920x720:rate=30 \ +# -c:v libx264 -pix_fmt yuv420p -color_primaries bt709 -color_trc bt709 \ +# -colorspace bt709 -color_range pc bt709_full_range.mp4 +# +# We can confirm the color space and color range with: +# ffprobe -v quiet -select_streams v:0 -show_entries stream=color_space,color_transfer,color_primaries,color_range -of default=noprint_wrappers=1 test/resources/bt709_full_range.mp4 +# color_range=pc +# color_space=bt709 +# color_transfer=bt709 +# color_primaries=bt709 +BT709_FULL_RANGE = TestVideo( + filename="bt709_full_range.mp4", + default_stream_index=0, + stream_infos={ + 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), + }, + frames={0: {}}, # Not needed for now +) + +# ffmpeg -f lavfi -i testsrc2=duration=2:size=1280x720:rate=30 -c:v libx264 -profile:v baseline -level 3.1 -pix_fmt yuv420p -b:v 2500k -r 30 -movflags +faststart output_720p_2s.mp4 +TEST_SRC_2_720P = TestVideo( + filename="testsrc2.mp4", + default_stream_index=0, + stream_infos={ + 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), + }, + frames={0: {}}, # Not needed for now +) +# ffmpeg -f lavfi -i testsrc2=duration=10:size=1280x720:rate=30 -c:v libx265 -crf 23 -preset medium output.mp4 +TEST_SRC_2_720P_H265 = TestVideo( + filename="testsrc2_h265.mp4", + default_stream_index=0, + stream_infos={ + 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), + }, + frames={0: {}}, # Not needed for now +) + +# ffmpeg -f lavfi -i testsrc2=size=1280x720:rate=30:duration=1 -c:v libvpx-vp9 -b:v 1M output_vp9.webm +TEST_SRC_2_720P_VP9 = TestVideo( + filename="testsrc2_vp9.webm", + default_stream_index=0, + stream_infos={ + 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), + }, + frames={0: {}}, # Not needed for now +) + +# ffmpeg -f lavfi -i testsrc2=size=1280x720:rate=30:duration=1 -c:v libvpx -b:v 1M output_vp8.webm +TEST_SRC_2_720P_VP8 = TestVideo( + filename="testsrc2_vp8.webm", + default_stream_index=0, + stream_infos={ + 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), + }, + frames={0: {}}, # Not needed for now +) + +# ffmpeg -f lavfi -i testsrc2=size=1280x720:rate=30:duration=1 -c:v mpeg4 -q:v 5 output_mpeg4.avi +TEST_SRC_2_720P_MPEG4 = TestVideo( + filename="testsrc2_mpeg4.avi", + default_stream_index=0, + stream_infos={ + 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), + }, + frames={0: {}}, # Not needed for now +) + + +def supports_approximate_mode(asset: TestVideo) -> bool: + # Those are missing the `duration` field so they fail in approximate mode (on all devices). + # TODO: we should address this, see + # https://github.com/meta-pytorch/torchcodec/issues/945 + return asset not in (AV1_VIDEO, TEST_SRC_2_720P_VP9, TEST_SRC_2_720P_VP8) + + @dataclass class TestAudio(TestContainerFile): """Base class for the *audio* streams of a container (potentially a video), @@ -698,110 +814,3 @@ def sample_format(self) -> str: ) }, ) - -H265_VIDEO = TestVideo( - filename="h265_video.mp4", - default_stream_index=0, - # This metadata is extracted manually. - # $ ffprobe -v error -hide_banner -select_streams v:0 -show_frames -of json test/resources/h265_video.mp4 > out.json - stream_infos={ - 0: TestVideoStreamInfo(width=128, height=128, num_color_channels=3), - }, - frames={ - 0: { - 6: TestFrameInfo(pts_seconds=0.6, duration_seconds=0.1), - }, - }, -) - -AV1_VIDEO = TestVideo( - filename="av1_video.mkv", - default_stream_index=0, - # This metadata is extracted manually. - # $ ffprobe -v error -hide_banner -select_streams v:0 -show_frames -of json test/resources/av1_video.mkv > out.json - stream_infos={ - 0: TestVideoStreamInfo(width=640, height=360, num_color_channels=3), - }, - frames={ - 0: { - 10: TestFrameInfo(pts_seconds=0.400000, duration_seconds=0.040000), - }, - }, -) - - -# This is a BT.709 full range video, generated with: -# ffmpeg -f lavfi -i testsrc2=duration=1:size=1920x720:rate=30 \ -# -c:v libx264 -pix_fmt yuv420p -color_primaries bt709 -color_trc bt709 \ -# -colorspace bt709 -color_range pc bt709_full_range.mp4 -# -# We can confirm the color space and color range with: -# ffprobe -v quiet -select_streams v:0 -show_entries stream=color_space,color_transfer,color_primaries,color_range -of default=noprint_wrappers=1 test/resources/bt709_full_range.mp4 -# color_range=pc -# color_space=bt709 -# color_transfer=bt709 -# color_primaries=bt709 -BT709_FULL_RANGE = TestVideo( - filename="bt709_full_range.mp4", - default_stream_index=0, - stream_infos={ - 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), - }, - frames={0: {}}, # Not needed for now -) - -# ffmpeg -f lavfi -i testsrc2=duration=2:size=1280x720:rate=30 -c:v libx264 -profile:v baseline -level 3.1 -pix_fmt yuv420p -b:v 2500k -r 30 -movflags +faststart output_720p_2s.mp4 -TEST_SRC_2_720P = TestVideo( - filename="testsrc2.mp4", - default_stream_index=0, - stream_infos={ - 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), - }, - frames={0: {}}, # Not needed for now -) -# ffmpeg -f lavfi -i testsrc2=duration=10:size=1280x720:rate=30 -c:v libx265 -crf 23 -preset medium output.mp4 -TEST_SRC_2_720P_H265 = TestVideo( - filename="testsrc2_h265.mp4", - default_stream_index=0, - stream_infos={ - 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), - }, - frames={0: {}}, # Not needed for now -) - -# ffmpeg -f lavfi -i testsrc2=size=1280x720:rate=30:duration=1 -c:v libvpx-vp9 -b:v 1M output_vp9.webm -TEST_SRC_2_720P_VP9 = TestVideo( - filename="testsrc2_vp9.webm", - default_stream_index=0, - stream_infos={ - 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), - }, - frames={0: {}}, # Not needed for now -) - -# ffmpeg -f lavfi -i testsrc2=size=1280x720:rate=30:duration=1 -c:v libvpx -b:v 1M output_vp8.webm -TEST_SRC_2_720P_VP8 = TestVideo( - filename="testsrc2_vp8.webm", - default_stream_index=0, - stream_infos={ - 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), - }, - frames={0: {}}, # Not needed for now -) - -# ffmpeg -f lavfi -i testsrc2=size=1280x720:rate=30:duration=1 -c:v mpeg4 -q:v 5 output_mpeg4.avi -TEST_SRC_2_720P_MPEG4 = TestVideo( - filename="testsrc2_mpeg4.avi", - default_stream_index=0, - stream_infos={ - 0: TestVideoStreamInfo(width=1280, height=720, num_color_channels=3), - }, - frames={0: {}}, # Not needed for now -) - - -def supports_approximate_mode(asset: TestVideo) -> bool: - # Those are missing the `duration` field so they fail in approximate mode (on all devices). - # TODO: we should address this, see - # https://github.com/meta-pytorch/torchcodec/issues/945 - return asset not in (AV1_VIDEO, TEST_SRC_2_720P_VP9, TEST_SRC_2_720P_VP8) From 46aae2d61a1320944ca211c1b5913b4f4f922c98 Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Mon, 20 Oct 2025 17:58:38 -0400 Subject: [PATCH 16/20] Decoder-native transforms benchmark (#982) --- benchmarks/decoders/benchmark_transforms.py | 164 ++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 benchmarks/decoders/benchmark_transforms.py diff --git a/benchmarks/decoders/benchmark_transforms.py b/benchmarks/decoders/benchmark_transforms.py new file mode 100644 index 000000000..75a49d63b --- /dev/null +++ b/benchmarks/decoders/benchmark_transforms.py @@ -0,0 +1,164 @@ +import math +from argparse import ArgumentParser +from pathlib import Path +from time import perf_counter_ns + +import torch +from torch import Tensor +from torchcodec._core import add_video_stream, create_from_file, get_frames_by_pts +from torchcodec.decoders import VideoDecoder +from torchvision.transforms import v2 + +DEFAULT_NUM_EXP = 20 + + +def bench(f, *args, num_exp=DEFAULT_NUM_EXP, warmup=1) -> Tensor: + + for _ in range(warmup): + f(*args) + + times = [] + for _ in range(num_exp): + start = perf_counter_ns() + f(*args) + end = perf_counter_ns() + times.append(end - start) + return torch.tensor(times).float() + + +def report_stats(times: Tensor, unit: str = "ms", prefix: str = "") -> float: + mul = { + "ns": 1, + "µs": 1e-3, + "ms": 1e-6, + "s": 1e-9, + }[unit] + times = times * mul + std = times.std().item() + med = times.median().item() + mean = times.mean().item() + min = times.min().item() + max = times.max().item() + print( + f"{prefix:<45} {med = :.2f}, {mean = :.2f} +- {std:.2f}, {min = :.2f}, {max = :.2f} - in {unit}" + ) + + +def torchvision_resize( + path: Path, pts_seconds: list[float], dims: tuple[int, int] +) -> None: + decoder = create_from_file(str(path), seek_mode="approximate") + add_video_stream(decoder) + raw_frames, *_ = get_frames_by_pts(decoder, timestamps=pts_seconds) + return v2.functional.resize(raw_frames, size=dims) + + +def torchvision_crop( + path: Path, pts_seconds: list[float], dims: tuple[int, int], x: int, y: int +) -> None: + decoder = create_from_file(str(path), seek_mode="approximate") + add_video_stream(decoder) + raw_frames, *_ = get_frames_by_pts(decoder, timestamps=pts_seconds) + return v2.functional.crop(raw_frames, top=y, left=x, height=dims[0], width=dims[1]) + + +def decoder_native_resize( + path: Path, pts_seconds: list[float], dims: tuple[int, int] +) -> None: + decoder = create_from_file(str(path), seek_mode="approximate") + add_video_stream(decoder, transform_specs=f"resize, {dims[0]}, {dims[1]}") + return get_frames_by_pts(decoder, timestamps=pts_seconds)[0] + + +def decoder_native_crop( + path: Path, pts_seconds: list[float], dims: tuple[int, int], x: int, y: int +) -> None: + decoder = create_from_file(str(path), seek_mode="approximate") + add_video_stream(decoder, transform_specs=f"crop, {dims[0]}, {dims[1]}, {x}, {y}") + return get_frames_by_pts(decoder, timestamps=pts_seconds)[0] + + +def main(): + parser = ArgumentParser() + parser.add_argument("--path", type=str, help="path to file", required=True) + parser.add_argument( + "--num-exp", + type=int, + default=DEFAULT_NUM_EXP, + help="number of runs to average over", + ) + + args = parser.parse_args() + path = Path(args.path) + + metadata = VideoDecoder(path).metadata + duration = metadata.duration_seconds + + print( + f"Benchmarking {path.name}, duration: {duration}, codec: {metadata.codec}, averaging over {args.num_exp} runs:" + ) + + input_height = metadata.height + input_width = metadata.width + fraction_of_total_frames_to_sample = [0.005, 0.01, 0.05, 0.1] + fraction_of_input_dimensions = [0.5, 0.25, 0.125] + + for num_fraction in fraction_of_total_frames_to_sample: + num_frames_to_sample = math.ceil(metadata.num_frames * num_fraction) + print( + f"Sampling {num_fraction * 100}%, {num_frames_to_sample}, of {metadata.num_frames} frames" + ) + uniform_timestamps = [ + i * duration / num_frames_to_sample for i in range(num_frames_to_sample) + ] + + for dims_fraction in fraction_of_input_dimensions: + dims = (int(input_height * dims_fraction), int(input_width * dims_fraction)) + + times = bench( + torchvision_resize, path, uniform_timestamps, dims, num_exp=args.num_exp + ) + report_stats(times, prefix=f"torchvision_resize({dims})") + + times = bench( + decoder_native_resize, + path, + uniform_timestamps, + dims, + num_exp=args.num_exp, + ) + report_stats(times, prefix=f"decoder_native_resize({dims})") + print() + + center_x = (input_height - dims[0]) // 2 + center_y = (input_width - dims[1]) // 2 + times = bench( + torchvision_crop, + path, + uniform_timestamps, + dims, + center_x, + center_y, + num_exp=args.num_exp, + ) + report_stats( + times, prefix=f"torchvision_crop({dims}, {center_x}, {center_y})" + ) + + times = bench( + decoder_native_crop, + path, + uniform_timestamps, + dims, + center_x, + center_y, + num_exp=args.num_exp, + ) + report_stats( + times, prefix=f"decoder_native_crop({dims}, {center_x}, {center_y})" + ) + print() + + +if __name__ == "__main__": + main() From 71ecddcd3183c0706298fff6c0d6939b0a48ffb8 Mon Sep 17 00:00:00 2001 From: Dan-Flores Date: Mon, 20 Oct 2025 16:25:25 -0700 Subject: [PATCH 17/20] Add to_file_like support for VideoEncoder (#958) Co-authored-by: Daniel Flores --- src/torchcodec/_core/__init__.py | 1 + src/torchcodec/_core/custom_ops.cpp | 27 +++++++ src/torchcodec/_core/ops.py | 41 ++++++++++ test/test_ops.py | 116 ++++++++++++++++++++++++++-- 4 files changed, 178 insertions(+), 7 deletions(-) diff --git a/src/torchcodec/_core/__init__.py b/src/torchcodec/_core/__init__.py index eb8dd9697..e04fcb8bd 100644 --- a/src/torchcodec/_core/__init__.py +++ b/src/torchcodec/_core/__init__.py @@ -26,6 +26,7 @@ encode_audio_to_file_like, encode_audio_to_tensor, encode_video_to_file, + encode_video_to_file_like, encode_video_to_tensor, get_ffmpeg_library_versions, get_frame_at_index, diff --git a/src/torchcodec/_core/custom_ops.cpp b/src/torchcodec/_core/custom_ops.cpp index 466ebe50d..4e8fb54bb 100644 --- a/src/torchcodec/_core/custom_ops.cpp +++ b/src/torchcodec/_core/custom_ops.cpp @@ -40,6 +40,8 @@ TORCH_LIBRARY(torchcodec_ns, m) { "encode_video_to_file(Tensor frames, int frame_rate, str filename, int? crf=None) -> ()"); m.def( "encode_video_to_tensor(Tensor frames, int frame_rate, str format, int? crf=None) -> Tensor"); + m.def( + "_encode_video_to_file_like(Tensor frames, int frame_rate, str format, int file_like_context, int? crf=None) -> ()"); m.def( "create_from_tensor(Tensor video_tensor, str? seek_mode=None) -> Tensor"); m.def( @@ -628,6 +630,30 @@ at::Tensor encode_video_to_tensor( .encodeToTensor(); } +void _encode_video_to_file_like( + const at::Tensor& frames, + int64_t frame_rate, + std::string_view format, + int64_t file_like_context, + std::optional crf = std::nullopt) { + auto fileLikeContext = + reinterpret_cast(file_like_context); + TORCH_CHECK( + fileLikeContext != nullptr, "file_like_context must be a valid pointer"); + std::unique_ptr avioContextHolder(fileLikeContext); + + VideoStreamOptions videoStreamOptions; + videoStreamOptions.crf = crf; + + VideoEncoder encoder( + frames, + validateInt64ToInt(frame_rate, "frame_rate"), + format, + std::move(avioContextHolder), + videoStreamOptions); + encoder.encode(); +} + // For testing only. We need to implement this operation as a core library // function because what we're testing is round-tripping pts values as // double-precision floating point numbers from C++ to Python and back to C++. @@ -892,6 +918,7 @@ TORCH_LIBRARY_IMPL(torchcodec_ns, CPU, m) { m.impl("_encode_audio_to_file_like", &_encode_audio_to_file_like); m.impl("encode_video_to_file", &encode_video_to_file); m.impl("encode_video_to_tensor", &encode_video_to_tensor); + m.impl("_encode_video_to_file_like", &_encode_video_to_file_like); m.impl("seek_to_pts", &seek_to_pts); m.impl("add_video_stream", &add_video_stream); m.impl("_add_video_stream", &_add_video_stream); diff --git a/src/torchcodec/_core/ops.py b/src/torchcodec/_core/ops.py index 03cf8cf6d..7123c83da 100644 --- a/src/torchcodec/_core/ops.py +++ b/src/torchcodec/_core/ops.py @@ -104,6 +104,9 @@ def load_torchcodec_shared_libraries(): encode_video_to_tensor = torch._dynamo.disallow_in_graph( torch.ops.torchcodec_ns.encode_video_to_tensor.default ) +_encode_video_to_file_like = torch._dynamo.disallow_in_graph( + torch.ops.torchcodec_ns._encode_video_to_file_like.default +) create_from_tensor = torch._dynamo.disallow_in_graph( torch.ops.torchcodec_ns.create_from_tensor.default ) @@ -203,6 +206,33 @@ def encode_audio_to_file_like( ) +def encode_video_to_file_like( + frames: torch.Tensor, + frame_rate: int, + format: str, + file_like: Union[io.RawIOBase, io.BufferedIOBase], + crf: Optional[int] = None, +) -> None: + """Encode video frames to a file-like object. + + Args: + frames: Video frames tensor + frame_rate: Frame rate in frames per second + format: Video format (e.g., "mp4", "mov", "mkv") + file_like: File-like object that supports write() and seek() methods + crf: Optional constant rate factor for encoding quality + """ + assert _pybind_ops is not None + + _encode_video_to_file_like( + frames, + frame_rate, + format, + _pybind_ops.create_file_like_context(file_like, True), # True means for writing + crf, + ) + + def get_frames_at_indices( decoder: torch.Tensor, *, frame_indices: Union[torch.Tensor, list[int]] ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: @@ -302,6 +332,17 @@ def encode_video_to_tensor_abstract( return torch.empty([], dtype=torch.long) +@register_fake("torchcodec_ns::_encode_video_to_file_like") +def _encode_video_to_file_like_abstract( + frames: torch.Tensor, + frame_rate: int, + format: str, + file_like_context: int, + crf: Optional[int] = None, +) -> None: + return + + @register_fake("torchcodec_ns::create_from_tensor") def create_from_tensor_abstract( video_tensor: torch.Tensor, seek_mode: Optional[str] diff --git a/test/test_ops.py b/test/test_ops.py index 7d996f259..627829689 100644 --- a/test/test_ops.py +++ b/test/test_ops.py @@ -28,6 +28,7 @@ create_from_tensor, encode_audio_to_file, encode_video_to_file, + encode_video_to_file_like, encode_video_to_tensor, get_ffmpeg_library_versions, get_frame_at_index, @@ -1151,7 +1152,7 @@ def test_bad_input(self, tmp_path): class TestVideoEncoderOps: - + # TODO-VideoEncoder: Test encoding against different memory layouts (ex. test_contiguity) # TODO-VideoEncoder: Parametrize test after moving to test_encoders def test_bad_input(self, tmp_path): output_file = str(tmp_path / ".mp4") @@ -1219,7 +1220,7 @@ def decode(self, source=None) -> torch.Tensor: @pytest.mark.parametrize( "format", ("mov", "mp4", "mkv", pytest.param("webm", marks=pytest.mark.slow)) ) - @pytest.mark.parametrize("method", ("to_file", "to_tensor")) + @pytest.mark.parametrize("method", ("to_file", "to_tensor", "to_file_like")) def test_video_encoder_round_trip(self, tmp_path, format, method): # Test that decode(encode(decode(frames))) == decode(frames) ffmpeg_version = get_ffmpeg_major_version() @@ -1246,11 +1247,22 @@ def test_video_encoder_round_trip(self, tmp_path, format, method): **params, ) round_trip_frames = self.decode(encoded_path).data - else: # to_tensor + elif method == "to_tensor": encoded_tensor = encode_video_to_tensor( source_frames, format=format, **params ) round_trip_frames = self.decode(encoded_tensor).data + elif method == "to_file_like": + file_like = io.BytesIO() + encode_video_to_file_like( + frames=source_frames, + format=format, + file_like=file_like, + **params, + ) + round_trip_frames = self.decode(file_like.getvalue()).data + else: + raise ValueError(f"Unknown method: {method}") assert source_frames.shape == round_trip_frames.shape assert source_frames.dtype == round_trip_frames.dtype @@ -1279,8 +1291,9 @@ def test_video_encoder_round_trip(self, tmp_path, format, method): pytest.param("webm", marks=pytest.mark.slow), ), ) - def test_against_to_file(self, tmp_path, format): - # Test that to_file and to_tensor produce the same results + @pytest.mark.parametrize("method", ("to_tensor", "to_file_like")) + def test_against_to_file(self, tmp_path, format, method): + # Test that to_file, to_tensor, and to_file_like produce the same results ffmpeg_version = get_ffmpeg_major_version() if format == "webm" and ( ffmpeg_version == 4 or (IS_WINDOWS and ffmpeg_version in (6, 7)) @@ -1292,11 +1305,24 @@ def test_against_to_file(self, tmp_path, format): encoded_file = tmp_path / f"output.{format}" encode_video_to_file(frames=source_frames, filename=str(encoded_file), **params) - encoded_tensor = encode_video_to_tensor(source_frames, format=format, **params) + + if method == "to_tensor": + encoded_output = encode_video_to_tensor( + source_frames, format=format, **params + ) + else: # to_file_like + file_like = io.BytesIO() + encode_video_to_file_like( + frames=source_frames, + file_like=file_like, + format=format, + **params, + ) + encoded_output = file_like.getvalue() torch.testing.assert_close( self.decode(encoded_file).data, - self.decode(encoded_tensor).data, + self.decode(encoded_output).data, atol=0, rtol=0, ) @@ -1379,6 +1405,82 @@ def test_video_encoder_against_ffmpeg_cli(self, tmp_path, format): ff_frame, enc_frame, percentage=percentage, atol=2 ) + def test_to_file_like_custom_file_object(self): + """Test with a custom file-like object that implements write and seek.""" + + class CustomFileObject: + def __init__(self): + self._file = io.BytesIO() + + def write(self, data): + return self._file.write(data) + + def seek(self, offset, whence=0): + return self._file.seek(offset, whence) + + def get_encoded_data(self): + return self._file.getvalue() + + source_frames = self.decode(TEST_SRC_2_720P.path).data + file_like = CustomFileObject() + encode_video_to_file_like( + source_frames, frame_rate=30, crf=0, format="mp4", file_like=file_like + ) + decoded_samples = self.decode(file_like.get_encoded_data()) + + torch.testing.assert_close( + decoded_samples.data, + source_frames, + atol=2, + rtol=0, + ) + + def test_to_file_like_real_file(self, tmp_path): + """Test to_file_like with a real file opened in binary write mode.""" + source_frames = self.decode(TEST_SRC_2_720P.path).data + file_path = tmp_path / "test_file_like.mp4" + + with open(file_path, "wb") as file_like: + encode_video_to_file_like( + source_frames, frame_rate=30, crf=0, format="mp4", file_like=file_like + ) + decoded_samples = self.decode(str(file_path)) + + torch.testing.assert_close( + decoded_samples.data, + source_frames, + atol=2, + rtol=0, + ) + + def test_to_file_like_bad_methods(self): + source_frames = self.decode(TEST_SRC_2_720P.path).data + + class NoWriteMethod: + def seek(self, offset, whence=0): + return 0 + + with pytest.raises( + RuntimeError, match="File like object must implement a write method" + ): + encode_video_to_file_like( + source_frames, + frame_rate=30, + format="mp4", + file_like=NoWriteMethod(), + ) + + class NoSeekMethod: + def write(self, data): + return len(data) + + with pytest.raises( + RuntimeError, match="File like object must implement a seek method" + ): + encode_video_to_file_like( + source_frames, frame_rate=30, format="mp4", file_like=NoSeekMethod() + ) + if __name__ == "__main__": pytest.main() From f2a64577c3a2605996732b1a908bfbf3fa64c653 Mon Sep 17 00:00:00 2001 From: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com> Date: Tue, 21 Oct 2025 23:46:03 +0800 Subject: [PATCH 18/20] Fix typo in FFmpeg description (#991) --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 8dea1dc8b..85f9a067c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,7 +11,7 @@ We achieve these capabilities through: * Pythonic APIs that mirror Python and PyTorch conventions. * Relying on `FFmpeg `_ to do the decoding / encoding. - TorchCodec uses the version of FFmpeg you already have installed. FMPEG is a + TorchCodec uses the version of FFmpeg you already have installed. FFmpeg is a mature library with broad coverage available on most systems. It is, however, not easy to use. TorchCodec abstracts FFmpeg's complexity to ensure it is used correctly and efficiently. From 44a29d0182470c55728c5efb0a18215cf3d6a016 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Tue, 21 Oct 2025 16:59:27 +0100 Subject: [PATCH 19/20] Add `_core._get_backend_details()` utility for testing and debugging (#987) --- .../_core/BetaCudaDeviceInterface.cpp | 5 +++++ .../_core/BetaCudaDeviceInterface.h | 2 ++ src/torchcodec/_core/CpuDeviceInterface.cpp | 4 ++++ src/torchcodec/_core/CpuDeviceInterface.h | 2 ++ src/torchcodec/_core/CudaDeviceInterface.cpp | 11 ++++++++++ src/torchcodec/_core/CudaDeviceInterface.h | 4 ++++ src/torchcodec/_core/DeviceInterface.h | 4 ++++ src/torchcodec/_core/SingleStreamDecoder.cpp | 5 +++++ src/torchcodec/_core/SingleStreamDecoder.h | 2 ++ src/torchcodec/_core/__init__.py | 1 + src/torchcodec/_core/custom_ops.cpp | 8 +++++++ src/torchcodec/_core/ops.py | 6 +++++ test/test_decoders.py | 22 +++---------------- 13 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp index 7124e4309..07ed92126 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp @@ -699,4 +699,9 @@ void BetaCudaDeviceInterface::convertAVFrameToFrameOutput( avFrame, device_, nppCtx_, nvdecStream, preAllocatedOutputTensor); } +std::string BetaCudaDeviceInterface::getDetails() { + return std::string("Beta CUDA Device Interface. Using ") + + (cpuFallback_ ? "CPU fallback." : "NVDEC."); +} + } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.h b/src/torchcodec/_core/BetaCudaDeviceInterface.h index 7424a877d..3a9520867 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.h +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.h @@ -59,6 +59,8 @@ class BetaCudaDeviceInterface : public DeviceInterface { int frameReadyForDecoding(CUVIDPICPARAMS* picParams); int frameReadyInDisplayOrder(CUVIDPARSERDISPINFO* dispInfo); + std::string getDetails() override; + private: int sendCuvidPacket(CUVIDSOURCEDATAPACKET& cuvidPacket); diff --git a/src/torchcodec/_core/CpuDeviceInterface.cpp b/src/torchcodec/_core/CpuDeviceInterface.cpp index 0e9b46434..5aa20b09e 100644 --- a/src/torchcodec/_core/CpuDeviceInterface.cpp +++ b/src/torchcodec/_core/CpuDeviceInterface.cpp @@ -346,4 +346,8 @@ torch::Tensor CpuDeviceInterface::convertAVFrameToTensorUsingFilterGraph( return rgbAVFrameToTensor(filterGraph_->convert(avFrame)); } +std::string CpuDeviceInterface::getDetails() { + return std::string("CPU Device Interface."); +} + } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/CpuDeviceInterface.h b/src/torchcodec/_core/CpuDeviceInterface.h index 9f44c4e8c..3f6f7c962 100644 --- a/src/torchcodec/_core/CpuDeviceInterface.h +++ b/src/torchcodec/_core/CpuDeviceInterface.h @@ -39,6 +39,8 @@ class CpuDeviceInterface : public DeviceInterface { std::optional preAllocatedOutputTensor = std::nullopt) override; + std::string getDetails() override; + private: int convertAVFrameToTensorUsingSwScale( const UniqueAVFrame& avFrame, diff --git a/src/torchcodec/_core/CudaDeviceInterface.cpp b/src/torchcodec/_core/CudaDeviceInterface.cpp index 01fdac827..be45050e6 100644 --- a/src/torchcodec/_core/CudaDeviceInterface.cpp +++ b/src/torchcodec/_core/CudaDeviceInterface.cpp @@ -284,9 +284,12 @@ void CudaDeviceInterface::convertAVFrameToFrameOutput( frameOutput.data = cpuFrameOutput.data.to(device_); } + usingCPUFallback_ = true; return; } + usingCPUFallback_ = 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 the NPP color conversion routines expect. This SHOULD @@ -351,4 +354,12 @@ std::optional CudaDeviceInterface::findCodec( return std::nullopt; } +std::string CudaDeviceInterface::getDetails() { + // 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. + return std::string("FFmpeg CUDA Device Interface. Using ") + + (usingCPUFallback_ ? "CPU fallback." : "NVDEC."); +} + } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/CudaDeviceInterface.h b/src/torchcodec/_core/CudaDeviceInterface.h index d240066f4..9f171ee3c 100644 --- a/src/torchcodec/_core/CudaDeviceInterface.h +++ b/src/torchcodec/_core/CudaDeviceInterface.h @@ -40,6 +40,8 @@ class CudaDeviceInterface : public DeviceInterface { std::optional preAllocatedOutputTensor = std::nullopt) override; + std::string getDetails() override; + private: // Our CUDA decoding code assumes NV12 format. In order to handle other // kinds of input, we need to convert them to NV12. Our current implementation @@ -60,6 +62,8 @@ class CudaDeviceInterface : public DeviceInterface { // maybeConvertAVFrameToNV12(). std::unique_ptr nv12ConversionContext_; std::unique_ptr nv12Conversion_; + + bool usingCPUFallback_ = false; }; } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/DeviceInterface.h b/src/torchcodec/_core/DeviceInterface.h index 8aad60f24..773317e83 100644 --- a/src/torchcodec/_core/DeviceInterface.h +++ b/src/torchcodec/_core/DeviceInterface.h @@ -119,6 +119,10 @@ class DeviceInterface { avcodec_flush_buffers(codecContext_.get()); } + virtual std::string getDetails() { + return ""; + } + protected: torch::Device device_; SharedAVCodecContext codecContext_; diff --git a/src/torchcodec/_core/SingleStreamDecoder.cpp b/src/torchcodec/_core/SingleStreamDecoder.cpp index 2fbc111c1..8d9e9f651 100644 --- a/src/torchcodec/_core/SingleStreamDecoder.cpp +++ b/src/torchcodec/_core/SingleStreamDecoder.cpp @@ -1702,4 +1702,9 @@ double SingleStreamDecoder::getPtsSecondsForFrame(int64_t frameIndex) { streamInfo.allFrames[frameIndex].pts, streamInfo.timeBase); } +std::string SingleStreamDecoder::getDeviceInterfaceDetails() const { + TORCH_CHECK(deviceInterface_ != nullptr, "Device interface doesn't exist."); + return deviceInterface_->getDetails(); +} + } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/SingleStreamDecoder.h b/src/torchcodec/_core/SingleStreamDecoder.h index 06ea0cd04..4d4c11aa2 100644 --- a/src/torchcodec/_core/SingleStreamDecoder.h +++ b/src/torchcodec/_core/SingleStreamDecoder.h @@ -186,6 +186,8 @@ class SingleStreamDecoder { DecodeStats getDecodeStats() const; void resetDecodeStats(); + std::string getDeviceInterfaceDetails() const; + private: // -------------------------------------------------------------------------- // STREAMINFO AND ASSOCIATED STRUCTS diff --git a/src/torchcodec/_core/__init__.py b/src/torchcodec/_core/__init__.py index e04fcb8bd..55ff697b3 100644 --- a/src/torchcodec/_core/__init__.py +++ b/src/torchcodec/_core/__init__.py @@ -14,6 +14,7 @@ ) from .ops import ( _add_video_stream, + _get_backend_details, _get_key_frame_indices, _test_frame_pts_equality, add_audio_stream, diff --git a/src/torchcodec/_core/custom_ops.cpp b/src/torchcodec/_core/custom_ops.cpp index 4e8fb54bb..c6204de8c 100644 --- a/src/torchcodec/_core/custom_ops.cpp +++ b/src/torchcodec/_core/custom_ops.cpp @@ -74,6 +74,7 @@ TORCH_LIBRARY(torchcodec_ns, m) { m.def( "get_stream_json_metadata(Tensor(a!) decoder, int stream_index) -> str"); m.def("_get_json_ffmpeg_library_versions() -> str"); + m.def("_get_backend_details(Tensor(a!) decoder) -> str"); m.def( "_test_frame_pts_equality(Tensor(a!) decoder, *, int frame_index, float pts_seconds_to_test) -> bool"); m.def("scan_all_streams_to_update_metadata(Tensor(a!) decoder) -> ()"); @@ -895,6 +896,11 @@ std::string _get_json_ffmpeg_library_versions() { return ss.str(); } +std::string get_backend_details(at::Tensor& decoder) { + auto videoDecoder = unwrapTensorToGetDecoder(decoder); + return videoDecoder->getDeviceInterfaceDetails(); +} + // Scans video packets to get more accurate metadata like frame count, exact // keyframe positions, etc. Exact keyframe positions are useful for efficient // accurate seeking. Note that this function reads the entire video but it does @@ -939,6 +945,8 @@ TORCH_LIBRARY_IMPL(torchcodec_ns, CPU, m) { m.impl( "scan_all_streams_to_update_metadata", &scan_all_streams_to_update_metadata); + + m.impl("_get_backend_details", &get_backend_details); } } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/ops.py b/src/torchcodec/_core/ops.py index 7123c83da..32995c964 100644 --- a/src/torchcodec/_core/ops.py +++ b/src/torchcodec/_core/ops.py @@ -142,6 +142,7 @@ def load_torchcodec_shared_libraries(): _get_json_ffmpeg_library_versions = ( torch.ops.torchcodec_ns._get_json_ffmpeg_library_versions.default ) +_get_backend_details = torch.ops.torchcodec_ns._get_backend_details.default # ============================= @@ -550,3 +551,8 @@ def scan_all_streams_to_update_metadata_abstract(decoder: torch.Tensor) -> None: def get_ffmpeg_library_versions(): versions_json = _get_json_ffmpeg_library_versions() return json.loads(versions_json) + + +@register_fake("torchcodec_ns::_get_backend_details") +def _get_backend_details_abstract(decoder: torch.Tensor) -> str: + return "" diff --git a/test/test_decoders.py b/test/test_decoders.py index 098e4e969..6e08e05a4 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1738,26 +1738,10 @@ def test_set_cuda_backend(self): with set_cuda_backend("BETA"): assert _get_cuda_backend() == "beta" - def assert_decoder_uses(decoder, *, expected_backend): - # TODO: This doesn't work anymore after - # https://github.com/meta-pytorch/torchcodec/pull/977 - # We need to define a better way to assert which backend a decoder - # is using. - return - # Assert that a decoder instance is using a given backend. - # - # We know H265_VIDEO fails on the BETA backend while it works on the - # ffmpeg one. - # if expected_backend == "ffmpeg": - # decoder.get_frame_at(0) # this would fail if this was BETA - # else: - # with pytest.raises(RuntimeError, match="Video is too small"): - # decoder.get_frame_at(0) - # Check that the default is the ffmpeg backend assert _get_cuda_backend() == "ffmpeg" dec = VideoDecoder(H265_VIDEO.path, device="cuda") - assert_decoder_uses(dec, expected_backend="ffmpeg") + assert _core._get_backend_details(dec._decoder).startswith("FFmpeg CUDA") # Check the setting "beta" effectively uses the BETA backend. # We also show that the affects decoder creation only. When the decoder @@ -1766,9 +1750,9 @@ def assert_decoder_uses(decoder, *, expected_backend): with set_cuda_backend("beta"): dec = VideoDecoder(H265_VIDEO.path, device="cuda") assert _get_cuda_backend() == "ffmpeg" - assert_decoder_uses(dec, expected_backend="beta") + assert _core._get_backend_details(dec._decoder).startswith("Beta CUDA") with set_cuda_backend("ffmpeg"): - assert_decoder_uses(dec, expected_backend="beta") + assert _core._get_backend_details(dec._decoder).startswith("Beta CUDA") # Hacky way to ensure passing "cuda:1" is supported by both backends. We # just check that there's an error when passing cuda:N where N is too From 0d43e0c3d14440cdb403c63f826d7377d564ff70 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Mon, 27 Oct 2025 17:08:55 +0000 Subject: [PATCH 20/20] Remove hard dep on `nvcuvid.so` and fallback to CPU if it is not available. (#996) --- .../_core/BetaCudaDeviceInterface.cpp | 19 +- .../_core/BetaCudaDeviceInterface.h | 1 + src/torchcodec/_core/CMakeLists.txt | 20 +- src/torchcodec/_core/NVCUVIDRuntimeLoader.cpp | 320 ++++++++++++++++++ src/torchcodec/_core/NVCUVIDRuntimeLoader.h | 14 + src/torchcodec/_core/NVDECCache.h | 2 + 6 files changed, 353 insertions(+), 23 deletions(-) create mode 100644 src/torchcodec/_core/NVCUVIDRuntimeLoader.cpp create mode 100644 src/torchcodec/_core/NVCUVIDRuntimeLoader.h diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp index 07ed92126..b0caa9705 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.cpp +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.cpp @@ -15,7 +15,7 @@ #include "src/torchcodec/_core/FFMPEGCommon.h" #include "src/torchcodec/_core/NVDECCache.h" -// #include // For cudaStreamSynchronize +#include "src/torchcodec/_core/NVCUVIDRuntimeLoader.h" #include "src/torchcodec/_core/nvcuvid_include/cuviddec.h" #include "src/torchcodec/_core/nvcuvid_include/nvcuvid.h" @@ -155,6 +155,7 @@ std::optional validateCodecSupport(AVCodecID codecId) { bool nativeNVDECSupport(const SharedAVCodecContext& codecContext) { // Return true iff the input video stream is supported by our NVDEC // implementation. + auto codecType = validateCodecSupport(codecContext->codec_id); if (!codecType.has_value()) { return false; @@ -222,6 +223,8 @@ BetaCudaDeviceInterface::BetaCudaDeviceInterface(const torch::Device& device) initializeCudaContextWithPytorch(device_); nppCtx_ = getNppStreamContext(device_); + + nvcuvidAvailable_ = loadNVCUVIDLibrary(); } BetaCudaDeviceInterface::~BetaCudaDeviceInterface() { @@ -249,7 +252,7 @@ void BetaCudaDeviceInterface::initialize( const AVStream* avStream, const UniqueDecodingAVFormatContext& avFormatCtx, [[maybe_unused]] const SharedAVCodecContext& codecContext) { - if (!nativeNVDECSupport(codecContext)) { + if (!nvcuvidAvailable_ || !nativeNVDECSupport(codecContext)) { cpuFallback_ = createDeviceInterface(torch::kCPU); TORCH_CHECK( cpuFallback_ != nullptr, "Failed to create CPU device interface"); @@ -700,8 +703,16 @@ void BetaCudaDeviceInterface::convertAVFrameToFrameOutput( } std::string BetaCudaDeviceInterface::getDetails() { - return std::string("Beta CUDA Device Interface. Using ") + - (cpuFallback_ ? "CPU fallback." : "NVDEC."); + std::string details = "Beta CUDA Device Interface."; + if (cpuFallback_) { + details += " Using CPU fallback."; + if (!nvcuvidAvailable_) { + details += " NVCUVID not available!"; + } + } else { + details += " Using NVDEC."; + } + return details; } } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/BetaCudaDeviceInterface.h b/src/torchcodec/_core/BetaCudaDeviceInterface.h index 3a9520867..29511df50 100644 --- a/src/torchcodec/_core/BetaCudaDeviceInterface.h +++ b/src/torchcodec/_core/BetaCudaDeviceInterface.h @@ -98,6 +98,7 @@ class BetaCudaDeviceInterface : public DeviceInterface { UniqueNppContext nppCtx_; std::unique_ptr cpuFallback_; + bool nvcuvidAvailable_ = false; }; } // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/CMakeLists.txt b/src/torchcodec/_core/CMakeLists.txt index 75d1b036c..6b4ccb5d4 100644 --- a/src/torchcodec/_core/CMakeLists.txt +++ b/src/torchcodec/_core/CMakeLists.txt @@ -99,7 +99,7 @@ function(make_torchcodec_libraries ) if(ENABLE_CUDA) - list(APPEND core_sources CudaDeviceInterface.cpp BetaCudaDeviceInterface.cpp NVDECCache.cpp CUDACommon.cpp) + list(APPEND core_sources CudaDeviceInterface.cpp BetaCudaDeviceInterface.cpp NVDECCache.cpp CUDACommon.cpp NVCUVIDRuntimeLoader.cpp) endif() set(core_library_dependencies @@ -108,27 +108,9 @@ function(make_torchcodec_libraries ) if(ENABLE_CUDA) - # Try to find NVCUVID. Try the normal way first. This should work locally. - find_library(NVCUVID_LIBRARY NAMES nvcuvid) - # If not found, try with version suffix, or hardcoded path. Appears - # to be necessary on the CI. - if(NOT NVCUVID_LIBRARY) - find_library(NVCUVID_LIBRARY NAMES nvcuvid.1 PATHS /usr/lib64 /usr/lib) - endif() - if(NOT NVCUVID_LIBRARY) - set(NVCUVID_LIBRARY "/usr/lib64/libnvcuvid.so.1") - endif() - - if(NVCUVID_LIBRARY) - message(STATUS "Found NVCUVID: ${NVCUVID_LIBRARY}") - else() - message(FATAL_ERROR "Could not find NVCUVID library") - endif() - list(APPEND core_library_dependencies ${CUDA_nppi_LIBRARY} ${CUDA_nppicc_LIBRARY} - ${NVCUVID_LIBRARY} ) endif() diff --git a/src/torchcodec/_core/NVCUVIDRuntimeLoader.cpp b/src/torchcodec/_core/NVCUVIDRuntimeLoader.cpp new file mode 100644 index 000000000..2bb501fc2 --- /dev/null +++ b/src/torchcodec/_core/NVCUVIDRuntimeLoader.cpp @@ -0,0 +1,320 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +#ifdef FBCODE_CAFFE2 +// No need to do anything on fbcode. NVCUVID is available there, we can take a +// hard dependency on it. +// The FBCODE_CAFFE2 macro is defined in the upstream fbcode build of torch, so +// we can rely on it, that's what torch does too. + +namespace facebook::torchcodec { +bool loadNVCUVIDLibrary() { + return true; +} +} // namespace facebook::torchcodec +#else + +#include "src/torchcodec/_core/NVCUVIDRuntimeLoader.h" + +#include "src/torchcodec/_core/nvcuvid_include/cuviddec.h" +#include "src/torchcodec/_core/nvcuvid_include/nvcuvid.h" + +#include +#include +#include + +#if defined(WIN64) || defined(_WIN64) +#include +typedef HMODULE tHandle; +#else +#include +typedef void* tHandle; +#endif + +namespace facebook::torchcodec { + +/* clang-format off */ +// This file defines the logic to load the NVCUVID library **at runtime**, +// along with the corresponding NVCUVID functions that we'll need. +// +// We do this because we *do not want* to link (statically or dynamically) +// against libnvcuvid.so: it is not always available on the users machine! If we +// were to link against libnvcuvid.so, that would mean that our +// libtorchcodec_coreN.so would try to look for it when loaded at import time. +// And if it's not on the users machine, that causes `import torchcodec` to +// fail. Source: that's what we did, and we got user reports. +// +// So, we don't link against libnvcuvid.so. But we still want to call its +// functions. So here's how it's done, we'll use cuvidCreateVideoParser as an +// example, but it works the same for all. We are largely following the +// instructions from the NVCUVID docs: +// https://docs.nvidia.com/video-technologies/video-codec-sdk/13.0/nvdec-video-decoder-api-prog-guide/index.html#dynamic-loading-nvidia-components +// +// This: +// typedef CUresult CUDAAPI tcuvidCreateVideoParser(CUvideoparser*, CUVIDPARSERPARAMS*); +// defines tcuvidCreateVideoParser, which is the *type* of a *function*. +// We define such a function of that type just below with: +// static tcuvidCreateVideoParser* dl_cuvidCreateVideoParser = nullptr; +// "dl" is for "dynamically loaded. For now dl_cuvidCreateVideoParser is +// nullptr, but later it will be a proper function [pointer] that can be called +// with dl_cuvidCreateVideoParser(...); +// +// For that to happen we need to call loadNVCUVIDLibrary(): in there, we first +// dlopen(libnvcuvid.so) which loads the .so somewhere in memory. Then we call +// dlsym(...), which binds dl_cuvidCreateVideoParser to its actual address: it +// literally sets the value of the dl_cuvidCreateVideoParser pointer to the +// address of the actual code section. If all went well, by now, we can safely +// call dl_cuvidCreateVideoParser(...); +// All of that happens at runtime *after* import time, when the first instance +// of the Beta CUDA interface is created, i.e. only when the user explicitly +// requests it. +// +// At the bottom of this file we have an `extern "C"` section with function +// definitions like: +// +// CUresult CUDAAPI cuvidCreateVideoParser( +// CUvideoparser* videoParser, +// CUVIDPARSERPARAMS* parserParams) {...} +// +// These are the actual functions that are compiled against and called by the +// Beta CUDA interface code. Crucially, these functions signature match exactly +// the NVCUVID functions (as defined in cuviddec.h). Inside of +// cuvidCreateVideoParser(...) we simply call the dl_cuvidCreateVideoParser +// function [pointer] that we dynamically loaded earlier. +// +// At runtime, within the Beta CUDA interface code we have a fallback mechanism +// to switch back to the CPU backend if any of the NVCUVID functions are not +// available, or if libnvcuvid.so itself couldn't be found. This is what FFmpeg +// does too. + + +// Function pointers types +typedef CUresult CUDAAPI tcuvidCreateVideoParser(CUvideoparser*, CUVIDPARSERPARAMS*); +typedef CUresult CUDAAPI tcuvidParseVideoData(CUvideoparser, CUVIDSOURCEDATAPACKET*); +typedef CUresult CUDAAPI tcuvidDestroyVideoParser(CUvideoparser); +typedef CUresult CUDAAPI tcuvidGetDecoderCaps(CUVIDDECODECAPS*); +typedef CUresult CUDAAPI tcuvidCreateDecoder(CUvideodecoder*, CUVIDDECODECREATEINFO*); +typedef CUresult CUDAAPI tcuvidDestroyDecoder(CUvideodecoder); +typedef CUresult CUDAAPI tcuvidDecodePicture(CUvideodecoder, CUVIDPICPARAMS*); +typedef CUresult CUDAAPI tcuvidMapVideoFrame(CUvideodecoder, int, unsigned int*, unsigned int*, CUVIDPROCPARAMS*); +typedef CUresult CUDAAPI tcuvidUnmapVideoFrame(CUvideodecoder, unsigned int); +typedef CUresult CUDAAPI tcuvidMapVideoFrame64(CUvideodecoder, int, unsigned long long*, unsigned int*, CUVIDPROCPARAMS*); +typedef CUresult CUDAAPI tcuvidUnmapVideoFrame64(CUvideodecoder, unsigned long long); +/* clang-format on */ + +// Global function pointers - will be dynamically loaded +static tcuvidCreateVideoParser* dl_cuvidCreateVideoParser = nullptr; +static tcuvidParseVideoData* dl_cuvidParseVideoData = nullptr; +static tcuvidDestroyVideoParser* dl_cuvidDestroyVideoParser = nullptr; +static tcuvidGetDecoderCaps* dl_cuvidGetDecoderCaps = nullptr; +static tcuvidCreateDecoder* dl_cuvidCreateDecoder = nullptr; +static tcuvidDestroyDecoder* dl_cuvidDestroyDecoder = nullptr; +static tcuvidDecodePicture* dl_cuvidDecodePicture = nullptr; +static tcuvidMapVideoFrame* dl_cuvidMapVideoFrame = nullptr; +static tcuvidUnmapVideoFrame* dl_cuvidUnmapVideoFrame = nullptr; +static tcuvidMapVideoFrame64* dl_cuvidMapVideoFrame64 = nullptr; +static tcuvidUnmapVideoFrame64* dl_cuvidUnmapVideoFrame64 = nullptr; + +static tHandle g_nvcuvid_handle = nullptr; +static std::mutex g_nvcuvid_mutex; + +bool isLoaded() { + return ( + g_nvcuvid_handle && dl_cuvidCreateVideoParser && dl_cuvidParseVideoData && + dl_cuvidDestroyVideoParser && dl_cuvidGetDecoderCaps && + dl_cuvidCreateDecoder && dl_cuvidDestroyDecoder && + dl_cuvidDecodePicture && dl_cuvidMapVideoFrame && + dl_cuvidUnmapVideoFrame && dl_cuvidMapVideoFrame64 && + dl_cuvidUnmapVideoFrame64); +} + +template +T* bindFunction(const char* functionName) { +#if defined(WIN64) || defined(_WIN64) + return reinterpret_cast(GetProcAddress(g_nvcuvid_handle, functionName)); +#else + return reinterpret_cast(dlsym(g_nvcuvid_handle, functionName)); +#endif +} + +bool _loadLibrary() { + // Helper that just calls dlopen or equivalent on Windows. In a separate + // function because of the #ifdef uglyness. +#if defined(WIN64) || defined(_WIN64) +#ifdef UNICODE + static LPCWSTR nvcuvidDll = L"nvcuvid.dll"; +#else + static LPCSTR nvcuvidDll = "nvcuvid.dll"; +#endif + g_nvcuvid_handle = LoadLibrary(nvcuvidDll); + if (g_nvcuvid_handle == nullptr) { + return false; + } +#else + g_nvcuvid_handle = dlopen("libnvcuvid.so", RTLD_NOW); + if (g_nvcuvid_handle == nullptr) { + g_nvcuvid_handle = dlopen("libnvcuvid.so.1", RTLD_NOW); + } + if (g_nvcuvid_handle == nullptr) { + return false; + } +#endif + + return true; +} + +bool loadNVCUVIDLibrary() { + // Loads NVCUVID library and all required function pointers. + // Returns true on success, false on failure. + std::lock_guard lock(g_nvcuvid_mutex); + + if (isLoaded()) { + return true; + } + + if (!_loadLibrary()) { + return false; + } + + // Load all function pointers. They'll be set to nullptr if not found. + dl_cuvidCreateVideoParser = + bindFunction("cuvidCreateVideoParser"); + dl_cuvidParseVideoData = + bindFunction("cuvidParseVideoData"); + dl_cuvidDestroyVideoParser = + bindFunction("cuvidDestroyVideoParser"); + dl_cuvidGetDecoderCaps = + bindFunction("cuvidGetDecoderCaps"); + dl_cuvidCreateDecoder = + bindFunction("cuvidCreateDecoder"); + dl_cuvidDestroyDecoder = + bindFunction("cuvidDestroyDecoder"); + dl_cuvidDecodePicture = + bindFunction("cuvidDecodePicture"); + dl_cuvidMapVideoFrame = + bindFunction("cuvidMapVideoFrame"); + dl_cuvidUnmapVideoFrame = + bindFunction("cuvidUnmapVideoFrame"); + dl_cuvidMapVideoFrame64 = + bindFunction("cuvidMapVideoFrame64"); + dl_cuvidUnmapVideoFrame64 = + bindFunction("cuvidUnmapVideoFrame64"); + + return isLoaded(); +} + +} // namespace facebook::torchcodec + +extern "C" { + +CUresult CUDAAPI cuvidCreateVideoParser( + CUvideoparser* videoParser, + CUVIDPARSERPARAMS* parserParams) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidCreateVideoParser, + "cuvidCreateVideoParser called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidCreateVideoParser( + videoParser, parserParams); +} + +CUresult CUDAAPI cuvidParseVideoData( + CUvideoparser videoParser, + CUVIDSOURCEDATAPACKET* cuvidPacket) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidParseVideoData, + "cuvidParseVideoData called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidParseVideoData(videoParser, cuvidPacket); +} + +CUresult CUDAAPI cuvidDestroyVideoParser(CUvideoparser videoParser) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidDestroyVideoParser, + "cuvidDestroyVideoParser called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidDestroyVideoParser(videoParser); +} + +CUresult CUDAAPI cuvidGetDecoderCaps(CUVIDDECODECAPS* caps) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidGetDecoderCaps, + "cuvidGetDecoderCaps called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidGetDecoderCaps(caps); +} + +CUresult CUDAAPI cuvidCreateDecoder( + CUvideodecoder* decoder, + CUVIDDECODECREATEINFO* decoderParams) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidCreateDecoder, + "cuvidCreateDecoder called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidCreateDecoder(decoder, decoderParams); +} + +CUresult CUDAAPI cuvidDestroyDecoder(CUvideodecoder decoder) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidDestroyDecoder, + "cuvidDestroyDecoder called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidDestroyDecoder(decoder); +} + +CUresult CUDAAPI +cuvidDecodePicture(CUvideodecoder decoder, CUVIDPICPARAMS* picParams) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidDecodePicture, + "cuvidDecodePicture called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidDecodePicture(decoder, picParams); +} + +#if !defined(__CUVID_DEVPTR64) || defined(__CUVID_INTERNAL) +// We need to protect the definition of the 32bit versions under the above +// conditions (see cuviddec.h). Defining them unconditionally would cause +// conflict compilation errors when cuviddec.h redefines those to the 64bit +// versions. +CUresult CUDAAPI cuvidMapVideoFrame( + CUvideodecoder decoder, + int pixIndex, + unsigned int* framePtr, + unsigned int* pitch, + CUVIDPROCPARAMS* procParams) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidMapVideoFrame, + "cuvidMapVideoFrame called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidMapVideoFrame( + decoder, pixIndex, framePtr, pitch, procParams); +} + +CUresult CUDAAPI +cuvidUnmapVideoFrame(CUvideodecoder decoder, unsigned int framePtr) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidUnmapVideoFrame, + "cuvidUnmapVideoFrame called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidUnmapVideoFrame(decoder, framePtr); +} +#endif + +CUresult CUDAAPI cuvidMapVideoFrame64( + CUvideodecoder decoder, + int pixIndex, + unsigned long long* framePtr, + unsigned int* pitch, + CUVIDPROCPARAMS* procParams) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidMapVideoFrame64, + "cuvidMapVideoFrame64 called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidMapVideoFrame64( + decoder, pixIndex, framePtr, pitch, procParams); +} + +CUresult CUDAAPI +cuvidUnmapVideoFrame64(CUvideodecoder decoder, unsigned long long framePtr) { + TORCH_CHECK( + facebook::torchcodec::dl_cuvidUnmapVideoFrame64, + "cuvidUnmapVideoFrame64 called but NVCUVID not loaded!"); + return facebook::torchcodec::dl_cuvidUnmapVideoFrame64(decoder, framePtr); +} + +} // extern "C" + +#endif // FBCODE_CAFFE2 diff --git a/src/torchcodec/_core/NVCUVIDRuntimeLoader.h b/src/torchcodec/_core/NVCUVIDRuntimeLoader.h new file mode 100644 index 000000000..e6ee40a05 --- /dev/null +++ b/src/torchcodec/_core/NVCUVIDRuntimeLoader.h @@ -0,0 +1,14 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +#pragma once + +namespace facebook::torchcodec { + +// See note in corresponding cpp file +bool loadNVCUVIDLibrary(); + +} // namespace facebook::torchcodec diff --git a/src/torchcodec/_core/NVDECCache.h b/src/torchcodec/_core/NVDECCache.h index b248ebc68..a0f2fb862 100644 --- a/src/torchcodec/_core/NVDECCache.h +++ b/src/torchcodec/_core/NVDECCache.h @@ -12,6 +12,8 @@ #include #include + +#include "src/torchcodec/_core/NVCUVIDRuntimeLoader.h" #include "src/torchcodec/_core/nvcuvid_include/cuviddec.h" #include "src/torchcodec/_core/nvcuvid_include/nvcuvid.h"